聊聊你不知道的 Objective-C++

发布于: 2017-04-16 13:39
阅读: 2277
评论: 0
喜欢: 19

引言

最近正在在开发一款iOSmacOS通用的OpenCV图像处理开源库。虽然OpenCV存在一套C语言接口,可以直接在 Objective-C 中使用,但不管是从语法层面还是内存管理上来说,它的 C++ 接口更好。而 Objective-C 本身是不支持 C++ 的,因此笔者采用了 Objective-C++ 来开发这套库,并在这里简单探讨一下 Objective-C++ 的内容。

本文中会使用正在开发的开源库EMCVLib中的一些代码作为例子,项目地址:https://github.com/trmbhs/EMCVLib

Objective-C++

什么是 Objective-C++

正如它的名字,Objective-C 加上 C++ 就是 Objective-C++。苹果允许开发者在同一个源文件中混编 Objective-C 和 C++,混编后的语言就称为 Objective-C++。

Objective-C++ 是一门语言,毕竟 GitHub 都能直接认出它。

为什么要使用 Objective-C++

Objective-C 是C语言的扩展,完全兼容C语言,但它并不兼容 C++。市面上有非常多的跨平台开源库使用的是 C++。虽然一部分库提供了C语言接口,但它们并不是那么好用。例如著名的图像库OpenGL,开发者利用C语言接口维护的是一个状态机,一个服务端。又例如Android NDK中的MediaCodec接口,需要频繁传入句柄去操作,这种操作明显是可以被面向对象开发代替的。

说到底,是为了更好地面向对象编程,更好地在苹果各个平台上使用第三方开源的 C++ 库。

如何使用 Objective-C++

在 Xcode 中,正常情况下的 Objective-C 源文件为.m源文件,将后缀名改成.mm即可开启 C++ 支持。在 Objective-C++ 中,开发者可以完美地在 Objective-C 类中嵌入 C++ 属性和方法。

说了这么多,来看一个例子:

#import <Foundation/Foundation.h>
//OpenCV头文件,里面包C++类
#include "opencv.h"
//省略其他头文件
//C++
using namespace std;
using namespace cv;

@interface EMCVSplitedImage : NSObject
//C++
{
@public
    vector<Mat> _mats;
    vector<MatND> _hists;
}
- (instancetype)initWithMats:(vector<Mat>)mats;
- (instancetype)initWithNoCopyMats:(vector<Mat>)mats;

@property (nonatomic, readonly) int channalCount;

- (instancetype)initWithPath:(NSString *)path;

- (EMCVImage *)mergeImage;
//省略其他方法

@end

这是库中的一个类的头文件代码片段。笔者引入了包含 C++ 类的头文件opencv.h,在 Objective-C 类中加入了 C++ 属性_mat_hists。且这两个属性的类型是std::vector<cv::Mat>std::vector<cv::MatND>。同时,在下面的两个构造函数参数中使用了 C++ 数据结构。在 Objective-C++ 中,Objective-C 和 C++ 完美融合了。

#import "EMCVSplitedImage.h"
//省略其他头文件

@implementation EMCVSplitedImage

- (int)channalCount {
    return (int)self->_mats.size();
}

- (instancetype)initWithMats:(vector<Mat>)mats {
    vector<Mat> newMats;
    for_each(mats.cbegin(), mats.cend(), [&newMats](const Mat mat)->void {
        Mat newMat;
        mat.copyTo(newMat);
        newMats.push_back(newMat);
    });
    return [self initWithNoCopyMats:newMats];
}

- (instancetype)initWithNoCopyMats:(vector<Mat>)mats {
    self = [super init];
    if (self) {
        self->_mats = mats;
    }
    return self;
}

- (EMCVImage *)mergeImage {
    return [[EMCVImage alloc] initWithCVSplitedImage:self];
}

//省略其他方法实现

@end

