大家好,今天小编来为大家解答深入解析:iOS应用热更新技术——JSPatch原理与现场恢复策略这个问题,很多人还不知道,现在让我们一起来看看吧!
现在业界基本上都在使用WaxPatch解决方案。由于Wax框架已经停止维护四五年了,waxPatch的使用仍然存在很多陷阱(比如参数转换过程中的问题,如果继承的类没有实例化而修改了继承的类),方法是无效,wax_gc中oc中instance的持有被延迟释放.)。另外,苹果对于Wax的使用态度也比较模糊,这也是一个潜在的使用风险。
借助FaceBook开源的React Native框架,使用JavaScriptCore.framework直接在JavaScript(JS)和Objective-C(OC)之间建立桥梁成为可能。 JSPatch也在此时应运而生。我最早是从唐桥的微信公众号推送中得知的。一开始我以为是在React Native的基础上封装的。不过最近仔细研究了源码,发现和React Native没有任何关系。这里先介绍一下JSPatch的作者(不是唐乔,而是Bang,博客地址)喜欢的。
深入了解JSPatch后,我的第一印象是这个解决方案体积小,易于理解,维护成本低。它通过OC代码直接调用运行时API。作为一名IOS开发者,我可以很快的理解它,而不需要花费大量的精力去理解和学习。卢阿。另外,作者在建立JS和OC的Bridge时,巧妙地利用了JS和OC的消息转发机制,做出了非常优雅的实现。稍有缺点的是JSPatch只能支持ios7及以上版本。
由于公司的一些应用程序仍然支持ios6,因此完全取代Wax不太现实,但一些新的应用程序已经直接开始支持ios7。个人感觉ios6和ios7的界面风格差别较大。相信应用的最低支持版本很快就会升级到ios7。另外考虑到JSPatch还不够成熟,我决定将JSPatch和WaxPatch结合起来,优势互补。下面我给大家讲一下一些学习和使用的心得。
JSPatch 和WaxPatch 的比较
关于JSPatch相对于WaxPatch的优点,这里摘录JSPatch的作者:
来源: JSPatch 动态更新iOS APP
方案比较
已经有一些解决方案可以实现动态修补,例如WaxPatch,它可以使用Lua调用OC方法。与WaxPatch相比,JSPatch具有以下优点:
1、JS语言:JS在应用开发领域比Lua应用更广泛。目前有前端开发和终端开发融合的趋势。作为一种扩展脚本语言,JS是最好的选择。
2. 符合Apple规则:JSPatch更符合Apple的规则。 iOS 开发者计划许可协议第3.3.2 节提到,可执行代码不能动态分发,通过Apple 的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的动态语言特性是在运行时系统上实现的(全部用C实现,Apple维护开源代码)。面向对象的Class和实例机制都是基于消息机制的。我们通常认为的【对象方法】的正确理解是【接收者sendMsg】。所有发送的消息都会在编译阶段被编译成运行时c函数调用:_obj_sendMsg(id, SEL)。
详细介绍可以参考博文:
Objective-C 运行时详细信息
Objective-C运行时源代码_Apple
Runtime提供了一些运行时API
反射类和选择器
类class=NSClassFromString("UIViewController"); SEL 选择器=NSSelectorFromString("viewDidLoad");
添加或替换类(IMP) 的方法选择器(SEL) 的实现
BOOLclass_addMethod(类cls, SEL 名称, IMP imp,constchar*类型);IMPclass_replaceMethod(类cls, SEL 名称, IMP imp,constchar*类型);
在运行时动态注册类
类superCls=NSClassFromString(superClassName); cls=objc_allocateClassPair(superCls, className.UTF8String,0); objc_registerClassPair(cls);
(2)JS如何调用OC
在JS运行环境中,需要解决两个问题。一是获取OC类对象(objc_class),二是使用该对象提供的接口方法。
关于第一个问题,JSPatch是通过Require调用在JS环境中创建一个同名的类对象(js形式)来实现的。发送alloc给OC接收消息后,OC环境中创建的对象地址会保存到this中。在与js同名的对象中,js本身并没有完成任何对象的初始化。关于JS持有的OC对象的引用,JSPatch作者的博文中介绍了回收的解释,但没有具体测试。详细信息请参见JSPatch.js 代码:
//请求OC类对象UIView=require("UIView");//缓存同名JS类对象var_require=function(clsName) {if(!global[clsName]) { global[clsName]={ __isCls:1, __clsName: clsName } } returnglobal[clsName] }//调用类方法,返回OC实例化对象进行封装varret=instance? _OC_callI(instance,selectorName,args,isSuper): _OC_callC(clsName,selectorName,args)//返回OC创建后的对象return@ {@"__clsName":NSStringFromClass([objclass]),@"__obj":obj};//解析JS中的OC对象return_formatOCToJS(ret)//_formatOCToJSif(obj instanceofObject) {varret={}for(varkey in obj) { ret [key]=_formatOCToJS(obj[key]) }returnret }
关于第二个问题,JSPatch在JS环境下采用的是集中转发。所有OC方法调用都是通过新的Object(js)原型方法_c(methodName)完成。通过JavaScriptCore 执行JS 脚本之前,首先所有方法都会调用字符替换
_c("方法") 方法;在_c函数中,通过JSContex建立的桥函数传入参数和返回参数,完成调用;
//字符替换staticNSString*_regexStr=@"\.\s*(\w+)\s*\(";staticNSString*_replaceStr=@".__c("$1")(";NSString *formatedScript=[NSStringstringWithFormat:@"try{@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];//__c() OC转发调用参数Object.prototype.__c=function(methodName) { .returnfunction(){ 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, selectName, args)return_formatOCToJS(ret) }//OC中的桥接函数,JS和OC的桥接函数是这样定义的context[@"_OC_callI"]=^id(JSValue *obj,NSString*selectorName, JSValue *arguments ,BOOLisSuper) {returncallSelector(nil, 选择器名称, 参数, obj, isSuper); }; context[@"_OC_callC"]=^id(NSString*className,NSString*selectorName, JSValue *arguments) {returncallSelector(className,selectorName,arguments,nil,NO); };
(3)如何用JS替换OC方法
JSPatch的主要功能是通过脚本修复一些线上的bug,希望达到替代OC方式的目的。 JSPatch实现的巧妙之处在于它利用了OC的消息转发机制。
1: 将原来选择器的IMP实现替换为空的IMP实现,这样当objc_class收到消息时,就会转发该消息。另外,选择器的初始实现需要保存;
//selector指向IMP的空实现msgForwardIMP=getEmptyMsgForwardIMP(typeDescription, methodSignature); class_replaceMethod(cls, Selector, msgForwardIMP, typeDescription);//保存原来的实现,这里修改了一下,增加了恢复场景的支持NSString*originalSelectorName=[ NSStringstringWithFormat:@"ORIG@", selectorName]; SEL OriginalSelector=NSSelectorFromString(originalSelectorName);if(class_respondsToSelector(cls,选择器)){if(!class_respondsToSelector(cls,originalSelector)){ class_addMethod(cls,originalSelector,originalImp,typeDescription); }其他{ class_replaceMethod(cls,originalSelector,originalImp,typeDescription); } }
2: 构造一个被替换的JS方法的JPSelector及其IMP实现(根据返回参数构造),添加到当前类中,通过cls+selecotr全局缓存JS方法(全局缓存没有多大用处,但是对于后期恢复场景更有用);
if(!_JSOverideMethods[clsName][JPSelectorName]) { _initJPOverideMethods(clsName); _JSOverideMethods[clsName][JPSelectorName]=function;constchar *returnType=[methodSignature methodReturnType]; IMP JPImplementation=NULL;//根据返回类型[0]构造switch(returnType){ . }if(!class_respondsToSelector(cls, JPSelector)){ class_addMethod(cls, JPSelector, JPImplementation, typeDescription); }其他{ class_replaceMethod(cls, JPSelector, JPImplementation,typeDescription); } }
3: 然后重写各个替换方法类的forwardInitation的实现进行拦截。如果拦截到的Inspiration的选择器转换成JPSelector并且能够响应,就说明它是一个替换方法。然后从Inphonic中取出参数后,调用JPSelector的IMP;
staticvoidJPForwardInitation(idslf, SEL 选择器,NSInvocau*invocation) {NSMethodSignature*methodSignature=[调用methodSignature];NSIntegernumberOfArguments=[methodSignature numberOfArguments];NSString*selectorName=NSStringFromSelector(involution.selector);NSString*JPSelectorName=[NSStringstringWithFormat:@"_ JP@ " , 选择器名称]; SEL JPSelector=NSSelectorFromString(JPSelectorName);if(!class_respondsToSelector(object_getClass(slf), JPSelector)) { . }NSMutableArray*argList=[[NSMutableArrayalloc] init]; [argList addObject:slf];for(NSUntegeri=2; i numberOfArguments; i++) { . }//获取参数后,invoke JPSector 调用JSFunction 的实现@synchronized(_context) { _TMPInitationArguments=formatOCToJSList(argList); } [调用setSelector:JPSelector]; [调用invoke]; _TMPIncationArguments=nil; } }
补丁现场恢复补充
Patch现场恢复功能主要应用于脚本持续更新的应用场景。因为当IOS App应用按下Home键或者被电话打断时,应用实际上是先进入后台运行阶段(applicationWillResignActive)。当我们下次再次使用App时,如果后台应用还没有被终止(applicationWillTerminate),那么App就不会走application:didFinishLaunchingWithOptions方法,而是会走(applicationWillEnterForeground)。对于这种场景,如果我们不断更新在线脚本,那么第二次脚本更新将无法保留原来的方法实现。另外,恢复现场功能也将帮助我们撤消在线脚本,恢复应用程序自身的代码功能。
JSPatch 的实时恢复
本文在JSPatch的基础上增加了现场恢复功能;源码地址参考:
新增JSPatchDemo: 用于现场恢复
https://github.com/philonpang/JSPatch.git
说明如下:
(1) JPEngine.h中添加两个起始和结束调用函数,如下:
voidjs_start(NSString* initScript);voidjs_end();
(2)JPEngine.m中调用函数的实现以及恢复站点部分代码的修改:主要使用替换方法和新方法的缓存(_JSOverideMethods,主要是这个)
//处理替换方法,selector指向回原来的IMP,JPSelector和ORIGSelector都指向未实现的IMPif([JPSelectorName hasPrefix:@"_JP"]){if(class_getMethodImplementation(cls,@selector(forwardIn Vocation:))==( IMP)JPForwardInitation ) { SEL ORIGforwardSelector=@selector(ORIGforwardIn Vocation:); IMP ORIGforwardImp=class_getMethodImplementation(cls, ORIGforwardSelector); class_replaceMethod(cls,@selector(forwardIn Vocation:), ORIGforwardImp,"v@:@"); class_replaceMethod(cls, ORIGforwardSelector, _ob jc_msgForward, "v@:@"); }NSString*selectorName=[JPSelectorName stringByReplacingOccurrencesOfString:@"_JP"withString:@""]; NSString*ORIGSelectorName=[JPSelectorName stringByReplacingOccurrencesOfString:@"_JP"withString:@"ORIG"]; S EL JPSelector=NSSelectorFromString(JPSelectorName) ; SEL 选择器=NSSelectorFromString(selectorName); SEL ORIGSelector=NSSelectorFromString(ORIGSelectorName);if(class_respondsToSelector(cls, ORIGSelector) class_respondsToSelector(cls, 选择器) class_respondsToSelector(cls, JPSelector)){NSMethodSignature*methodSignature=[cls instanceMethodSignatureFor Selector:ORIGSelector];方法方法=class_getInstanceMethod(cls, ORIGSelector); char*typeDescription=(char*)method_getTypeEncoding(method); IMPforwardEmptyIMP=getEmptyMsgForwardIMP(typeDescription, methodSignature); IMP ORIGSelectorImp=class_getMethodImplementation(cls, ORIGSelector); class_replaceMethod(cls, 选择器, ORIGSelector Imp, typeDescription ; (cls, JPSelector)){NSMethodSignature*methodSignature=[cls instanceMethodSignatureForSelector:JPSelector];方法方法=class_getInstanceMethod(cls, JPSelector); char*typeDescription=(char*)method_getTypeEncoding(method); IMPforwardEmptyIMP=getEmptyMsgForwardIMP(typeDescription, methodSignature) ; class_replaceMethod(cls,JPSelector,forwardEmptyIMP,typeDescription); } }
HotfixPatch的陷阱
JSPatch在使用过程中也会遇到很多陷阱。虽然这两个框架现在可以添加可执行代码,但是不建议将其应用到功能组件的开发中。比如我第一次使用JSPatch时遇到的坑:
当JS脚本重写了派生类中未实现的继承类的可选协议方法时,重新加载tableView时不会调用JS patch方法,但如果在tableView中显式调用则可以调用替换选择器方法;另外,如果在派生类中通过重写这个协议方法,就可以调用它;
关于深入解析:iOS应用热更新技术——JSPatch原理与现场恢复策略到此分享完毕,希望能帮助到您。
【深入解析:iOS应用热更新技术——JSPatch原理与现场恢复策略】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于不用再刷机修复 bug 了!
有11位网友表示赞同!
这篇文章来得太及时了,最近几个 app 的 Bug 让我头疼。
有18位网友表示赞同!
JSPatch 能做到实时更新?听起来很厉害啊!
有11位网友表示赞同!
感觉 iOS 热更新的应用场景很多,对开发来说也是一种福音呀!
有14位网友表示赞同!
想自己动手试下 JSPatch 实现原理,学习一下。
有9位网友表示赞同!
patch 现场恢复是个啥意思呢? 具体的例子能细说一下吗?
有16位网友表示赞同!
之前听说过热更新,现在知道是可以通过 JSPatch 实现的了!
有14位网友表示赞同!
这篇文章有不会太难懂吧?我刚接触 iOS 开发。
有5位网友表示赞同!
看这样子的,iOS 系统将来应该越来越智能和灵活了!
有14位网友表示赞同!
希望能看到更多关于 iOS 热更新的文章,深入解析原理!
有5位网友表示赞同!
如果热更新功能能够应用到系统层面,那就简直完美了!
有15位网友表示赞同!
感觉 JSPatch 是一种非常巧妙的技术方案。
有9位网友表示赞同!
这种技术能提高用户体验,开发者也能更快地修复 bug。
有13位网友表示赞同!
iOS 开发终于跟 Android 不相上下啦!
有15位网友表示赞同!
学习 JSPatch 技术可以丰富我的 iOS 开发技能!
有15位网友表示赞同!
希望以后更多开发工具能够像 JSPatch 这样方便实用!
有7位网友表示赞同!
感觉这个标题讲得很到点,能详细解释吗?
有8位网友表示赞同!
这种技术应用的范围是不是很广?
有15位网友表示赞同!
我一直在想怎么优化 iOS 应用,这篇文章很有帮助!
有17位网友表示赞同!