首页 >iOS开发

滴滴 iOS 动态化方案 DynamicCocoa 的诞生与起航

2016-12-20 13:54 编辑: suiling 分类:iOS开发 来源:CSDN云计算

作者简介: 孙源(@我就叫Sunny怎么了),滴滴出行 iOS 技术专家,多年专注于 iOS 开发,现就职于滴滴 App 架构组,在技术上做探索和深挖;善于刨根问底,对未知的东西兴趣强烈,代码风格强迫症;同时喜欢写博客(http://blog.sunnyxx.com/),线上线下分享,贡献开源(forkingdog)累计 star 破万。

滴滴出行客户端App架构团队在对ReactNative、Weex进行调研尝试后发现并不适用于滴滴现有业务,由此自研了iOS动态化方案——DynamicCocoa,在这篇文章中,作者详细分享了它的背景以及具体功能实现。

方案诞生

动态化一直是App开发梦寐以求的能力,而在iOS环境下,Apple禁止了在MainBundle外加载和执行的自己的动态库,所以像Android一样下发原生代码的方案被堵死。

后来像ReactNative、Weex这样的基于Web标准的跨端方案出现,各大公司都有对其进行尝试,但对于滴滴现状,也许并不适合:

  • 滴滴App强交互、以地图为主体、端特异性高;

  • 客户端人员充足,跨技术栈学习和开发有较大成本;

  • 大量固化Native代码,重写成本高。

所以我们思考,能不能做一套保持iOS原生技术栈、不重写代码就神奇的拥有动态化能力的方案呢?

于是,我们设计和实现了一个具有里程碑意义的iOS专属动态化方案:DynamicCocoa

DynamicCocoa初识

DynamicCocoa可以让现有的Objective-C代码转换生成中间代码(JS),下发后动态执行,相比其他动态化方案,优势在于:

  • 使用原生技术栈:使用者完全不用接触到JS或任何中间代码,保持原生的Objective-C开发、调试方式不变;

  • 无需重写已有代码:已有native模块能很方便的变成动态化插件;

  • 语法支持完备性高:支持绝大多数日常开发中用到的语法,不用担心这不支持那不支持;

  • 支持HotPatch:改完bug后直接从源码打出patch,一站式解决动态化和热修复需求。

不论是动态化还是HotPatch,我们都能让开发者“Write Cocoa,Run Dynamically”。

ff577fae869247068dd2853a6b65afdb_th.png

语法支持

DynamicCocoa能支持绝大部分日常使用的Objective-C/C语法,挑几个特殊的:

  • 完整的Class定义:interface、category、classextension、method、property,最重要的是支持完备的ivar定义,保持和native完全一致的实例内存结构;

  • ARC:可以正确处理strong、weak、unsafe_unretained等对象的引用计数,对象的ivar也可以正确的释放;

  • C函数:支持C函数的定义与C函数的调用、内联函数的调用;

  • 可变参数:支持C与OC的可变参数方法的调用,如NSLog;

  • struct:支持任意结构体的使用,无需额外处理;

  • block:支持创建和调用任意参数类型的block;

  • 其他OC特性:如@selector、@protocol、@encode、for..in等;

  • 其他C特性:支持使用宏、static变量、全局变量,取地址等。

举个例子,你可以放心的使用下面的写法,并能被正确的动态执行:

a67a4ced370444a0bba4a05f2fca2b3e_th.jpeg

资源支持

一个功能模块,除了代码外,资源也是必不可少的,DynamicCocoa的动态bundle支持:

  • xib和storyboard;

  • xcassets;

  • 不放在xcassets里的图片资源;

  • 其他资源文件。

对于习惯于使用IB来开发UI的人来说,这将是一个很好的开发体验。

工具链支持

我们使用Ruby开发了一套命令行工具(类比为xcodebuild),大幅简化了配置开发环境、OC代码转换、资源处理、打包的复杂度,它可以:

  • 解析XcodeProject:读取工程编译选项,保持和native编译参数一致;

  • 增量编译:缓存JS转换结果,只重新转换修改过的文件,大幅提高build速度;

  • 链接:分析类依赖,将多个JS按依赖顺序合并,提高文件读取速度;

  • 资源编译:编译用到的xib、storyboard和xcassets;

  • 打包:将JS、资源等打包成bundle。

对于开发者来说,就像pod命令一样,所有操作都可以通过这个命令完成。

动态插件开发流程

首先App中需要集成DynamicCocoaEngineSDK,用来执行下发的bundle开发到发布的流程如下图所示:

fe86cf549e4647be83b2f4547622dd15_th.jpeg

当然,DynamicCocoa只提供命令行工具和EngineSDK,可以完成本地打包、运行和测试,而线上发布后台、服务端、CDN等需要自行解决。

在滴滴内部,我们构建了开发、Review、线上回归测试、灰度、发布、回滚、统计的闭环系统,以服务的形式给内部接入。

HotPatch过程

HotPatch本质上是方法粒度上的动态化,所以在整个框架搭建起来后,HotPatch也不难实现,使用DynamicCocoa做热修复的最大优势是开发者依然只对源码负责,修改完bug后,打个patch包,修复成功后把源码改动直接push到代码仓库就行了。

假设我们发现了下面的bug:

706b4b9ace7e42e980b91de252b079f9_th.png

然后在Native进行修复并自测:

36.jpeg

自测完成后,在这个方法后面添加一个神奇的Annotation:

37.jpeg

使用命令行工具在patch模式下进行打包,就能把所有标记了的method提取出来,分别转换成JS表示,打到一起进行发布。

除了修改一个方法外,patch模式还支持:

  • 调用原方法;

  • 新增一个方法;

  • 新增一个property来辅助修复bug;

  • 新增一个Class。

最后,开发者可以安心的把修改后的代码(甚至可以保留Annotation)gitpush,完成热修复工作。

打开黑箱

66.png

就像Objective-C是由Clang编译器和Objective-CRuntime共同实现一样,DynamicCocoa也是由对应的两部分构成:

  • 在Clang的基础上,实现了一个OC源码到JS代码的转换器;

  • 实现OC-JS互调引擎的DynamicCocoaSDK。

我们知道,Clang-LLVM的标准编译流程是从源代码经过预处理、词法解析、语法解析生成语法树,CodeGen生成LLVM-IR,进入编译器后端进行优化和汇编,最终生成目标文件(Mach-O)。

14.png

而我们既希望Clang帮助完成源码处理的步骤,又希望生成结果是JS表示形式,于是在Clang生成抽象语法树(AST)后,我们进行接管,实现了一个OC2JSCodeGen,遍历各个特定语法节点输出JS表示:

15.png

由于转换器和Clang前端标准编译流程相同,所以只要native代码能build,转换器就能build,这也是DynamicCocoa能让动态包和native保持严格一致的先决条件。

注:转换器是基于Clang开发的独立命令行工具,它的使用并不会对原有的Xcode工程产生任何影响。

另一部分是要集成进App的DynamicCocoaSDK,它的职责是为JS中间代码提供Runtime环境,实现OC-JS的互调引擎,能够加载动态bundle,提供便捷的API,整体架构如下:

16.png

其中一些有趣的点:

  • 底层使用libffi来处理各个架构下的callingconventions,实现caller调用栈的构建和callee调用栈的解析,用于实现OC/C函数调用、动态imp、block等。

  • 由于JS的弱类型,数值变量在做计算时很容易丢失类型信息,比如inta=1/2;在OC中表示整除,结果为0,但进入JS就都会按照double计算,结果为0.5,造成了不一致。所以DynamicCocoa接管了JS中的类型信息,强转或运算符都需要特殊处理。

  • 为了实现block,我们构造了和nativeblock一致的内存结构,不论是JS创建的block还是native传进JS的block,都可以无差别的调用。

  • 虽然runtime提供了动态创建OCClass的API,但只能创建MRC的Class,导致ARC下ivar并不会乖乖释放,我们深入到Class和实例真实内存结构中,给动态创建的类增加了ARC能力,并按照Non-FragileABI模拟真实ivar内存布局和ivarlayout编码,如果你重写了dealloc方法,DynamicCocoa甚至能够像native一样自动调用super。

DynamicCocoa带来的改变

DynamicCocoa动态化技术给App开发带来了很大的想象空间:

  • 低成本的动态化:无需额外学习,无需重写代码,可以快速的将已有模块动态化;

  • 协作方式:对于大团队,发布版本不必再彼此牵制;

  • 功能快速迭代:无需经过审核和AppStore发版,像HTML5一样随发随上;

  • App瘦身:Native只需要留好插件入口,实现由网络下发,减少App体积;

  • ABTest:不必局限于Native埋进去的AB功能Test,发版后能动态下发各种Test。

相比跨端方案,也带来了一个新思路:iOS和Android都保留Native开发模式,用各自的方式将Native代码直接动态化,保持各平台的差异性。

Q&A

与JSPatch有什么区别?

两者思路上都是实现JS和OC的互调:DynamicCocoa的重点是动态化能力,优势在于完全不用写JS和更多的语法特性支持;对于HotPatch来说JSPatch是更加小巧、轻量的解决方案。

这套框架在滴滴App有上线使用么?

有,在滴滴App已经上线并使用了好几个版本,如滴滴小巴、专车接送机都有过10k级别的动态化模块上线。

动态包运行的性能是否有很大下降?

动态JS代码的运行要经过频繁的JSCore和OC间的切换,性能相比Native必定会有损耗,但经过优化,现在已经达到了无感知的程度:在我们的实际使用中,若不在页面上添加特定标志,开发者和QA都无法分辨出当前页面运行的是native还是动态包…后续会有详细的性能分析和大家分享。

动态包大小如何?

与资源大小和Native源码量有很大关系,不考虑资源的情况下,量级大概在10000行代码100KB的动态包。

是否支持多线程?

现在简单的支持GCD来处理多线程,可以使用dispatch_async将一个block放到另一个queue中执行。

如何定位动态包的Crash?

动态JS代码运行在JSCore中,并没有直接获取调用栈的方式,我们提供了stacktrace功能,将最近调用栈中每个JS到OC/C的互调都记录下来,在发生Crash时便可以取出来作为附加信息随Crash日志上报给统计平台,方便问题的定位。

会不会过不了苹果审核?

市面上很多动态化、HotPatch方案都基于JS的下发,运行在原生JSCore上,相信只要不在审核期间下发动态功能,Apple是不太会拒绝的。

有没有可能支持Swift直接动态化?

相比OC,Swift的动态化和HotPatch更加有难度,但我们已经有了可行的方案,是可以做到的,只是对于当前滴滴的现状(绝大多数都在用OC开发),紧急程度并不高,后面再考虑支持。

是否有开源计划?

有,我们正在积极的准备相关事项,于2017年初考虑开源。

该从哪里关注后续进展?

滴滴App架构组正式创建了微信公众号DDApp(直接搜索),这也是其中的第一篇文章,我们会在上面发布DynamicCocoa的最新的进展,还会把滴滴iOS和Android开发的干货技术文章分享给大家,欢迎关注。

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:如何用Xcode 8和Swift 3 构建条形码和二维码识别器
下一篇:高度自定义的TabBarController 两个TableView之间的联动
我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部