jspatch

JSPatch在社区的推进下不断在优化改善,这篇文章总结下这几个月以来JSPatch的一些新特性,以及它们的完成原理。
performSelectorInOC
JavaScript语言是单线程的,在OC运用JavaScriptCore引擎履行JS代码时,会对JS代码块加锁,确保同个JSContext下的JS代码都是次序履行。所以调用JSPatch替换的办法,以及在JSPatch里调用OC办法,都会在这个锁里履行,这导致三个问题:
JSPatch替换的办法无法并行履行,假如假如主线程和子线程一起运行了JSPatch替换的办法,这些办法的履行都会次序排队,主线程会等待子线程的办法履行完后再履行,假如子线程办法耗时长,主线程会等好久,卡住主线程。
某种状况下,JavaScriptCore的锁与OC代码上的锁混合时,会发生死锁。
UIWebView的初始化会与JavaScriptCore抵触。若在JavaScriptCore的锁里(第一次)初始化UIWebView会导致webview无法解析页面。
为处理这些问题,JSPatch新增了.performSelectorInOC(selector,arguments,callback)接口,能够在履行OC办法时脱离JavaScriptCore的锁,一起又确保程序次序履行。
举个比如:
defineClass(‘JPClassA’,{
methodA:function(){
//runinmainThread
},
methodB:function(){
//runinchildThread
varlimit=20;
vardata=self.readData(limit);
varcount=data.count();return{data:data,count:count};
}
})
上述比如中若在主线程和子线程一起调用-methodA和-methodB,而-methodB里self.readData(limit)这句调用耗时较长,就会卡住主线程办法-methodA的履行,对此能够让这个调用改用.performSelectorInOC()接口,让它在JavaScriptCore锁开释后再履行,不卡住其他线程的JS办法履行:
defineClass(‘JPClassA’,{
methodA:function(){//runinmainThread},
methodB:function(){//runinchildThreadvarlimit=20;returnself.performSelectorInOC(‘readData’,[limit],function(ret){varcount=ret.count();return{data:ret,count:count};
});
}
})
这两份代码在调用次序上的区别如下图:
第一份代码对应左边的流程图,-methodB办法被替换,当OC调用到-methodB时会去到JSPatch核心的JPForwardInvocation办法里,在这儿面调用JS函数-methodB,调用时JavascriptCore加锁,接着在JS函数里做这种处理,调用reloadData()函数,从而去到OC调用-reloadData办法,这时-reloadData办法是在JavaScriptCore的锁里调用的。直到JS函数履行完毕return后,JavaScriptCore的才解锁,完毕本次调用。
第二份代码对应右边的流程图,前面是一样的,调用JS函数-methodB,JavaScriptCore加锁,但-methodB函数在调用某个OC办法时(这儿是reloadData()),不直接去调用,而是直接return回来一个目标{obj},这个{obj}的结构如下:
{
__isPerformInOC:1,
obj:self.__obj,
clsName:self.__clsName,sel:args[0],
args:args[1],cb:args[2]
}
JS函数回来这个目标,JS的调用就完毕了,JavaScriptCore的锁也就开释了。在OC能够拿到JS函数的回来值,也就拿到了这个目标,然后判别它是否__isPerformInOC=1目标,若是就依据目标里的selector/参数等信息调用对应的OC办法,这时这个OC办法的调用是在JavaScriptCore的锁之外调用的,咱们的意图就达到了。
履行OC办法后,会去调{obj}里的的cb函数,把OC办法的回来值传给cb函数,重新回到JS去履行代码。这儿会循环判别这些回调函数是否还回来__isPerformInOC=1的目标,若是则重复上述流程履行,不是则完毕。
整个原理便是这样,相关代码在这儿和这儿,完成起来其实挺简略,也不会对其他流程和逻辑形成影响,便是理解起来会有点费劲。
performSelectorInOC文档里还有关于死锁的比如,有兴趣能够看看。
可变参数办法调用
一向以来这样参数个数可变的办法是不能在JSPatch动态调用的:
-(instancetype)initWithTitle:(nullableNSString*)titlemessage:(nullableNSString*)messagedelegate:(nullableid)delegatecancelButtonTitle:(nullableNSString*)cancelButtonTitleotherButtonTitles:(nullableNSString*)otherButtonTitles,…
原因是JSPatch调用OC办法时,是依据JS传入的办法名和参数拼装成NSInvocation动态调用,而NSInvocation不支撑调用参数个数可变的办法。
后来@wjacker换了种办法,用objc_msgSend的办法支撑了可变参数办法的调用。之前一向想不到运用objc_msgSend是由于它不适用于动态调用,在办法界说和调用上都是固定的:
1.界说
需求事前界说好调用办法的参数类型和个数,例如想通过objc_msgSend调用办法
-(int)methodWithFloat:(float)num1withObj:(id)objwithBool:(BOOL)flag
那就需求界说一个这样的c函数:
int(*new_msgSend)(id,SEL,float,id,BOOL)=(int(*)(id,SEL,float,id,BOOL))objc_msgSend;
才干通过new_msgSend调用这个办法。而这个进程是无法动态化的,需求编译时确认,而各种办法的参数/回来值类型不同,参数个数不同,是没办法在编译时穷举写完的,所以不能用于一切办法的调用。
而关于可变参数办法,只支撑参数类型和回来值类型都是id类型的办法,现已能够满意大部分需求,所以让运用它变得可能:
id(*new_msgSend1)(id,SEL,id,…)=(id(*)(id,SEL,id,…))objc_msgSend;
这样就能够用new_msgSend1调用固定参数一个,后续是可变参数的办法了。实际上在模拟器这个办法也能够支撑固定参数是N个id的办法,也便是现已满意咱们调用可变参数办法的需求了,但依据@wjacker和@Awhisper的测试,在真机上不可,不同的固定参数都需求给它界说好对应的函数才行,官网文档对这点略有阐明。所以,多了一大堆这样的界说,以敷衍1-10个固定参数的状况:
id(*new_msgSend2)(id,SEL,id,id,…)=(id(*)(id,SEL,id,id,…))objc_msgSend;
id(*new_msgSend3)(id,SEL,id,id,id,…)=(id(*)(id,SEL,id,id,id,…))objc_msgSend;
id(*new_msgSend4)(id,SEL,id,id,id,id,…)=(id(*)(id,SEL,id,id,id,id,…))objc_msgSend;

