IOS热更新方案

2017/03/04

私人 : JSPatch

//main.js
defineClass("XRTableViewController", {
  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
    var row = indexPath.row()
    if (self.dataSource().length > row) {  //加上判断越界的逻辑
      var content = self.dataArr()[row];
      var controller = XRViewController.alloc().initWithContent(content);
      self.navigationController().pushViewController(controller);
    }
  }
})

上面是JSPatch动态下发客户端解决线上Bug的实例,通过这段代码将 XRTableViewController 原来的 tableView_didSelectRowAtIndexPath 替换成新的JS函数实现。那么问题来了?

  1. JSPatch里的JS对象如何有OC对象的方法?
    • 得到OC对象的方法之后,如何将这个方法名和参数传给OC?
    • 传递给OC之后,怎么替换原来的方法 ?
    • 内存管理上有没有问题?数据结构的支持怎么样?是否支持多线程?

1.JSPatch里的JS对象如何有OC对象的方法?

require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)
  • 类方法 :require(‘UIView’) 方法生成了一个UIView的全局变量,这个全局变量可以转换UIView的所有类方法。
  • 实例方法:JS通过自己的语言特性实现一个解释器能解释自己的语法 ,然后在JS代码被调用前,用正则将方法全都转化成 UIView.alloc().init() -> UIView.__c('alloc')().__c('init')() “给JS对象的父类添加元函数 __c
//JSPatch.js
Object.defineProperty(Object.prototype, '__c', {value: function(methodName) {
  if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
  var self = this
  return function(){
    var args = Array.prototype.slice.call(arguments)
    return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
  }
}})

2.得到OC对象的方法之后,如何将这个方法名和参数传给OC?

元函数 __c 里将消息统一转发到 _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)

  var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
  	....省略部分代码
    var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
                         _OC_callC(clsName, selectorName, args)
    return _formatOCToJS(ret)
  }

