Qt6 C++头文件调用 Objective-C 代码最佳实践
2025-01-03 20:50:26
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 可能会引发编译错误。比如 NSURL
,UIApplication
等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
) 作为中间层
- 创建 .mm 文件 : 创建一个 Objective-C++ 源文件(比如
permission_ios.mm
),并在此文件中编写settingsMedia
方法所需要的Objective-C代码。 - 定义接口 : 在
.mm
文件中声明一个函数或者类方法,这个函数或者类方法提供一个明确的 C++ 接口。 比如定义一个名为openSettings()
函数。 - C++ 头文件声明 : 在原来的 C++ 头文件
permission.h
中声明与permission_ios.mm
文件中openSettings
对应的方法声明。比如声明void openSettings();
,这里不需要增加 Objective-C 类型。 - 头文件实现调用 : 在C++ 头文件函数
settingsMedia
中,直接调用上一步声明的openSettings
方法。这样保证了 C++ 编译器不会去解析具体的 Objective-C 代码。 - 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 代码。 记住,合理的代码组织和适当的抽象层级对于构建健壮、易于维护的项目至关重要。