这是EMCVSplitedImage对应的.mm实现文件。在 Objective-C 中,使用object.property来访问对象属性,在 C++ 中,使用object->property*object.property来访问对象属性。而在 Objective-C 中,开发者可以使用这两种方法访问各自的属性。Objective-C 中完全遵守原来的语法规定,C++ 也完全遵守原来的语法规定。同样的,两者完美融合了。

不光如此,C++ 的标准库给开发者带来了大量的工具,例如std::vector向量容器,std::for_each枚举器等。使用其他第三方 C++ 库也是如此,开发者可以在 Objective-C 方法的实现中嵌入 C++ 实现,去构造 C++ 类,去访问 C++ 接口,去使用std的工具类,没有任何问题。 例如这样:

std::vector<NSObject *> vector;
NSObject * obj = [[NSObject alloc] init];
vector.push_back(obj);

类接口多态性

这是 Objective-C++ 中非常有意思的特性,我称它为类接口多态。一个类,可以由两种方式呈现:纯 Objective-C 类和Objective-C++类。以不同语言接入时,这个类会表现出不同的属性,不同的接口。同样的,来看一个例子:

#import <Foundation/Foundation.h>
//OpenCV头文件,里面包C++类
#ifdef __cplusplus
#include "opencv.h"
//省略其他头文件
//C++
using namespace std;
using namespace cv;
#endif

@interface EMCVSplitedImage : NSObject
#ifdef __cplusplus
//C++
{
@public
    vector<Mat> _mats;
    vector<MatND> _hists;
}
- (instancetype)initWithMats:(vector<Mat>)mats;
- (instancetype)initWithNoCopyMats:(vector<Mat>)mats;
#endif

@property (nonatomic, readonly) int channalCount;

- (instancetype)initWithPath:(NSString *)path;

- (EMCVImage *)mergeImage;
//省略其他方法

@end

同样是存在于EMCVLib中的代码片段。由于引入了opencv.h这个头文件,包含了 C++ 相关内容,在纯 Objective-C 类接入这个类时,由于纯 Objective-C 类无法识别 C++ 的关键字,会发生无法编译的问题。这就意味着所有接入这个类的语言都必须是 Objective-C++ 了吗?答案是否定的。

在上面的例子中,笔者加入了预编译命令#ifdef __cplusplus,当外部调用者是纯 Objective-C 时,C++ 相关属性和方法对其是不可见的,对它来说,这就是个纯 Objective-C 类。但自然,它也无法直接访问这些属性和方法,毕竟在例子中,它连参数的类型cv::Mat是个什么东西都不知道。而当接入者是 Objective-C++ 时,它就可以访问所有属性和方法了。

存在的问题

Objective-C++ 如此强大,但也并不是完美的。

Objective-C 的类和 C++ 的类在底层实现上是有很大不同的,最明显的表现就是动态性。Objective-C 的类为了实现动态性,在编译时候是会做很多工作的,例如构建方法表等具体不展开,但是 C++ 不会。Objective-C 非常多的功能都依赖动态性例如 KVO,这些是不能用在 C++ 对象上的。

因此这两者并非完美融合,但是还是有非常有意思的用法,例如把 C++ 对象放进 NSArray

CppObject * cppObj = new CppObject();
NSValue * value = [NSValue valueWithPointer:cppObj];
NSArray * array = @[value];

ARC 桥接

ARC 对象指针向C语言指针转换时需要手动指定是否增加引用计数,这个问题并不是 Objective-C++ 的问题,相信很多开发者已经遇到过了。例如NSObject *转向void *时。

编码体验

将源文件后缀名改为.mm后,Xcode 的代码提示会变得非常非常慢。

结语

Objective-C++ 能帮助开发者更好地在项目中集成 C++ 内容。类接口多态性能帮助开发者在语言层面选择性地暴露接口,在封装 C++ 库的过程中尽可能不将 C++ 细节暴露给用户。

考虑到目前 Swift 不能直接调用 C++ 类,因此本次开源库还是采用了 Objective-C 来开发。


Thanks for reading.

All the best wishes for you! 💕