同过JaveScriptCore提供的JSContext JS运行环境,调用OC代码。

	//JPEngine.m
    context[@"_OC_callI"] = ^id(JSValue `obj, NSString `selectorName, JSValue `arguments, BOOL isSuper) {
        return callSelector(nil, selectorName, arguments, obj, isSuper);
    };

在callSelector方法中用JS传过来的obj,selectorName,argus 生成 NSInvocation 然后调用 [invocation invoke] 实现OC方法调用.

JSPatch 通过 JavaScriptCore 将实现了js调用oc的过程,jscore就相当于一个解释器,将js的代码都依次翻译成oc的方法去执行。

3.方法名和参数传递给OC之后,怎么替换原来的方法 ?

替换的方法大家都知道 class_replaceMethod,但是这个函数要穿入事先要写好函数IMP static void newTableView_didSelectRowAtIndexPath (id slf, SEL sel ,NSIndexPath indexPath) 的,不可能为每一个要替换的OC的方法都要写一遍呀?这里利用了_objc_msgForward函数。

_objc_msgForward 是一个C语言的全局函数,当NSObject 接收到 未知的消息时就会返回这个_objc_msgForward的函数,并传入 id,sel,args调用。调用结果就是 实现消息的转发,_objc_msgForward里面会依次对id发送这些消息。

  1. resolveInstanceMethod: / resolveClassMethod: 这里可以有机会用class_addMethod 方法马上给自己添加一个方法实现
  2. forwardingTargetForSelector: 尝试找到一个能响应该消息的对象,return otherObj or 也可以直接返回 nil 就会调用 doesNotRecognizeSelector ,这里可以用于在对象和代理插入中间者,实现代理方法的捕获。
  3. methodSignatureForSelector: 自己不能实现 又找不到别的对象,那么就用NSInvocation打包这个消息的所有信息传给 forwardInvocation: 做最后的消息转发 ,也可以直接返回 nil 就会调用 doesNotRecognizeSelector
  4. forwardInvocation: 这里得到已经签名好的invocation。JSPatch可以在这里面拿到这个消息的所有的信息,包括所有参数,然后将这些参数发送到JSPatch新添加的方法中去 eg:JSviewWillAppear ,从而调用JS的函数实现。
  5. doesNotRecognizeSelector:

JSPatch 的做法就是先给这个类 XRTableViewController 添加两个方法:

  • ORIGtableView_didSelectRowAtIndexPath 指向原来的IMP实现
  • JPtableView_didSelectRowAtIndexPath 新的JS代码实现

再用 class_replaceMethod() 将要替换的原生方法 tableView_didSelectRowAtIndexPath 的指针替换为 _objc_msgForward , 对象接收到这个消息之后就会走消息转发流程 将方法名和参数打包成一个NSInvocation. 发送到 forwardInvocation:

重写 XRTableViewControllerforwardInvocation: 函数 将收到的NSInvocation里面的参数拿出来,传给 JPtableView_didSelectRowAtIndexPath 就实现了方法的替换。

4.内存管理上有没有问题?数据结构的支持怎么样?是否支持多线程?

虽然 JavaScriptCore 可以实现 JS 和OC环境的互通,函数的相互调用,基本数据结构的转换,但其中OC的语言特性还是没法被JS解释 ,还有可能的内存问题 ,例如多线程的处理上,JavaScript 语言是单线程的,在 OC 使用 JavaScriptCore 引擎执行 JS 代码时,会对 JS 代码块加锁,保证同个 JSContext 下的 JS 代码都是顺序执行 ,作者新加入了这样的语法.performSelectorInOC(selector, arguments, callback) 让函数的调用从JS的环境转移到OC上,这在写法上需要按照作者事先定义好的语法书写,很容易掉进未知的坑里。导致开发环境不一致。

滴滴 : DynamicCocoa

从Clang编译器 生成语法树后, CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。滴滴 自己翻译树上的每一个语法节点。用js代码来表示,输出 .js 文件。动态下发到app端,同时放入客户端这边的 DynamicCocoaEngineSDK 就相当于一个JS-OC的运行时系统,帮助OC和JS相互调用。

值得一提的是,CodeGen 生产 LLVM IR 做了大量的OC语言特性工作,这也是说OC是通过Clang+OCRuntime实现的根据,因为在这个过程中,真正的开始将OC的各个类,方法,成员变量这些转化内存中的实实在在的结构体,放到可执行文件Mach-O的section中,还有将语法树上的调用表达式转化成objc_msgSend,super则转换成objc_msgSendSuper,插入ARC代码,还有 strong,weak,copy,atomic 合成 @property 自动实现 setter 和 getter。还有__block 和 __weak 的实现,block_layout 数据结构的布局等,DynamicCocoa 借鉴IR生产OC各个结构的内存分布,在生成JS对象时,用一样的内存结构,在JS和OC相互调用,传递指针时,有了极大的便利,从而支持众多OC语法特性。

DynamicCocoa 好处就是不用学习js,使用OC原生技术栈就能够轻易开发拥有热更新能力的app ,因为使用底层库 libffi ,使支持的OC语法也更加全面。

腾讯 : OCS

像滴滴相似的地方是,都从clang的生成语法树之后开始接管,但OCS自己开发了一套 编译器OCSCompiler用于生成定义好的一套 字节码指令集OCScript,然后运行在自己开发了一套解释执行系统 OCSVM上。 OCSVM这套系统通过 OCSBridge无缝对接OCRunTime,驱动Native运行完成逻辑执行, 相比滴滴 DynamicCocoa 使用JS做中间语言和JSCore解释执行系统。自己开发了一套OCScript+ OCSVMOCSBridge 各个部分都基于 OCSABI 来构建,OCSABI使用汇编来实现,确保了虚拟机可以与Native最底层进行直接高速通信,效率非常高。

脸书 : React Native

利用JS做中间语言,JavaScriptCore做JS的解析器,但JS和OC之间的调用没用通过 JavaScriptCore,而是通过webview提供的-stringByEvaluatingJavaScriptFromString方法实现两边互通,估计是考虑JSCore在ios7一下不兼容的问题,在OC端和JS端每一面都做了一个对接桥,Object-c BridgeJs BridgeObject-c Bridge这边 通过编译属性__attribute__ 将需要导出的OC方法都添加到 Object-c Bridge 里面,同时也在 Js Bridge 端生产一对一的OC调用模型ModuleID,并且Js Bridge 还有一个消息队列,Js Bridge 收到JS的调用信号就会生产相应的OC调用模型 ModuleID ,然后放到消息队列里面,依次发送到Object-c Bridge那边实现调用。

另外在界面渲染上,React Native采用前端的DOM技术,对界面的渲染做了缓存和优化,只会刷新被修改的部分区域或组件。UI体验上也很流畅

阿里 : Weex

用前端技术写WebAPP,同样将iOS系统api暴漏给前端。//未完待续

微信 : 小程序

用前端技术写WebAPP,同样将iOS系统api暴漏给前端。//未完待续

Post Directory