关于HotfixPatch
在IOS开发领域,由于Apple严格的审核标准和低效率,IOS应用的发版速度极慢,稍微大型的app发版基本上都在一个月以上,所以代码热更新(HotfixPatch)对于IOS应用来说就显得尤其重要。
现在业内基本上都在使用WaxPatch方案,由于Wax框架已经停止维护四五年了,所以waxPatch在使用过程中还是存在不少坑(比如参数转化过程中的问题,如果继承类没有实例化修改继承类的方法无效, wax_gc中对oc中instance的持有延迟释放…)。另外苹果对于Wax使用的态度也处于模糊状态,这也是一个潜在的使用风险。
随着FaceBook开源React Native框架,利用JavaScriptCore.framework直接建立JavaScript(JS)和Objective-C(OC)之间的bridge成为可能,JSPatch也在这个时候应运而生。最开始是从唐巧的微信公众号推送上了解到,开始还以为是在React Native的基础上进行的封装,不过最近仔细研究了源代码,跟React Native半毛钱关系都没有,这里先对JSPatch的作者(不是唐巧,是Bang,博客地址)赞一个。
深入了解JSPatch之后,第一感觉是这个方案小巧,易懂,维护成本低,直接通过OC代码去调用runtime的API,作为一个IOS开发者,很快就能看明白,不用花大精力去了解学习lua。另外在建立JS和OC的Bridge时,作者很巧妙的利用JS和OC两种语言的消息转发机制做了很优雅的实现,稍显不足的是JSPatch只能支持ios7及以上。
由于现在公司的部分应用还在支持ios6,完全取代Wax也不现实,但是一些新上应用已经直接开始支持ios7。个人觉得ios6和ios7的界面风格差别较大,相信应用最低支持版本会很快升级到ios7. 还考虑到JSPatch的成熟度不够,所以决定把JSPatch和WaxPatch结合在一起,相互补充进行使用。下面给大家说一些学习使用体会。
JSPatch和WaxPatch对比
关于JSPatch对比WaxPatch的优势,下面摘抄一下JSPatch作者的话:
方案对比
目前已经有一些方案可以实现动态打补丁,例如WaxPatch,可以用Lua调用OC方法,相对于WaxPatch,JSPatch的优势:
- 1.JS语言: JS比Lua在应用开发领域有更广泛的应用,目前前端开发和终端开发有融合的趋势,作为扩展的脚本语言,JS是不二之选。
- 2.符合Apple规则: JSPatch更符合Apple的规则。iOS Developer Program License Agreement里3.3.2提到不可动态下发可执行代码,但通过苹果JavaScriptCore.framework或WebKit执行的代码除外,JS正是通过JavaScriptCore.framework执行的。
- 3.小巧: 使用系统内置的JavaScriptCore.framework,无需内嵌脚本引擎,体积小巧。
- 4.支持block: wax在几年前就停止了开发和维护,不支持Objective-C里block跟Lua程序的互传,虽然一些第三方已经实现block,但使用时参数上也有比较多的限制。
JSPatch的劣势:
- 相对于WaxPatch,JSPatch劣势在于不支持iOS6,因为需要引入JavaScriptCore.framework。另外目前内存的使用上会高于wax,持续改进中。
JSPatch的实现原理理解
JSPatch的实现原理作者的博文已经很详细的介绍了,我这里就不多说了,贴一下学习之处:
- JSPatch实现原理详解 http://blog.cnbang.net/tech/2808/
- JSPatch Git源码和使用说明 https://github.com/bang590/JSPatch
看实现原理详解的时候对照着源码看,比较好理解,我在这里说一下我对JSPatch的学习和理解:
(1)OC的动态语言特性
不管是WaxPatch框架还是JSPatch的方案,其根本原理都是利用OC的动态语言特性去动态修改类的方法实现。
OC的动态语言特性是在runtime system(全部用C实现,Apple维护了一份开源代码)上实现的,面向对象的Class和instance机制都是基于消息机制。我们平时认为的[object method],正确的理解应该是[receiver sendMsg], 所有的消息发送会在编译阶段编译为runtime c函数的调用:_obj_sendMsg(id, SEL).
详细介绍参考博文:
runtime提供了一些运行时的API
- 反射类和选择器
1 2 |
Class class = NSClassFromString("UIViewController"); SEL selector = NSSelectorFromString("viewDidLoad"); |
- 为某个类新增或者替换方法选择器(SEL)的实现(IMP)
1 2 |
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); |
- 在runtime中动态注册类
1 2 3 |
Class superCls = NSClassFromString(superClassName); cls = objc_allocateClassPair(superCls, className.UTF8String, 0); objc_registerClassPair(cls); |
(2)JS如何调用OC
在JS运行环境中,需要解决两个问题,一个是OC类对象(objc_class)的获取,另一个就是使用对象提供的接口方法。
对于第一个问题,JSPatch在实现中是通过Require调用在JS环境下创建一个class同名对象(js形式),当向OC发送alloc接收消息之后,会将OC环境中创建的对象地址保存到这个这个js同名对象中,js本身并不完成任何对象的初始化。关于JS持有OC对象的引用,其回收的解释在JSPatch作者的博文中有介绍,没有具体测试。详见JSPatch.js代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//请求OC类对象 UIView = require("UIView"); //缓存JS class同名对象 var _require = function(clsName) { if (!global[clsName]) { global[clsName] = { __isCls: 1, __clsName: clsName } } return global[clsName] } //调用class方法,返回OC实例化对象进行封装 var ret = instance ? _OC_callI(instance, selectorName, args, isSuper): _OC_callC(clsName, selectorName, args) //OC创建后返回对象 return@{@"__clsName": NSStringFromClass([obj class]), @"__obj": obj}; //JS中解析OC对象 return _formatOCToJS(ret) //_formatOCToJS if (obj instanceof Object) { var ret = {} for (var key in obj) { ret[key] = _formatOCToJS(obj[key]) } return ret } |
对于第二个问题,JSPatch在JS环境中通过中心转发方式,所有OC方法的调用均是通过新增Object(js)原型方法_c(methodName)完成调用,在通过JavaScriptCore执行JS脚本之前,先将所有的方法调用字符替换
_c(‘method’)的方式; 在_c函数中通过JSContex建立的桥接函数传入参数和返回参数即完成了调用;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//字符替换 static NSString *_regexStr = @"\\.\\s*(\\w+)\\s*\\("; static NSString *_replaceStr = @".__c(\"$1\")("; NSString *formatedScript = [NSString stringWithFormat:@"try{@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]]; //__c()向OC转发调用参数 Object.prototype.__c = function(methodName) { ... return function(){ var args = Array.prototype.slice.call(arguments) return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper) } } //_methodFunc调用桥接函数 var _methodFunc = function(instance, clsName, methodName, args, isSuper) { ... var ret = instance ? _OC_callI(instance, selectorName, args, isSuper): _OC_callC(clsName, selectorName, args) return _formatOCToJS(ret) } //OC中的桥接函数,JS和OC的桥接函数都是通过这样定义 context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) { return callSelector(nil, selectorName, arguments, obj, isSuper); }; context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) { return callSelector(className, selectorName, arguments, nil, NO); }; |
(3)JS如何替换OC方法
JSPatch的主要作用还是通过脚本修复一些线上bug,希望能够达到替换OC方法的目标。JSPatch的实现巧妙之处在于:利用了OC的消息转发机制。
- 1:替换原有selector的IMP实现为一个空的IMP实现,这样当objc_class接受到消息之后,就会进行消息转发, 另外需要将selector的初始实现进行保存;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//selector指向空实现 IMP msgForwardIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature); class_replaceMethod(cls, selector, msgForwardIMP, typeDescription); //保存原有实现,这里进行了修改,增加了恢复现场的支持 NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG@", selectorName]; SEL originalSelector = NSSelectorFromString(originalSelectorName); if(class_respondsToSelector(cls, selector)) { if(!class_respondsToSelector(cls, originalSelector)){ class_addMethod(cls, originalSelector, originalImp, typeDescription); } else { class_replaceMethod(cls, originalSelector, originalImp, typeDescription); } } |
- 2:将替换的JS方法构造一个JPSelector及其IMP实现(根据返回参数构造),添加到当前class中,并通过cls+selecotr全局缓存JS方法(全局缓存并没有多大用途,但是对于后面恢复现场比较有用);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |