将Swift与Objective-C相结合(swift和objective-c)
yuyutoo 2024-11-17 03:42 2 浏览 0 评论
原文:Swifty Objective-C
作者:Peter Steinberger / Michael Ochs / Matej Bukovinski
译者:孙薇
审校:唐小引(@唐门教主),欢迎技术投稿、约稿,给文章纠错,请发送邮件tangxy#csdn.net(请将#更换为@)。
Objective-C起源于20世纪80年代初,尽管多年来这种语言有了长足的发展,却仍不敌Swift这样真正的现代化语言。随着Swift 3.0即将上线,使用Swift来编写新的应用会更加智能化。然而在PSPDFKit中,我们仍坚守在Objective-C的世界里。我们建立并分发二进制框架,来渲染编辑PDF文件,想要正确获取所有的PDF细节是非常复杂的。除了核心的PDF功能之外,我们还提供了大量可在典型用例中运用的UI类,从而产生了大约60万行的代码库,包含了UI和封装模型的混合代码——shared C++和Objective-C++都有,header部分完全是现代化的Objective-C,并使用泛型和nullability来注释,确保在Swift中运行良好。
尽管目前我们还处于Objective-C的世界中,但这种情况也并非全然糟糕的:通过一些精巧的设计,在类似我们这样的代码库中甚至可以享用Swift的诸多好处。下面我们列出了一些将“新旧世界”结合起来的方法。
为什么不单纯地使用Swift呢?
我们先来谈谈这个十分明显却无人肯谈的问题。Swift是很棒的语言,有很多原因促使我们使用它,不过也有很多场景和需求使得选择Objective-C更为明智,具体选择哪种语言,要取决于应用及用例本身、你的团队还有项目的范围及本质。在这里苹果给了我们选择,这真是太棒了。
Swift的发展速度快得不可思议。苹果的开放过程简直令人惊异,特别要考虑到这家公司谨言慎行的本质。尽管将Swift最初发布的版本称为1.0版尚且有些草率,不过很快它就发展成了一种快速、安全、可以编写出优雅代码的语言。对于早期的采用者来说,还有很多甚至称得上严重的bug和问题需要解决。对于较小的项目或者典型应用来说,Swift可能运行良好,但大型项目可能会因为编译时间、优化问题或者仅仅缺乏资源来停止开发并花上数周更新数据库到Swift 3(这项任务可能会带来极大的破坏性而推迟采用Swift。
当前形式下的Swift在很多方面与C++很相似,都属于非常静态的类型,在动态消息发送和运行环境方面比不上Objective-C。在过去,这一点虽然导致了很严重的问题出现,比如优化问题或者不应使用的monkey-patching代码,但也带来了优雅的解决方案,比如核心数据对象的动态特性解析,包括
NSManagedObject
、NSUndoManager
、UIAppearance
还有很多其他苹果框架中我们所喜爱的功能。这个问题很难解决,即便是苹果UIKit团队的员工也要谨慎对待这种危险。使用不具有二进制兼容性的Swift语言意味着我们必须对用户关闭一些技术参数,同时他们在选择Xcode时也会有局限——如果我们的SDK仍旧使用Xcode 7.3.0编译,他们也许就不能升级到Xcode 7.3.1。每个极小的编译器版本更改都可能会导致代码与其他版本不能兼容,而我们并不想让用户烦恼这种额外出现的技术复杂性。我们明白,我们是极端的例子,对大多项目来说并不会有太大问题。我们也坚信,推迟采用并等待稳定的Swift ABI是件好事,短期内不够方便,但长期来讲所使用的语言更完善。同时,我们的用户也很在意数据包的大小,也许不会喜欢采用Swift所带来的每个架构6MB的额外负载。由于我们一直支持最近的两版iOS,也就是说至少在最近两年内我们都无法采用Swift。
在编写测试和样例代码时,我们越来越多地使用Swift,也非常喜欢它。但同时,我们也担心Xcode 8的转变与额外的复杂性会对团队产生负担。由于ABI还在变化,我们无法在主要的SDK中使用Swift。因此我们决定采用Objective-C++在恰当的地方对纯粹的Objective-C进行补充。
这种做法看似非常复杂,可能会让很多人产生担心:在代码中加入C++这种可能非常复杂的语言——难以学习甚至难以驾驭,可能会花费很多时间和精力,但实际上并非如此。与其将Objective-C++当作Objective-C加上C++,不如把它当成Objective-C的一个小语种。在我们的Objective-C类中仅使用了极少量的C++,以享受C++的便利、安全性与性能表现。与完全成熟的C++实现不同,在以Objective-C为主的代码库中学习运用一个小的子集非常简单,即便是对于从没有C++经验的开发者来说都是如此。
Objective-C++入门
我们先来看一下在项目中使用Objective-C++所需的步骤,假设已有以Objective-C编写的项目:
- 将想要使用Objective-C++的文件重命名,从
<MyClass>.m
改为<MyClass>.mm
; - 完成,就是这样,不需要步骤二。
真的非常简单,Objective-C与C++具有高度的协作性,因此无需安装任何内容,也完全不用修改设置。当然,也并非所有的C代码都是有效的C++代码,有时可能需要添加一些额外的转换,不过大部分情况都是没问题的。Xcode 7并不支持Objective-C++中的模块,因此必须使用较旧的#import
语法,而不是新的@import
。
现在我们知道,在应用中支持Objective-C++实际上非常简单,来看一下能用它做些什么。下面是我们最喜欢的一些功能:
auto
看下这段代码:
NSArray *files = [NSFileManager.defaultManager contentsOfDirectoryAtURL:samplesURL includingPropertiesForKeys:nil options:0 error:NULL];
PSPDFDocument *document = [[PSPDFDocument alloc] initWithBaseURL:samplesURL files:files];
这实际上是我们测试中的一个bug,在打算将文件名列表作为字符串时,其中有文件包含NSURL
对象。最终由于相关文件被自动过滤掉,测试过了,有一阵子没人注意日志记录。如果我们使用Objective-C的新泛型功能(苹果专为Swift 2添加),编译器就会捕捉到这个问题:
在使用泛型时,区分符的输入非常烦人:
NSDictionary<NSNumber*, NSArray<PSPDFAnnotation*>*> *allAnnotationsDict = [document allAnnotationsOfType:PSPDFAnnotationTypeAll];
现在一下子就能解决了,我们可以简化它,同时保持C++正确的模板参数:
auto allAnnotationsDict = [document allAnnotationsOfType:PSPDFAnnotationTypeAll];
好多了,allAnnotationsDict
是什么仍然很明显。在编译时,auto
功能可以像上面这样自动完成,无需配置运行环境。Swift编译器团队的Joe Groff指出:目前在标准C和ObjC中,top-of-tree clang支持__auto_type
类型推论了,因此我们终于可以在无需C++编译开销的情况使用它了。
内联块
考虑一下使用内联块处理注释的情况,由于需要三个参数,使得声明的长度几乎让人难以忍受。通常我们会将它改成辅助函数,不过由于无法捕获变量,可能会让情况更加复杂。
void (^processAnnotation)(PSPDFAnnotation *annotation, BOOL addToIndex, NSUInteger objectID) =
^(PSPDFAnnotation *annotation, BOOL addToIndex, NSUInteger objectID) {
// code
};
这个声明还有个问题,就是非常冗长,每个参数类型都要写两遍,开发者通常都很厌恶冗长,因此我们来做些清理。auto
再次成了救星:
auto processAnnotation = ^(PSPDFAnnotation *annotation, BOOL addToIndex, NSUInteger objectID) {
// code
};
let
Swift的优点之一在于:在声明变量时,let
是使用最多也最方便的办法,会自动产生const
。同时在C语言中也有const
,只不过非常丑陋:
NSString *const password = @"test123";
有了auto
,可以写成可读性更高的样子:
const auto password = @"test123";
甚至可以更疯狂——使用一个宏:
#define let auto const
let password = @"test123";
vector
在Swift中,我们可以将任何数据类型放在数组中:
let anglePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 32, y: 32), CGPoint(x: 32, y: 0)]
在Objective-C中,NSArray
只能包含对象,不但更为复杂,同时由于封装的问题,对于基本类型速度也更慢。当然可以使用C数组,但会使得添加移除元素或另存数组之类的通用操作更为复杂,可能需要手动执行内存管理与调用malloc
。有了Objective-C++,我们可以简单地使用std::vector
:
auto points = std::vector<CGPoint>{{0, 0}, {.x=32, .y=32}, {32, 0}};
无论显式struct
字段命名,还是较短的隐式版本{0, 0}
都是可用的,由于vector<CGPoint>
已知想要的数据类型,无需再编写(CGPoint)
转换。此外,C++容器const
之后也会自动让它们成为不可变量。
vector <-> NSArray
有时候会出现需要将vector
转化为NSArray
的情况,反过来也是一样。这种操作非常简单,但如果使用helper会更好。
template <typename T>
static inline NSArray *PSPDFArrayWithVector(const std::vector<T> &vector,
id(^block)(const T &value)) {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:vector.size()];
for (const T &value : vector) {
[result addObject:block(value)];
}
return result;
}
template <typename T>
static inline std::vector<T> PSPDFVectorWithElements(id<NSFastEnumeration> array,
T(^block)(id value)) {
std::vector<T> result;
for (id value in array) {
result.push_back(block(value));
}
return result;
}
运算符重载
是否有时候需要计算CGRect
、CGSize
或Core Graphics
的其他几何类型呢?它们都是struct
,虽然好处很多,但计算时非常烦人,这里再次出现了冗余代码:
const CGSize zoomSize = CGSizeMake(self.bounds.size.width/zoomScale, self.bounds.size.height/zoomScale);
在Swift中,定义运算符非常简单,从而使得这些操作也很简单,但在Objective-C++中我们也能这样做。
CGSize operator/(const CGSize &lhs, CGFloat f) {
return (CGSize){lhs.width / f, lhs.height / f};
}
const CGSize zoomSize = self.bounds.size / zoomScale;
锁定(Locks)
在构建线程安全API时,需要锁定。在标准Objective-C中,可以像下面这样做:
@interface PSPDFDocumentParser {
NSLock *_parserLock;
}
@end
@implementation PSPDFDocumentParser
- (instancetype)initWithDocumentProvider:(PSPDFDocumentProvider *)documentProvider {
if ((self = [super init])) {
_parserLock = [NSLock new];
}
return self;
}
- (void)parse {
[_parserLock lock];
// Do stuff that needs locking
[_parserLock unlock];
}
@end
代码很多,但只描述了一个代码应当执行的状态。在Objective-C++中,我们可以采用更简单的办法:
@interface PSPDFDocumentParser {
std::mutex _parserLock;
}
@end
@implementation PSPDFDocumentParser
- (void)parse {
std::lock_guard<std::mutex> parserGuard(_parserLock);
// Do stuff that needs locking
}
@end
在超出范围后,C++会自动锁定,在C++中,到处都是资源分配即初始化(RAII)模式,它也确实很好用,允许我们通过返回语句来执行需要内联锁定的操作,因为锁定只会在返回后自动解锁。
如果我们只需要锁定某个method的很小一部分,可以简单地创建一个较小的范围来执行:
- (void)parse {
// Do stuff without locking
{
std::lock_guard<std::mutex> parserGuard(_parserLock);
// Do stuff that needs locking
}
// Do stuff without locking
}
如果需要递归锁,可以使用std::recursive_mutex
来代替std::mutex
。
可选方案:有一个简单的纯Objective-C解决方案,生成一个method,在锁定时执行一个块参数,比如[NSManagedObjectContext performBlock:]
。
模板
有时候模板在避免重复代码方面非常有效,试想一下负责比较类似CGFloat
或NSInteger
的helper,我们随时可以将其封装并调用compare:
,但开销很大。更好的办法是使用模板函数:
template <typename T>
inline NSComparisonResult PSPDFCompare(const T value1, const T value2) {
if (value1 < value2) return (NSComparisonResult)NSOrderedAscending;
else if (value1 > value2) return (NSComparisonResult)NSOrderedDescending;
else return (NSComparisonResult)NSOrderedSame;
}
另一个很有用的helper是条件转换,查看某个类是否是正确的类型。
template<typename T>
static inline T *PSPDFDynamicCast(__unsafe_unretained id obj) {
if ([obj isKindOfClass:[T class]]) {
return obj;
}
return nil;
}
// Usage:
auto objectOrNil = PSPDFDynamicCast<PSPDFNavigationController>(self.navigationController);
在if中的变量声明
在Swift中,典型用法就是在if-else块区中声明变量:
if let nav = controller.navigationController {
nav.pushViewController(myViewController, animated: true)
} else {
//show an alert or something else
}
在Objective-C++也可以采用类似的做法:
if (const auto nav = controller.navigationController) {
//...
}
STL算法
问题和缺点
这些简单的调整有什么缺点呢?我们不想说谎——缺点确实有一些,但我们认为到目前为止使用它们的优势更大。
编译时间
将文件扩展名从.m
修改为.mm
之后,clang将开始从C++的角度评估文件,而在自动转换方面C++更为严格。因此有时在使用中会收到一些警告,特别当代码中包含类似MAX
这样的宏时。在std::max
的情况下,可以通过显式转换或者使用C++函数的替代来解决这些问题。如果出现问题,或者有时Objective-C在类型上太松懈,就必须自行确定该如何处理。
编译.mm
文件比标准的.m
文件花费的时间更长一些,不过凭我们的经验来看,这点代价是值得的。如果你在使用一个大型代码库的话,额外时间可以累积起来,不过使用一些额外的编译缓存能够抵消很多时间消耗。大量使用模板或者用到模板的库会产生更大的影响。
工具
另一个风险在于:很多人广泛使用Objective-C++,因此很可能会遇到编译错误或边缘情况。目前我们只遇到过一个Clang Analyzer崩溃的问题,不过在纯Objective-C代码中我们也曾经设法重现过这个问题。
在header中避免使用C++
如果非要添加的话,将它或者放在单独的.hpp header
中,或者放在#if __cplusplus
后面;否则很快你就必须将整个项目转换为.mm
格式,而且header
对Swift来说不可访问。
意外的副本
C++喜欢复制内容,看看这段代码:
@property (nonatomic) std::vector<int> values;
// later on
self.values.emplace_back(5);
这段代码没有影响。属性会返回一个变异后的向量副本,在调用后会被破坏掉。有很多办法来解决这个问题,使用共享指针就是办法之一:
@property (nonatomic) std::shared_ptr<std::vector<int>> values;
// later on
self.values.get.emplace_back(5);
C++11加入了unique_ptr
、shared_ptr
和weak_ptr
,它们在很多方面与ARC类似,只是速度更快也更有确定性,因为其中没有自动释放池。共享指针很好用,在大多情况下调用new
或者delete
命令属于糟糕的设计,可以使用智能指针来替代。
Objective-C的功能
我们使用了大量并非人尽皆知的Objective-C功能,还有大量可用的辅助函数让代码更具有可读性,尤其是在处理集合时。
NS_NOESCAPE
在Swift中,@noescape声明允许编译器在block内优化代码。尽管我们没有NS_NOESCAPE,也可以使用下面的办法:
// Equivalent to Swift's @noescape
#define PSPDF_NOESCAPE __attribute__((noescape))
当然我们提交了rdar://25737301,另有一个Swift proposal建议将其添加到Objective-C中,希望很快能看到这样的变化。
点语法
这是一个有争议的话题,我们在任何没有负面作用的方法中使用点语法,即便没有作为属性声明:
// Typical
[UIApplication sharedApplication].keyWindow
// Shorter
UIApplication.sharedApplication.keyWindow
在iOS 7的SDK中,苹果将很多应当是属性的方法都转换成了属性。功能上并无区别,只是现在能更好地执行自动补全了。这里的缺点在于,Xcode无法自动补全点语法调用的方法。
map, filter, flatMap
类似NSArray
和NSSet
这样的数据结构缺少高阶函数。一些有用的方法长度夸张,使用起来也很不方便。
看下这段从页面访问中收集选择注释的代码:
- (NSArray<PSPDFAnnotation *> *)selectedAnnotations {
NSMutableArray<PSPDFAnnotation *> *selectedAnnotations = [NSMutableArray array];
for (PSPDFPageView *visiblePageView in self.visiblePageViews) {
if (visiblePageView.selectedAnnotations.count > 0) {
[selectedAnnotations addObjectsFromArray:visiblePageView.selectedAnnotations];
}
}
return [selectedAnnotations copy];
}
使用我们的flatMap
助手来编辑相同的代码:
- (NSArray<PSPDFAnnotation *> *)selectedAnnotations {
return [self.visiblePageViews pspdf_flatMap:^NSArray<PSPDFAnnotation *> *(PSPDFPageView *pageView) {
return pageView.selectedAnnotations;
}];
}
整个helper非常简单,还有不同的变体返回一个链接更佳的block,我们选择了更为Objective-C风格的API,在数组为空的情况下不会崩溃:
- (NSArray *(^)(NSArray * _Nullable (^)(__kindof id obj)))pspdf_flatMapBlock {
return ^(NSArray *(^block)(id obj)) {
NSMutableArray *result = [NSMutableArray new];
for (id obj in self) {
NSArray * _Nullable array = block(obj);
[result pspdf_addObjectsFromArray:array];
}
return [result copy];
};
}
- (NSArray *)pspdf_flatMap:(PSPDF_NOESCAPE NSArray * _Nullable (^)(__kindof id obj))block {
return self.pspdf_flatMapBlock(block);
}
我们有类似的方法可用于filter
或map
,不过需要一系列其他helper的协助,比如-[NSArray pspdf_mutatedArrayUsingBlock:]
,以封装大量每个人都写了数百次的样板代码。尽管我们的helper目前还没有开源,但有不少有用的开源项目。BlocksKit在上述实现方面表现很优秀。
结论
在PSPDFKit中,我们平时会使用本文中提到的方法,并且确信这些方法不但会使我们的代码可读性更高,同时也增加了代码库的安全性,另外,由于无需再重复编写相同的样板代码(在Objective-C开发中太过常见的一些代码),其中的很多方法也加快了开发速度。有很多其他的应用与框架也使用Objective-C++,Realm Cocoa、Paper by FiftyThree、RxPromise、Dropbox Djinni、Facebook的ComponentKit还有Pop——甚至很多苹果的框架,比如Core Graphics、WebKit/WKWebView甚至Objective-C runtime都有运用到Objective-C++。
相关推荐
- 微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型
-
IT之家4月3日消息,科技媒体WindowsLatest昨日(4月2日)发布博文,报道称Windows10、Windows11新版Copilot应用已摘掉Beta帽...
- WinForm 双屏幕应用开发:原理、实现与优化
-
在当今的软件开发领域,多屏幕显示技术的应用越来越广泛。对于WinForm应用程序来说,能够支持双屏幕显示不仅可以提升用户体验,还能满足一些特定场景下的业务需求,比如在演示、监控或者多任务处理等场景...
- OpenJDK 8 安装(openjdk 8 windows)
-
通常OpenJDK8和11都能互相编译和通用。我们建议使用11,但是如果你使用JDK8的话也是没有问题的。建议配置使用OpenJDK,不建议使用OracleJDK,主要是因为版...
- 基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)
-
一、前言在如今远程办公和内网穿透需求日益增长的背景下,搭建一套安全、稳定、高效的VPN系统显得尤为重要。OpenConnectServer(ocserv)是一个开源、高性能的VPN服务端软件...
- 巧妙设置让Edge浏览器更好用(edge怎么设置好用)
-
虽然现在新版本的Edge浏览器已经推出,但是毕竟还处于测试的状态中。而Win10系统里面自带的老版Edge浏览器,却越来越不被人重视。其实我们只需要根据实际情况对老版本的Edge浏览器进行一些简单的设...
- 微软开源博客工具Open Live Writer更新:多项Bug修复
-
OpenLiveWriter前身是WindowsLiveWriter,是微软WindowsLive系列软件之一,曾经是博主们非常喜爱的一款所见即所得博文编辑工具,支持离线保存,还支持图像编辑...
- 基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品
-
在第二届拯救者杯OPENAIGC开发者大赛中,涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到,我们特意开设了优秀作品报道专栏,旨在展示其独特之处和开发者的精彩故事。...
- Python open函数详解(python open函数源码)
-
演示环境,操作系统:Win1021H2(64bit);Python解释器:3.8.10。open是Python的一个内置函数,一般用于本地文件的读写操作。用法如下。my_file=open(fi...
- 世界上最好用的Linux发行版之一,OpenSUSE安装及简单体验
-
背景之前无意在论坛里看到openSUSE的Linux发行版,被称为世界上最好用的Linux发行版之一(阔怕),一直想体验一下,于是这期做一个安装和简单体验教程吧。...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
推荐7个模板代码和其他游戏源码下载的网址
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- 微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型
- WinForm 双屏幕应用开发:原理、实现与优化
- 推荐一个使用 C# 开发的 Windows10 磁贴美化小工具
- OpenJDK 8 安装(openjdk 8 windows)
- 基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)
- 巧妙设置让Edge浏览器更好用(edge怎么设置好用)
- WPF做一个漂亮的登录界面(wpf页面设计)
- 微软开源博客工具Open Live Writer更新:多项Bug修复
- 基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品
- C#开源免费的Windows右键菜单管理工具
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)