2.调用
处理上述参数类型和个数界说问题后,还有调用的问题,objc_msgSend不像NSInvocation能够在运行时动态增加拼装传入的参数个数,objc_msgSend则需求在编译时确认传入多少个参数。这关于1-10个参数的调用,不得不必ifelse写10遍调用语句,别的依据办法界说的固定参数个数不一样,还需求调用不同的new_msgSend函数,所以需求写10!条调用,所以有了这样的大长篇(gist代码点击预览)。后来用宏格式化了一下,会美观一点。
defineProtocol
JSPatch为一个类新增原本OC不存在的办法时,一切的参数类型都会界说为id类型,这样完成是由于这种在JS里新增的办法一般不会在OC上调用,而是在JS上用,JS能够以为悉数变量都是目标,没有类型之分,所以悉数界说为id类型。
但在实际运用JSPatch进程中,呈现了这样的需求:在OC里.h文件界说了一个办法,这个办法里的参数和回来值不都是id类型,但是在.m文件中由于忽略没有完成这个办法,导致其他地方调用这个办法时找不到这个办法形成crash,要用JSPatch修正这样的bug,就需求JSPatch能够动态增加指定参数类型的办法。
实际上假如在JS用defineClass()给类增加新办法时,通过某些接口把办法的各参数和回来值类型名传进去,内部再做些处理就能够处理上述问题,但这样会把defineClass接口搞得很复杂,不期望这样做。终究@Awhisper想出了个很好的办法,用动态新增protocol的办法支撑。
首先defineClass是支撑protocol的:
defineClass(“JPViewController:UIViewController”,{})
这样做的作用是,当增加Protocol里界说的办法,而类里没有完成的办法时,参数类型不再满是id,而是会依据Protocol里界说的参数类型去增加。
所以若想增加一些指定参数类型的办法,只需动态新增一个protocol,界说新增的办法名和对应的参数类型,再在defineClass界说里加上这个protocol就能够了。这样的不污染defineClass()的接口,也没有更多概念,非常简洁地处理了这问题。范例:
defineProtocol(‘JPDemoProtocol’,{
stringWithRect_withNum_withArray:{
paramsType:”CGRect,float,NSArray*”,
returnType:”id”,
},
}
defineClass(‘JPTestObject:NSObject’,{
stringWithRect_withNum_withArray:function(rect,num,arr){//userect/num/arrparamshere
return@”success”;
},
}
具体完成原理原作者已写得挺清楚,参见这儿。
支撑重写dealloc办法
之前JSPatch不能替换-dealloc办法,原因:
1.按之前的流程,JS替换-dealloc办法后,调用到-dealloc时会把self包装成weakObject传给JS,在包装的时候就会呈现以下crash:
Cannotformweakreferencetoinstance(0x7fb74ac26270)ofclassJPTestObject.Itispossiblethatthisobjectwasover-released,orisintheprocessofdeallocation.
意思是在dealloc进程中目标不能赋给一个weak变量,无法包装成一个weakObject给JS。
2.若在这儿不包装当时调用目标,或不传任何目标给JS,就能够成功履行到JS上替换的dealloc办法。但这时没有调用原生dealloc办法,此目标不会开释成功,会形成内存泄露。
-dealloc被替换后,原-dealloc办法IMP对应的selector现已变成ORIGdealloc,若在履行完JS的dealloc办法后再强制调用一遍原OC的ORIGdealloc,会crash。猜测原因是ARC对-dealloc有特殊处理,履行它的IMP(也便是真实函数)时传进去的selectorName有必要是dealloc,runtime才干够调用它的[superdealloc],做一些其他处理。
到这儿我就没什么办法了,后来@ipinka来了一招欺骗ARC的完成,处理了这个问题:
1.首先对与第一个问题,调用-dealloc时self不包装成weakObject,而是包装成assignObject传给JS,处理了这个问题。
2.关于第二个问题,调用ORIGdealloc时由于selectorName改变,ARC不认这是dealloc办法,所以用下面的办法调用:
ClassinstClass=object_getClass(assignSlf);
MethoddeallocMethod=class_getInstanceMethod(instClass,NSSelectorFromString(@”ORIGdealloc”));
void(*originalDealloc)(__unsafe_unretainedid,SEL)=(__typeof__(originalDealloc))method_getImplementation(deallocMethod);
originalDealloc(assignSlf,NSSelectorFromString(@”dealloc”));
做的工作便是,拿出ORIGdealloc的IMP,也便是原OC上的dealloc完成,然后调用它时selectorName传入dealloc,这样ARC就能认得这个办法是dealloc,做相应处理了。
扩展
JPCleaner即时回退
有些JSPatch运用者有这样的需求:脚本履行后期望能够回退到没有替换的状况。之前我的主张运用者自己操控下次启动时不要履行,就算回退了,但还是有不重启APP即时回退的需求。但这个需求并不是核心功用,所以想办法把它抽离,放到扩展里了。
只需引入JPCleaner.h,调用+cleanAll接口就能够把当时一切被JSPatch替换的办法恢复原样。别的还有+cleanClass:接口支撑只回退某个类。这些接口能够在OC调用,也能够在JS脚本动态调用:
[JPCleanercleanAll][JPCleanercleanClass:@“JPViewController”];
完成原理也很简略,在JSPatch核心里一切替换的办法都会保存在内部一个静态变量_JSOverideMethods里,它的结构是_JSOverideMethods[cls][selectorName]=jsFunction。我给JPExtension增加了个接口,把这个静态变量暴露给外部,遍历这个变量里保存的class和selectorName,把selector对应的IMP重新指向原生IMP就能够了。详见源码。
JPLoader
JSPatch脚本需求后台下发,客户端需求一套打包下载/履行的流程,还需求考虑传输进程中安全问题,JPLoader便是帮你做了这些工作。
下载履行脚本很简略,这儿主要做的事是确保传输进程的安全,JPLoader包含了一个打包东西packer.php,用这个东西对脚本文件进行打包,得出打包文件的MD5,再对这个MD5值用私钥进行RSA加密,把加密后的数据跟脚本文件一起大包发给客户端。JPLoader里的程序对这个加密数据用私钥进行解密,再计算一遍下发的脚本文件MD5值,看解密出来的值跟这边计算出来的值是否共同,共同阐明脚本文件从服务器到客户端之间没被第三方篡改正,确保脚本的安全。对这一进程的具体描绘详见旧文JSPatch布置安全策略。对JPLoader的运用办法能够参照wiki文档
jspatch原理
本着追本溯源的思维(以及面试官的要求……),结合JSPatch作者对JSPatch原理的解释,说一说个人对JSPatch原理的了解。
1.根据苹果的JavascriptCore结构
JavascriptCore结构是完成JS和OC相互交互的结构,运用这个结构,你能够在OC里面调用JS代码,也能够在JS中调用OC的代码。这个结构是JSPatch完成的根底。关于JavascriptCore结构,能够检查这篇文章。
2.根底原理是OC的动态言语特性
运用JSPatch来进行热修正,在很多情况下我们都是经过替换办法来完成的,在OC中,利用runtime能够很容易做到这一点。
3.完成办法调用
当运用require办法引进一个类的时分,JSPatch实际做的是在大局效果域的某个大局目标里,创立一个目标:
{__clsName:”UIView”}
当运用这个类,调用类办法的时分,就会去大局目标里取这个目标。现在这个目标还没有办法,而在JS中调用某个目标没有的办法会抛出反常。这儿JSPatch作者的解决办法是给Object目标界说一个__c()办法,而在OC那儿,JSEngine实际运用JavascriptCore结构履行JS代码之前,将一切的办法调用改为调用__c()办法,这个办法内部做的就是将办法名参数等信息传到OC,去OC里经过runtime履行,这样一来就不会溃散了。
除了这个问题以外,JSPatch还对OC回传的目标做了包装,经过_obj这个key把他包装:
{__obj:[OCObject目标指针]}
经过判别目标是否有__obj特点得知这个目标是否表明OC目标指针,在__c函数里若判别到调用者有__obj特点,取出这个特点,跟调用的实例办法一同传回给OC,就完成了实例办法的调用。
4.办法替换
JSPatch内部的办法替换是经过动态办法解析完成的,用的是forwardInvocation办法,因为能够从NSInvocation里面取得原办法的一切参数(经过va_list取得类结构体的办法列表获取参数在arm64下不可用)。
JSPatch是在应用的didFinishLaunchWithOptions里履行的,所以办法替换也是在这儿完成,它会将原办法的IMP指向_objc_msgForward(这个办法会履行forwardInvocation),新添加一个办法指向原办法的IMP。当原办法被调用的时分,会直接走forwardInvocation,在这儿能够直接调用JS中的替换办法,至于JS的办法调用看上一部分就行了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注