在 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,可以用 Lua 调用 OC 方法,相对于 WaxPatch,JSPatch 的优势:
JSPatch 的劣势:
JSPatch 的实现原理作者的博文已经很详细的介绍了,我这里就不多说了,贴一下学习之处:
看实现原理详解的时候对照着源码看,比较好理解,我在这里说一下我对 JSPatch 的学习和理解:
不管是 WaxPatch 框架还是 JSPatch 的方案,其根本原理都是利用 OC 的动态语言特性去动态修改类的方法实现。
OC 的动态语言特性是在 runtime system(全部用 C 实现,Apple 维护了一份开源代码) 上实现的,面向对象的 Class 和 instance 机制都是基于消息机制。我们平时认为的 [object method],正确的理解应该是 [receiver sendMsg], 所有的消息发送会在编译阶段编译为 runtime c 函数的调用:_obj_sendMsg(id, SEL).
详细介绍参考博文:
runtime 提供了一些运行时的 API
- Class class = NSClassFromString("UIViewController");
- SEL selector = NSSelectorFromString("viewDidLoad");
- BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * types);
- IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char * types);
- Class superCls = NSClassFromString(superClassName);
- cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
- objc_registerClassPair(cls);
在 JS 运行环境中,需要解决两个问题,一个是 OC 类对象(objc_class)的获取,另一个就是使用对象提供的接口方法。
对于第一个问题,JSPatch 在实现中是通过 Require 调用在 JS 环境下创建一个 class 同名对象(js 形式),当向 OC 发送 alloc 接收消息之后,会将 OC 环境中创建的对象地址保存到这个这个 js 同名对象中,js 本身并不完成任何对象的初始化。关于 JS 持有 OC 对象的引用,其回收的解释在 JSPatch 作者的博文中有介绍,没有具体测试。详见 JSPatch.js 代码:
- //请求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 建立的桥接函数传入参数和返回参数即完成了调用;
- //字符替换
- 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);
- };
JSPatch 的主要作用还是通过脚本修复一些线上 bug,希望能够达到替换 OC 方法的目标。JSPatch 的实现巧妙之处在于:利用了 OC 的。
- //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);
- }
- }
- if (!_JSOverideMethods[clsName][JPSelectorName]) {
- _initJPOverideMethods(clsName);
- _JSOverideMethods[clsName][JPSelectorName] = function;
- const char * returnType = [methodSignature methodReturnType];
- IMP JPImplementation = NULL;
- //根据返回类型构造
- switch (returnType[0]) {...
- }
- if (!class_respondsToSelector(cls, JPSelector)) {
- class_addMethod(cls, JPSelector, JPImplementation, typeDescription);
- } else {
- class_replaceMethod(cls, JPSelector, JPImplementation, typeDescription);
- }
- }
- static void JPForwardInvocation(id slf, SEL selector, NSInvocation * invocation) {
- NSMethodSignature * methodSignature = [invocation methodSignature];
- NSInteger numberOfArguments = [methodSignature numberOfArguments];
- NSString * selectorName = NSStringFromSelector(invocation.selector);
- NSString * JPSelectorName = [NSString stringWithFormat: @"_JP@", selectorName];
- SEL JPSelector = NSSelectorFromString(JPSelectorName);
- if (!class_respondsToSelector(object_getClass(slf), JPSelector)) {...
- }
- NSMutableArray * argList = [[NSMutableArray alloc] init]; [argList addObject: slf];
- for (NSUInteger i = 2; i < numberOfArguments; i++) {...
- }
- //获取参数之后invoke JPSector调用JSFunction的实现
- @synchronized(_context) {
- _TMPInvocationArguments = formatOCToJSList(argList); [invocation setSelector: JPSelector]; [invocation invoke];
- _TMPInvocationArguments = nil;
- }
- }
Patch 现场恢复的功能主要用于连续更新脚本的应用场景。由于 IOS 的 App 应用按 Home 键或者被电话中断的时候,应用实际上是首先进入到后台运行阶段(applicationWillResignActive),当我们下次再次使用 App 的时候,如果后台应用没有被终止(applicationWillTerminate),那么 App 不会走 appliation:didFinishLaunchingWithOptions 方法,而是会走(applicationWillEnterForeground)。 对于这种场景如果我们连续更新线上脚本,那么第二次脚本更新则无法保留最开始的方法实现,另外恢复现场功能也有助于我们撤销线上脚本能够恢复应用的本身代码功能。
本文在 JSPatch 基础上添加了现场恢复功能;源码地址参考:
说明如下:
(1)在 JPEngine.h 中添加了两个启动和结束的调用函数如下:
- void js_start(NSString * initScript);
- void js_end();
(2) JPEngine.m 中调用函数的实现以及恢复现场对部分代码的修改:主要是利用了替换方法和新增方法的 cache(_JSOverideMethods, 主要是这个)
- //处理替换方法,selector指回最初的IMP,JPSelector和ORIGSelector都指向未实现IMP
- if ([JPSelectorName hasPrefix: @"_JP"]) {
- if (class_getMethodImplementation(cls, @selector(forwardInvocation: )) == (IMP) JPForwardInvocation) {
- SEL ORIGforwardSelector = @selector(ORIGforwardInvocation: );
- IMP ORIGforwardImp = class_getMethodImplementation(cls, ORIGforwardSelector);
- class_replaceMethod(cls, @selector(forwardInvocation: ), ORIGforwardImp, "v@:@");
- class_replaceMethod(cls, ORIGforwardSelector, _objc_msgForward, "v@:@");
- }
- NSString * selectorName = [JPSelectorName stringByReplacingOccurrencesOfString: @"_JP"withString: @""];
- NSString * ORIGSelectorName = [JPSelectorName stringByReplacingOccurrencesOfString: @"_JP"withString: @"ORIG"];
- SEL JPSelector = NSSelectorFromString(JPSelectorName);
- SEL selector = NSSelectorFromString(selectorName);
- SEL ORIGSelector = NSSelectorFromString(ORIGSelectorName);
- if (class_respondsToSelector(cls, ORIGSelector) && class_respondsToSelector(cls, selector) && class_respondsToSelector(cls, JPSelector)) {
- NSMethodSignature * methodSignature = [cls instanceMethodSignatureForSelector: ORIGSelector];
- Method method = class_getInstanceMethod(cls, ORIGSelector);
- char * typeDescription = (char * ) method_getTypeEncoding(method);
- IMP forwardEmptyIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);
- IMP ORIGSelectorImp = class_getMethodImplementation(cls, ORIGSelector);
- class_replaceMethod(cls, selector, ORIGSelectorImp, typeDescription);
- class_replaceMethod(cls, JPSelector, forwardEmptyIMP, typeDescription);
- class_replaceMethod(cls, ORIGSelector, forwardEmptyIMP, typeDescription);
- }
- }
- //处理添加的新方法
- else {
- isClsNew = YES;
- SEL JPSelector = NSSelectorFromString(JPSelectorName);
- if (class_respondsToSelector(cls, JPSelector)) {
- NSMethodSignature * methodSignature = [cls instanceMethodSignatureForSelector: JPSelector];
- Method method = class_getInstanceMethod(cls, JPSelector);
- char * typeDescription = (char * ) method_getTypeEncoding(method);
- IMP forwardEmptyIMP = getEmptyMsgForwardIMP(typeDescription, methodSignature);
- class_replaceMethod(cls, JPSelector, forwardEmptyIMP, typeDescription);
- }
- }
JSPatch 在使用过程中也会遇到不少坑,虽然这两个框架现在虽然都能够做到新增可执行代码,但是将其应用到开发功能组件还不太可取。比如说我在第一次使用 JSPatch 遇到了一个坑:
来源: http://lib.csdn.net/article/ios/42232