返回

Qt6 C++头文件调用 Objective-C 代码最佳实践

IOS

Qt6 中在 C++ 头文件使用 Objective-C 代码

在 Qt 项目开发中,有时需要在 C++ 代码中调用 Objective-C 代码,尤其是在涉及到平台特定功能(比如 iOS 的权限请求)时。一般情况下,会使用 .mm 文件来实现 Objective-C 和 C++ 的混编,.mm 文件实际上是一个 Objective-C++ 的源文件。但在某些情景下,例如出于代码组织或者项目结构的考量,可能需要在 C++ 的头文件(.h)中直接包含 Objective-C 代码片段。这带来了一些编译和链接上的挑战。

问题分析

直接在 C++ 头文件中嵌入 Objective-C 代码,主要的难点在于 C++ 编译器无法直接理解 Objective-C 的语法。尝试在 .h 文件里编译 Objective-C 可能会引发编译错误。比如 NSURLUIApplication等Objective-C的类,这些类型需要通过Objective-C的编译器来编译处理,但.h 文件只使用c++ 编译器。

上述示例代码中,第一个requestMedia函数正常运行,这是因为在编译时通过条件编译预处理器#if defined(Q_OS_IOS) 选择合适的代码块,objc运行时 API(objc_getClass, sel_registerName,objc_msgSend),通过objc动态执行消息发送的方式运行objective-c的代码,不需要使用特定的类型。 但 settingsMedia 函数里的 NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; 以及相关 UIApplication 相关操作在 .h 文件编译时就会出错,因为这些需要使用 Objective-C 的特定语法和类型,这些无法直接在头文件中进行编译。

解决方案

为了解决这个问题,需要将 Objective-C 的代码进行一定的包装和处理,保证其能够在 C++ 环境中顺利执行。有两种可行的策略:

方案一:使用 Objective-C++ 源文件(.mm) 作为中间层

  1. 创建 .mm 文件 : 创建一个 Objective-C++ 源文件(比如 permission_ios.mm),并在此文件中编写 settingsMedia 方法所需要的Objective-C代码。
  2. 定义接口 : 在 .mm 文件中声明一个函数或者类方法,这个函数或者类方法提供一个明确的 C++ 接口。 比如定义一个名为 openSettings() 函数。
  3. C++ 头文件声明 : 在原来的 C++ 头文件 permission.h 中声明与 permission_ios.mm 文件中 openSettings 对应的方法声明。比如声明 void openSettings();,这里不需要增加 Objective-C 类型。
  4. 头文件实现调用 : 在C++ 头文件函数 settingsMedia 中,直接调用上一步声明的openSettings方法。这样保证了 C++ 编译器不会去解析具体的 Objective-C 代码。
  5. mm文件定义实现 :在 permission_ios.mm 中实现C++中声明的方法openSettings。并在此函数中完成 objective-c 版本的设置逻辑。

permission.h 的代码修改示例:

 #ifndef PERMISSION_H
    #define PERMISSION_H

    #include <QMainWindow>
    #include <QMessageBox>
    #include <QtCore/QObject>
    #include <QtCore/QMetaObject>
    #include <QUrl>
    #include <QDesktopServices>

    #if defined(Q_OS_ANDROID)
        #include <QtCore/qjniobject.h>
        #include <QtCore/qcoreapplication.h>
        #include <QtCore/private/qandroidextras_p.h>
    #elif defined(Q_OS_IOS)
        #include <objc/objc.h>
        #include <objc/message.h>
        #include <objc/runtime.h>
    #endif

    class Permission : public QObject
    {
        Q_OBJECT
        
        
        public:
            Permission(QObject *parent = nullptr) : QObject(parent) {}

             // Photo Library
            Q_INVOKABLE bool requestMedia() {
                #if defined(Q_OS_ANDROID)
                    // ... 省略 Android 代码
                #elif defined(Q_OS_IOS)
                    __block BOOL isAuthorized;
                    id phPhotoLibrary = (id) objc_getClass("PHPhotoLibrary");
                    SEL mediaPermission = sel_registerName("requestAuthorization:");
                    typedef void (^CompletionHandler)(int);
                    CompletionHandler completionHandler = ^(int status) {
                        switch (status) {
                            case 3:
                                qDebug() << "Permission for photo library access granted.";
                                isAuthorized = YES;
                                break;
                            case 1:
                            case 2:
                                qDebug() << "Permission for photo library access denied.";
                                isAuthorized = NO;
                                break;
                            case 0:
                                qDebug() << "Permission for photo library access not determined.";
                                isAuthorized = NO;
                                break;
                        }
                    };
                    ((void (*)(id, SEL, CompletionHandler))objc_msgSend)(phPhotoLibrary, mediaPermission, completionHandler);
                    return isAuthorized;

                #endif
                return true;   
            }
          // C++ interface for open settings
            #if defined(Q_OS_IOS)
                 void openSettings();
            #endif
            // Photo Library - Settings
             Q_INVOKABLE void settingsMedia() {
                #if defined(Q_OS_ANDROID)
                    // ... 省略 Android 代码
                #elif defined(Q_OS_IOS)
                     openSettings(); // Call C++ interface
                #endif
            }
    };

    #endif // PERMISSION_H

permission_ios.mm 的代码示例:

 #include "permission.h"
 #include <UIKit/UIKit.h>

 #if defined(Q_OS_IOS)

  void Permission::openSettings()
    {

                    NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
                    if ([[UIApplication sharedApplication] canOpenURL:url]) {
                    [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
                      if (success) {
                      qDebug() << "Opened settings";
                    }
                }];
            }

    }

#endif

这个方法比较清晰,.h 文件仅需维护 C++ 接口。编译时 .mm 文件将会作为 Objective-C++ 源文件进行编译,这样就可以解决 .h 文件里不能使用 Objective-C 语法的问题。

方案二: 使用 objc_msgSend 函数进行运行时动态调用

objc_msgSend允许通过选择器动态调用对象的方法。利用 objc_msgSend 以及相关objc 运行时函数,可以在运行时创建和调用 Objective-C 对象及其方法,这提供了一种不依赖显式 Objective-C 语法的实现方案。但是,它通常会降低代码的可读性和维护性,容易出错且不容易调试,并且需要开发者非常清楚地了解 Objective-C 运行时的原理。同时也会弱化编译期的静态类型检查的能力。因此不建议新手使用该方法,仅供进阶的开发人员使用。这里给出的方法是对原先的 objective-c 代码进行运行时转换的方式,只做学习使用:

 // iOS: objective-c script to open app settings and from here user needs to manualy navigate to permisison>camera and switch to allow (same comment as above, if I can open directly in app settings in camera permission, i would like to know how)...
                     // THIS is what does not work... seems that it does not know NSURL
                        //  NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
                       //  if ([[UIApplication sharedApplication] canOpenURL:url]) {
                      //       [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
                       //           if (success) {
                        //             qDebug() << "Opened settings";
                        //      }
                         //  }];
                         //}

                    id uiApplication = (id) objc_getClass("UIApplication");
                    SEL sharedApplicationSelector = sel_registerName("sharedApplication");
                    id sharedApplicationInstance = ((id (*)(id, SEL))objc_msgSend)(uiApplication, sharedApplicationSelector);

                     // Convert UIApplicationOpenSettingsURLString to Objective-C NSString
                     id nsString = (id) objc_getClass("NSString");
                      SEL stringSelector = sel_registerName("stringWithUTF8String:");
                      const char *openSettingsUrl = "UIApplicationOpenSettingsURLString";

                    id openSettingsString = ((id (*)(id, SEL, const char*))objc_msgSend)(nsString, stringSelector, openSettingsUrl);

                  
                     id nsURL = (id) objc_getClass("NSURL");
                     SEL nsURLSelector = sel_registerName("URLWithString:");
                   
                     id nsURLInstance =  ((id (*)(id, SEL,id))objc_msgSend)(nsURL, nsURLSelector, openSettingsString);

                      SEL canOpenURLSelector = sel_registerName("canOpenURL:");
                      bool canOpen =  ((bool (*)(id, SEL,id))objc_msgSend)(sharedApplicationInstance, canOpenURLSelector, nsURLInstance);

                if (canOpen)
                 {
                      SEL openURLWithOptionsCompletionHandlerSelector = sel_registerName("openURL:options:completionHandler:");

                
                       // 创建一个空字典,作为选项参数。需要转换为字典objc对象。
                        id nsDictionary = (id) objc_getClass("NSDictionary");
                         SEL  dictionarySelector =  sel_registerName("dictionary");
                         id optionsDict = ((id(*)(id,SEL))objc_msgSend)(nsDictionary,dictionarySelector);


                          // Create CompletionHandler
                          typedef void (^CompletionHandler)(bool);
                            CompletionHandler completionHandler = ^(bool success){

                           if(success){
                            qDebug() << "Opened settings";

                            }
                         };

                        // 使用 block 调用函数
                        ((void(*)(id,SEL,id, id, CompletionHandler))objc_msgSend)(sharedApplicationInstance, openURLWithOptionsCompletionHandlerSelector, nsURLInstance, optionsDict,completionHandler );
               }

这个方式完全不使用 UIKit 框架中的 UIApplication 以及 NSURL 这样的类型和常量。在 #elif defined(Q_OS_IOS) 分支下直接进行 objective-c 运行时的逻辑,绕开了 .h 文件无法直接编译包含 objective-c 代码的问题,同时不需要单独.mm文件的中间层。

选择哪种方案?

在考虑不同方案时,建议权衡几个关键因素:可读性、可维护性、和性能。

  • 对于大多数应用场景,优先选择方案一, 使用.mm中间层的方法 。方案一使得C++代码结构清晰,并使用户更加方便地利用objective-c的标准语法特性,提高代码可维护性。
  • 对于特殊的运行时,需要动态调用Objective-C 对象及方法并且不方便创建 .mm 文件进行中间层转发的情况,可以使用objc_msgSend 函数动态调用Objective-C对象及方法(即方案二),例如需要在不同框架或者第三方SDK里做适配的情况。 但请仔细考虑方案二潜在的代码维护风险和稳定性。

通过这些步骤和解释,你就可以在 Qt6 项目的 C++ 头文件中有效管理 Objective-C 代码。 记住,合理的代码组织和适当的抽象层级对于构建健壮、易于维护的项目至关重要。