考虑到Objective-C++“美丽而疯狂”的特性, Peter Steinberger 向我们介绍了何时使用Objective-C++是最合适的,何时不该使用Objective-C++,以及如何避免在尝试的时候掉进坑里。比如说,您是否知晓现代的C++已经拥有属于自己的ARC机制、 weak
标识以及闭包呢?
我们将要学会分辨常见的性能瓶颈,并且专注于一个C++的小小子集,然后将它们即刻放在您的工具箱当中。
本次演讲在AltConf 2015上记录,点此观看全部视频!Sign up to be notified of new videos — we won’t email you for any other reason, ever.
大家好,我的名字是 Peter Steinberger
,网上的昵称是 @steipete 。我很高兴见到有这么多的人来听这个关于Objective-C++的讲座,因为我觉得大多数人看到标题上的C++字样就退避三舍了。
让我们先来谈论一下这个讲座适合哪些人吧!我希望诸位曾经接触过C++代码,并且对代码的简洁和高性能很感兴趣。亦或者您不想止步于iOS或者Mac开发,想要学一点所谓的“黑科技”。即使您从未写过或者见过C++代码,您仍然可以跟随我们的讲解。这个讲座涉及到的内容并不会很难,希望诸位不要害怕。
从2011年开始,我就一直在维护着一个PDF框架。我们在iOS平台上对其进行开发了很长时间,现在Android平台上也提供了该框架。在您的手机上就很有可能拥有至少一个正在使用我们这个框架的应用。如果您正在使用Dropbox、Mincrosoft SkyDrive、Box Silent Mode或者ScanPod的话,那么您就正在体验我们的产品。并且,世界上绝大部分的“线上会议”应用也在使用我们的框架。
我们曾经尽可能避免使用灾难一般的C++代码,而是使用Objective-C来进行开发。直到今年年初,我们才看见C++的曙光,然后转用其进行开发。它确实给我们的产品带来了更好的性能,并且提供了极佳的减少闭包使用量的方式。
我想以一个有趣的结论开头:所有的 malloc
函数都是反人类。我曾经写过无数个 malloc
函数和 free
函数,然后在上面吃了无数的亏,掉了无数的坑,打脸打得啪啪响。
当然,我并不是反对您使用这玩意,事实上我们可以使用 NSMutableData
,通过指针来使用就可以避免调用 malloc
和 free
函数了,但是其本质上仍然是一个“指针”,天性使然,它自身并不安全。
那么Swift的效果怎样呢?我们在keynote上见过了它,并且我们对Swift的体验表示良好,它真的很赞。我是Swift的粉丝,它的优点很多,不过缺点也有不少。
首先,Swift和Objective-C和C的交互非常方便,但是它却不能直接和C++交互。因此,如果您想使用Swift的话,对Objective-C++有一定的了解会带来极大的帮助。因为这可以让您轻松地对第三方C++代码库进行封装,这样就可以让Swift调用C++代码,从而达成和Objective-C调用C++代码相同的作用。
我们主要考虑到二进制兼容性的问题,所以我们创建了一个二进制框架。这个框架需要收费,虽然苹果会更新其Swift标准库,但是通过一些极小的额外更新,这个框架仍能运行得非常好。不过如果万一它突然不能用了,您很有可能会怪罪我们。所以这就是我们不能使用Swift的原因,接下来我们继续谈论Objective-C++。
首先,我们知道,以前的C++是相当的可怕,不过现在情况不同了,C++在逐步进行改善。现在绝大多数编译器都支持C++14标准,并且在逐步向C++17标准靠拢,相当于对这门语言进行了重新设计。
C++拥有非常好用的工具,非常棒的编译器(毕竟是Clang),以及非常完善的生态系统,并且它是跨平台的。因此在C语言能够编译的地方几乎都可以编译C++。
C++的运行速度非常快,它的标准库也是很给力的。倘若您在使用过程中遇到了始料未及的坑,不要担心,C++拥有一个充满活力的第三方生态系统,因此如果您忘记了标准库的某些部分,谷歌“ Boost ”,这个网站基本上可以满足您的要求。
还有,C++圈子中也形成了一个小小的文化,所以要注意,使用Objective-C++的时候使用CocoaPods是一个非常Low的行为。
因此,C++已经与时俱进了,很多老掉牙的缺点已经被改正了。现在C++有自动类型(auto type),它可以自行推断对象类型;有基于ARC的共享指针(shared pointers);有和Objective-C运行时(runtime)中一样的 weak
标识,有Lambda表达式(闭包的一个别名),以及很多很多的新特性,稍后我会对其进行解释,因为它看起来可能会比较疯狂。
首先,让我们先来看“自动范围循环迭代(Auto & Range-Based For Loops)”这一个特性。正如我之前所说,C++98标准是非常可怕的,现在您或许会对其可怕程度有个清楚的认识。使用 CGPoint
的vector容器是存储多个点的极佳选择。
在C++98当中,我们必须先创建一个迭代器(iterator),调用 begin
和 end
函数,然后让迭代器递增,接着才是引用它以获得相应的对象。
C++11让这个步骤变得简单多了,它向语法中引进了 for in
语句,因此我们只需要给定类型和目标即可。不仅如此,我们甚至无需指定类型,因为这个类型是通过泛型定义的,因此编译器已经知道了vector容器中的类型,我们只需要给定次序即可。我们还可以再简化它,我们无需声明auto特性,因为编译器知道什么是您心中所想。
这些特性已经在Clang中存在,但是在现在的版本中又被再次移除了,因为这个功能存在某些边界案例导致的BUG。这项功能计划在C++17重新引进,因此C++语法是在不断调整、改进和简化的。
在这项功能里面, std::vector
可能是最有用的语法了。我们能够在其中放入任何东西,并且其当中的元素在内存中的地址是连续的,因此我们将CGPoint放进去的话,我们就能够通过行指针(raw pointer)来进行指向和定位,然后使用C函数来对其进行访问。如果操作出现错误,还会抛出相应的异常。
不过要注意的是,如果通过行指针访问了无效内存的话,那么会导致应用崩溃。此外,vector当中还拥有非常强大的设置方法,比如说数组翻转(rotate)等等。
为了保持一致性,同样还存在一个类似于vector的标准数组容器,不过大小是固定的,并且性能更加高效。在实践中,vector运行已经足够快了,我们无需再来一个。
接下来是初始化列表(initializer list)。在原来的C++中,如果我们有一个vector容器并且想要对其进行填充,这个过程可能会有一点烦人。不过现在这个麻烦已经解决:
// C++11 std::vector<int> v = {1, 2, 3, 4, 5};
您可能注意到了,这个语法实际上和Objective-C非常类似,实际上C++早就用上了这个语法。
试想有个函数返回一个 developer
类型的vector容器,每个 developer
都是个在某处初始化过的对象,vector容器中包含了是这些对象的指针。问题是,当我们对其处理完毕之后,我们必须要将这些指针进行清空。在C++中我们没有ARC的帮助,因此我们就得使用老语法,遍历这个vector容器,然后为每个对象调用 delete
函数。
Smart指针充当了C++中ARC的概念,因此我们所要做的替代工作就是让 developer
使用唯一指针(unique pointer)。当我们查看内存布局的时候,会发现这种做法和之前其实是没有任何区别的,因为唯一指针仅仅只是一个特殊的指针而已。
其余工作在代码中已经做好,编译器会确保 new
和 delete
一一对应,因此我们无需关注内存管理,一切事情都交给编译器就行。总而言之,我们创建一个vector容器,然后调用 push_back
函数,创建一个唯一指针,进行类型转换,然后创建一个新的 developer
对象。
然而,我们现在却打破了之前说的:那就是所有的 malloc
都是反人类这一结论。事实上,我们可以不使用这个语法,我们不调用 push_back
函数,而是使用更加优化的 emplace_back
函数。接下来再使用 make_unique
函数,这个函数和前面的创建唯一指针的方法很函数,但是在内存方面更加高效。在其作用范围内, make_unique
只调用一次 malloc
函数,然后立即调用 developer
和指针所需要的全部空间。
// C++17 for (point : v) { reticulateSpline(point); }
C++有四种主要类型的指针,分别是 auto
指针、 unique
指针、 shared
指针和 weak
指针。
请赶紧忘掉第一种指针,因为它是在太难用了。C++花了数年时间来指明如何使用它才是正确的,并且绝大多数情况都需要进行说明,因此我的建议是:永远、不要、使用、 auto
指针。
如果我们想要共享一个对象的话,那么我们可以使用 unique
或者 shared
指针,两者相互转换是非常简单的。
有些时候我们可能需要使用 weak
指针,不过这种情况相当地少见,就如同我们在Objective-C中使用weak的几率一般。它的语法和原始C数组是基本相同的,并且正如我所说,它能够自动地在 unique
和 shared
指针之间转换。
移动语义(Move Semantics)对我个人来说是喜欢的语法了。试想有一个函数,其中有一个vector容器,然后需要得到其中的点。因此我们调用 get points
函数,然后这个函数返回一个包含点的vector容器。但是如果 get points
方法创建了上万个点并且将这些点进行了拷贝,然后全部返回回来,这个速度将会非常慢,这也是为什么我们经常在C中返回指针的原因。
当然,我们需要摒弃指针,C++中有一个名为“移动语义”的特性。移动语义非常奇怪,它让程序能够获取赋值语句两端引用的地址,多出来的那个被称之为右值 rvalue 。基本上编译器基本上会识别您刚刚创建的右值引用是否会被再次使用,如果没有的话,编译器将直接移动数据进行赋值而不是对其进行拷贝赋值。
这样做的原因在于新创建的vector容器和拷贝出来的vector容器实际上是相同的,它们双方互相知晓,因此编译器会调用 move
方法。这创建一个新的vector容器,它知晓原先vector容器中存放的所有私有数据,然后将这些数据偷走,并将其他对象的数据设为null。因此,换句话说,我们仍然复制了这些对象,但是复制的过程变得更有效率。
// get all the points vector<CGPoint> points = get_points(); vector(vector&& that) { data = that.data; that.data = nullptr; }
还有一个更为简单的例子。试想一个变量 a
和一个变量 b
。当我们声明 a
为 b
的时候,我们执行了一次复制操作。因为 b
此时仍然存在,而 a
是 b
的一个新的拷贝。但是如果我们声明 a
是 x+y
的话,那么我们是执行了移动操作。因为 x+y
的结果是一个新的临时变量,我们的编译器会立刻知道不会有其余对象再使用这个变量,因此它就会执行移动操作。
auto a = b; // 复制 auto a = x + y; // 移动
C++同样也有Lambda表达式。正如我之前所说,Lambda实际上只不过是闭包的另一种称呼罢了。它的语法和我们在Objective-C中所见的闭包不同,只是因为它们的语法表现形式不同。
如果将Lambda和闭包用二轮车进行类比的话,那么闭包就是摩托车,能够带你去任何地方,并且操作十分容易,性能强劲;而Lambda则如同自行车,您必须要对它的使用方式了如指掌才能使用,一旦当您熟悉它的操作之后,您就能够很快地前往您的目的地,并且它还能产生很多的乐趣。当然,这取决于您定义“乐趣”的方式了。
Lambda同样可以选择不进行捕获,或者选择捕获引用,亦或者是选择捕获拷贝。在Objective-C中,我们通常选择拷贝任何东西,依赖ARC帮助我们解决余下的问题。而在这儿,我们着重强调捕获引用这种方式。因此,大体上来说,我们将会比Objective-C捕获到更多的内容。
使用Lambda有不少好处:试想我们拥有一个CGPDF字典,我们要在一个C语言函数使用它,那么我们必须要想一个办法来遍历这个字典。在C中唯一的做法是调用Apply函数,它需要一个函数指针作为参数。或许这种调用将会让您的代码变得复杂,因此您必须要将某几段代码单独独立出来成为一个函数,然后再执行转换。这种做法将会导致阅读体验十分艰难。
我们可以借助lambda来进行改善,将其在lambda表达式中进行展示。lambda并不会捕获任何变量,它被要求表现得要如同一个函数指针一样。我们并不需要了解其底层是如何实现的,只要知道它确实是这样做的就好。这个特性是在最近的版本中才定义的,但是它运行得十分完美,并且让代码变得更加容易理解,只需要忽略掉这些该死的方括号即可,不过方括号似乎是Objective-C开发者的爱好之一。下面是让复制代码变得更优美的例子之一:
auto parseFont = [] (const char *key, CGPDFObjectRef value, void *info) { NSLog(@"key: %s", key); }; CGPDFDictionaryApplyFunction(fontDict, parseFont, (__bridge void *)self);
好的,那么我们应该从哪里开始才能体会到现代C++的好处呢?C++实在太庞大了,并且要全部搞懂它是一个疯狂的想法。您完全可以从C++基础教程开始学习,或者如果您有基础的话,可以前去购买 ffective Modern C++ 这本书。
网上的信息虽然繁杂,但是却也有不少好用的资源,您完全可以通过谷歌搜索所有您不清楚以及想迫切知晓的特性以及问题。Objective-C++是C++和Objective-C的混合体,两者的特性都没有被抛弃。苹果对此有着很详细的文档,不过很不幸的是现在在官网上已经找不到了,似乎苹果已经不再公开关注这个项目了。
当然,Objective-C++仍然还有很多限制,因此您不能使用C++创建一个Objective-C类的子类,以及其他类似的做法。因为它们的内存模型是完全不同的。您能够做的是将C++对象放在Objective-C的 ivar
声明当中。我们的目标不是用C++来写UIKit应用,我们只在必要时刻使用C++。
当然Objective-C++也有不少陷阱,其中之一就是编译时间。随着Objective-C代码使用量的增加,编译速度会变得愈来愈慢,甚至可能会花费以往两到三倍的时间。不过这仍然比编译Swift(就目前而言)来得快,但是您也需要注意,不要试图将所有的文件盲目地改成.mm后缀,只有在必须的时候才将其后缀进行修改。
另一个陷阱就是属性。苹果修复了许多关于C++对象作为属性所产生的问题,但是仍然有许多不能正常工作的情况发生,因此我的建议是使用 ivar
,并且手动编写访问权限。或许现在Xcode中C++对象属性可以正常工作了,不过 ivar
还是提供了比较安全的使用方式。
C++同时还是一个严格的编译器,因此当您将文件更名为.mm的时候可能会产生许多警告甚至错误。通常情况下,这实际上是一个好事情,因为有些操作在C环境下是合法的,而在C++环境中却是非法的,比如说执行强制转换,或者使用关于枚举的某些黑魔法。不过这些警告和错误在大多数情况下是很容易修复的。不过如果您确信您使用的黑魔法是有效的话,您可以选择将这些警告移除忽略,不过通常情况下警告是好事,不是么?
现在让我们谈论一下使用Objective-C++的项目情况。首先,我们能够使用Objective-C的运行时机制。如果您在想C++是如何实现 weak
的功能的话,那么需要使用 dense_hash_map
。
WebKit是C++写的,而WKWebView和UIWebView则是Objective-C++占主导的项目。
移动端数据库Realm的核心是基于C++的,它们不仅使用Objective-C++完成和核心进行交互,还通过其将对象进行打包,就如同C数组做的那样,此外还有许多有意义的功能。
如果您真的想要见识一下什么叫做疯狂的C++,那么Facebook的 Pop 框架是个不错的选择,它是一个基于C++模板的spring解决方案,因此这个项目的代码是非常有意思的。
同样还有 ComponentKit ,这是Facebook为了改善布局所做的尝试之一,并且部分已经应用在Facebook当中,提供了一个良好的运行速度。或许Facebook的未来是 React Native ,也可能是ComponentKit,但是ComponentKit仍然是一个重度使用C++的有趣项目。
另外一个不错的项目是Dropbox的 Djinni 。Djinni和创建C++、Objective-C++和Java的界面定义语言很类似,它通过Objective-C++或者JNI进行打包。这个框架在我们进行跨平台工作的时候十分有用。
让我们回到ComponentKit来,因为它是一个Objective-C的极佳例子。下面的代码创建了一个“堆栈布局组件”(Stack Layout Component)并且使用了“聚合初始化”(Aggregated Initialization)添加了三个子类。我觉得这个代码非常简单易懂。
如果您打算在Objective-C中这样做的话,最大的问题就在于默认方法(default method)并不存在。您可以通过实现上下左右四条导航线(heading line)来改善Objective-C版本的代码,但是这个做法很让人反感。您必须手动来处理这些东西,然后加以重构,总而言之不如C++版本来得漂亮。
[CKStackLayoutComponent newWithStyle:{ .direction = CKStackLayoutComponentDirectionVertical, } children:{ {[HeaderComponent newWithArticle:article]}, {[MessageComponent newWithArticle:article]}, {[FooterComponent newWithArticle:article]}, }];
聚合初始化实际上并不是C++的特性,而是C的一个特性。或许您曾经使用过它,但是这却是处理点集的最简单也是最笨的方法。您可以只设定部分点,其余将会自动初始化为零。下面的例子就是很好的说明:
CGPoint p1 = (CGPoint){ .x = 3 }; CGPoint p2 = (CGPoint){3, 0};
Objective-C++另外一个优秀的特性是类型安全。今年的 WWDC 上我们迎来了Objective-C泛型,这是一个非常棒的语法糖。我对此非常期待,我觉得我会在码代码的时候笑出声,不过很多时候我们却会得到诸如“可空性(nullability)”方面的编译警告。
您仍然可以在单个定位线(string)中添加三个布局组件,然后在运行的时候程序就见鬼般地崩溃了。对于vector容器来说,它不会让你通过编译,除非我们执行强制转换。因此高效率是我们使用C++的原因之一。因为C++在处理循环或者数据结构的时候要高效得多。因此当我们碰见困难的时候,C++将会保平安。
另一个使用Objective-C++的理由是它是空值安全的。如果您观察下方的例子,您会发现下面的这些元素既可能不是定位线,夜可能不是动态创建的布局。由于我们可能还没有设置基底(footer),也可能图片并不存在,从而导致这些元素可能为 nil
。因此,第一行代码会在运行时崩溃。第二行则会正常工作。这是因为vector容器实际上允许您放入一个空指针。它会察觉到这个指针中有四个对象,虽然第二个对象是空指针,但是这完全没有任何关系。有很多方法可以将这个空指针过滤掉,比如说我们可以使用Lambda表达式,可以轻松地将其简化为只有实际数据存在。Objective-C++的例子看起来非常的简洁,创建vector容器也是得心应手。其对应的Objective-C代码可能写起来会相当的糟糕。起码要写超过两倍的代码量,拖慢了运行速度。
auto vector = vector<NSString*>{@"a", @"b", nullptr, @"d"}; vector.erase(remove(begin(vector), end(vector), nullptr), end(vector)); children:{ headerComponent, messageComponent, attachmentComponent, footerComponent }
另一个很赞的特性是Objective-C的闭包语法,这货可以用来亲自创建一个内含变量。我从2009年开始写Objective-C代码,然而我仍然必须是不是去 f*ckingblocksyntax.com 网站上查阅资料,因为我常常发现由于遗漏了某些奇怪的符号,导致我写的闭包不能正常工作。我建立了无数个内含闭包,因为这样可以让代码变得容易组织。
另一个替代方式是使用 id
,因为所有的C闭包都是一个对象。这种方式在NSURLSession中也是非常方便的。但是有些时候您可能会搞混了签名(signature),也有可能忘记需要获取的是NSData数据,也还有可能您觉得您已经使用了定位线,然而使用 id
却发现编译器根本不识别,在运行时分配给 e
之后就崩溃了。如果您尝试修复这个问题的话,您可能会在编译时就抓瞎了,而不可能等到顾客抓瞎的那一刻。
因此,替代 id
的方法就是使用 auto
。 auto
和 id
的作用基本类似,但是它会自动锁定第一个类型,而无需为其指定类型。这个特性是一个纯编译特性,我们无需在库中将其链接起来。当然,我们必须把这个文件命名为.mm,但是这确实是我最喜欢的辅助方法之一,因为他能够让编程变得简单。
auto handler = ^(NSData *data, NSURLResponse *response, NSError *error) { // parse data }; [NSURLSession sharedSession] dataTaskWithURL:URL completionHandler:handler];
另一个我们使用的小例子是比较。我很确信在座的所有人都曾经写过像下面的例子那样的代码,也就是我们有一个 NSComparisonReuslt
,我们需要用其来比较整数、枚举量或者是 CGFloat
等等。接下来,您很就可能会同时写个四五个相似的方法,哦,这是多么难看的做法。
虽然我们也可以使用宏来解决,但是太多宏会让代码变得十分难看,反正我是这么觉得的。
我们还可以使用模板来这个问题,这样就可以实现一次实现,多次使用的特性。
我们还可以使用运算符重载。在Swift之前,所有人似乎都很反感运算符重载。现如今,通过Swift,大家都对运算符重载的强大特性感到激动。我们在C++中也可以做到这一点。因此,我们只需要给 CGPoints
、 CGSize
添加运算符,这样就可以实现加减乘除相等等等简单的操作,这样可以让代码更加简洁,写起来更加轻松。
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; }
最后一个就以我的项目为例。大家都知道,PDF相比其他文本格式来说是有点反人类的,其中一点就是它是线注(line annotation)形式的,并且它的行尾非常不同。它有十种不同的行和类型,因此创建一个路径是相当的复杂。
当我们创建路径的时候,一般都是先描边,然后再填充,因此我们为此写了一个辅助方法。我们将其称之为 CreatePathsForLineEndType
,通过给定行和类型,以及点坐标和点数还有线宽,此外还需要指定两个指针给Core Foundation对象,从而提供解析。
比方说,或许我只需要描边,而不需要填充,因此我只需要给fill设置为 nil
即可,但是我并不清楚这样做能不能正常运行。如果这个方法写得完善的话,那么它应该是可以用的,但是不行,编译器会视图解除空指针的引用,然后导致应用崩溃。
或许我们可以用另一种方式,采用 CGPass
,但是我们必须得将其释放,否则会导致移除,而通常情况下,往往已有的常见的方法都会被称作 create
。我假设我通过得到了一个带有读取总数的对象,但是却担心它会被编译器自动释放掉,因此我必须自行将其保留起来。同样对于编译器也是一样,静态分析器或许可以帮助您。当然,当我产生了拥有两个点的指针后,我会读取到禁用的内存空间,从而导致奇怪的事情出现。
void CreatePathsForLineEndType(PSPDFLineEndType const endType, CGPoint const* points, NSUInteger const pointsCount, CGFloat const lineWidth, CGPathRef *storedFillPath, CGPathRef *storedStrokePath);
改善这个代码的第一步就是将 points
和 pointsCount
进行合并,只需使用vector容器即可。我们在之前已经见识过vector容器的强大之处了,这货真的非常好用,它更有效率,知道里面空间的大小,直到其占用的内存,因此搞砸它似乎是不可能的事情。但是如果我们调用范围外的索引的话,就会导致抛出异常,因此我知道我有地方做错了。
在下面的代码中,大家可以看到将行指针数组转换为vector容器是多么的容易。我们有多种方法来实现它,注意其中一个是使用括号,另一个类似于初始化列表,因此即时您拥有一个有着大量函数的代码库,您扔能够将其一点点的转换过来,只需要在百忙之中抽出时间创建一个vector容器,然后以一个安全的方法调用它即可。
void CreatePathsForLineEndType(PSPDFLineEndType const endType, std::vector<CGPoint> const& points, CGFloat const lineWidth, CGPathRef *storedFillPath, CGPathRef *storedStrokePath); // copy points into a vector auto vector = std::vector<CGPoint>(points, points+pointsCount); auto vector = std::vector<CGPoint>{point1, point2, point3};
某些时候,当您结束转换后可能会发现这样做真的会耗费大量的性能,但是实际上我们并不止步于此。我们能够做得更好。我们仍然使用指针来完成输出,因此我们并不清楚是否可以为其赋值为 nil
。
那么如果我们通过元组来存储一个新的对象会怎样呢?只需要一个拥有填充和描边的C++对象即可。这样我们就可以返回这个对象,然后通过它来获取我们想要的路径。注意到我重命名了这个函数,让其更容易理解。所以现在他的名字变成了: FillAndStrokePathForLineEndType
。开始是填充,然后是描边,现在看起来是不是更清晰明了了呢?
唯一的问题是我们仍然需要考虑到内存管理,不过这个函数目前将使用ARC,因此这可以是一个解决方案,但是我们并不想让Core Foundation对象使用ARC。
因此最后我们要做的就是写一个指针,让其能够自动打包 CGPathRef
然后知晓如何处理Core Foundation对象。不要觉得我疯了,这实际上是WebKit在内部已经完成的工作,它们处理了大量的Core Foundation对象,并且工作得十分优秀。我们将要使用 CFPointer
来工作,并且不必写海量的代码,一般情况下如果您想要让细节完美一点的话,大概只需要100行代码即可。是的,这种代码写起来是有点复杂,但是它却可以一次撰写,多次使用。如果您和之前我们做的对比的话,代码似乎会变得有点难以理解,但是不会出现相当糟糕的BUG。对于第一个版本来说,我需要知道平台以及其语法规则,并且我还得确保我直接调用了它。
调试对此工作得很好, LLDB 可谓是我们的好帮手。有些时候它在处理Objective-C对象的时候更加有用。我们能够打开一个vector容器或者一个映射(map),然后查看里面的内容,就如同我们所期待的哪样。
tuple<CFPointer<CGPathRef>, CFPointer<CGPathRef>> FillAndStrokePathForLineEndType(PSPDFLineEndType endType, std::vector<CGPoint> const& points, CGFloat lineWidth);
我的演讲就到此结束,谢谢大家!
Sign up to be notified of new videos — we won’t email you for any other reason, ever.