本文针对JSPatch在近期的不断完善和完善,对JSPatch实现原理详解(一)进行补充。SpecialStruct先说_objc_msgForward。上一篇文章提到为了让替换方法使用forwardInvocation,将其指向一个不存在的IMP:class_getMethodImplementation(cls,@selector(__JPNONImplementSelector)),其实这个实现是多余的,如果class_getMethodImplementation不能找到class/selector对应的IMP,会返回IMP_objc_msgForward,所以更直接的方式是把所有要替换的方法都指向_objc_msgForward,省去找方法的时间。那么还有一个问题,如果替换方法的返回值是一些struct,使用_objc_msgForward(或者之前的@selector(__JPNONImplementSelector))会崩溃。几经周折,我找到了解决办法:对于某些结构和某些结构,必须使用_objc_msgForward_stret而不是_objc_msgForward。为什么要使用_objc_msgForward_stret?我发现一篇文章解释了objc_msgSend_stret和objc_msgSend之间的区别)。很明显,原理是一样的。这是C的一些底层机制的原因,我简单回顾一下:当大多数CPU执行C函数时,前几个参数会被放入寄存器。对于obj_msgSend,前两个参数固定为self/_cmd,它们会放在寄存器中,执行完***后返回值也会保存在寄存器中。取这个寄存器的值就是返回值:-(int)method:(id)arg;r3=selfr4=_cmd,@selector(method:)r5=arg(onexit)r3=returnedint普通返回值(int/pointer)很小,放在寄存器里没问题,但是有些struct太大,寄存器放不下,所以还有一种方法就是一开始就申请一块内存,把指针保存在寄存器里,并将值返回到指针指向的内存中写入数据,所以寄存器需要为这个指针腾出空间,self/_cmd在寄存器中的位置会发生变化:-(structst)method:(id)arg;r3=&struct_var(incaller'sstackframe)r4=selfr5=_cmd,@selector(method:)r6=arg(onexit)returnvaluewrittenintostruct_varobjc_msgSend不知道self/_cmd的位置变了,所以应该改用另一个方法objc_msgSend_stret。原理大概是这样的。都说某些架构、某些结构有问题,那到底是什么问题呢?iOS架构下non-arm64有这个问题,但是对于什么样的struct需要经过上面的流程,使用xxx_stret代替原来的方法,并没有明确的规定,OC也没有提供接口,只揭秘这个在一个奇妙的界面上,于是就有了这么神奇的判断:if([methodSignature.debugDescriptionrangeOfString:@"isspecialstructreturn?YES"].location!=NSNotFound)NSMethodSignature的debugDescription上是否显示特殊struct只能通过这个字符串来判断.所以最后的处理是,在non-arm64下,如果是特殊的struct,就去_objc_msgForward_stret,否则就去_objc_msgForward。内存泄漏先说下上篇文章遗留下来的一个问题。获取NSInvocation的返回值时,为什么在取参数的时候会崩溃idarg;[invocationgetReturnValue:&arg];这是因为&arg传入-getArgument:atIndex:方法后,arg指向返回的对象,但是不持有,不会对返回的对象引用+1,但是因为idarg相当于ARC下的__strongidarg,而arg是强类型的局部变量,所以退出函数release时会调用一次domain关闭了,在没有hold住对象的情况下调用release,导致对象再次被release,从而crash。只要把arg类型改成__unsafe_unretained,表示只指向对象不持有,退出作用域时不释放就好了:__unsafe_unretainedidarg;[invocationgetReturnValue:&arg];也可以使用__bridge转换让局部变量持久化有一个返回对象,所以这样做是没有问题的:idreturnValue;void*result;[invocationgetReturnValue:&result];returnValue=(__bridgeid)result;doublerelease问题是解决了,但是这里还有一个大坑:内存泄漏。有一天,有人在githubissue上提到对象生成后没有释放。几经排查,发现还是NSInvocationgetReturnValue的问题出在这里。当NSInvocation调用alloc时,返回的对象不会被释放,造成内存泄漏。只有返回的对象才交出内存管理权,外部对象可以为其释放:idreturnValue;void*result;[invocationgetReturnValue:&result];if([selectorNameisEqualToString:@"alloc"]||[selectorNameisEqualToString:@"new"]){returnValue=(__bridge_transferid)result;}else{returnValue=(__bridge_id)result;}我不明白为什么,从开源的Cocotron中NSInvocation的实现来看,NSInvocation并没有持有返回的对象,而且调用方式也是直接使用msgSend也没有什么特别之处,让人怀疑这是ARC的bug。'_'JSPatch的处理使用下划线'_'连接OC方法的多个参数之间的区间:-(void)setObject:(id)anObjectforKey:(id)aKey;<==>setObject_forKey()那么如果OC方法名中包含'_',则有歧义:-(void)set_object:(id)anObjectforKey:(id)aKey;<==>set_object_forKey()无法知道set_object_forKey对应的selector是否为set_object:forKey:或设置:对象:forKey:。需要为此设置一个规则,在JS中用其他字符代替OC方法名中的_。JS的命名规则除了字母和数字,只有$和_,貌似用$代替了,但是效果很难看:-(void)set_object:(id)anObjectforKey:(id)aKey;-(void)_privateMethod();<==>set$object_forKey()$privateMethod()所以尝试另一种方法,使用两个下划线__代替:set__object_forKey()__privateMethod()但是用两个下划线代替有问题,OC方法名参数添加一个末尾的下划线不会匹配-(void)setObject_:(id)anObjectforKey:(id)aKey;<==>setObject___forKey()其实setObject___forKey()匹配的对应选择器是setObject:_forKey:。虽然有这个坑,但是因为很少看到这么奇葩的命名方式,所以感觉问题不大。使用$也会导致OC方法名称包含$字符。代码值最后用双下划线.__表示。#p#JPBoxing在使用JSPatch的过程中发现,JS无法调用NSMutableArray/NSMutableDictionary/NSMutableString的方法来修改这些对象的数据,因为JavaScriptCore在从OC返回到JS/String的时候,将它们转换成了JS的Array/Object,当它被返回,它将脱离与原始对象的连接。这种转换在JavaScriptCore中是强制性的,不能选择。对象返回JS后返回OC后如果想调用此对象的方法,必须阻止JavaScriptCore的转换。唯一的办法就是不直接返回这个对象,而是封装这个对象。JPBoxing执行以下操作:@interfaceJPBoxing:NSObject@property(nonatomic)idobj;@end@implementationJPBoxing+(instancetype)boxObj:(id)obj{JPBoxing*boxing=[[JPBoxingalloc]init];boxing.obj=obj;returnboxing;}把NSMutableArray/NSMutableDictionary/NSMutableString对象作为JPBoxing的成员保存在JPBoxing实例对象上,返回给JS。JS拿到JPBoxing对象的指针,返回给OC时,通过对象成员就可以得到原来的NSMutableArray/NSMutableDictionary/NSMutableString对象。类似于装箱/拆箱操作,这可以防止这些对象被JavaScriptCore转换。其实只有变量NSMutableArray/NSMutableDictionary/NSMutableString这三个类需要调用它们的方法来修改对象中的数据。对于不可变的NSArray/NSDictionary/NSString就不用这么干了,直接将它们转换成对应的JS类型使用会更方便,但是JSPatch为了规则简单,允许NSArray/NSDictionary/NSString是以封装的方式返回,避免调用OC方法返回对象时需要关心返回的是可变对象还是不可变对象。***整个规则非常清楚:NSArray/NSDictionary/NSString及其子类的行为与其他NSObject对象相同。你在JS上得到的只是它的对象指针,你可以调用它们的OC方法。如果你想使用这三个来将一个对象转换成对应的JS类型,使用额外的.toJS()接口来转换。JPBoxing也封装了对C指针和Class类型的参数和返回值的支持。指针和类作为成员存储在JPBoxing对象上并返回给JS,使得JSPatch支持OC<->JS所有数据类型的互传。nil的处理区分NSNull/nil为“空”,JS有null/undefined,OC有nil/NSNull,JavaScriptCore是这样处理这些参数传递的:从JS到OC,直接传递null/undefined给OC,传递的就是nil,如果一个包含null/undefined的Array传给OC,它会被转换为NSNull。从OC到JS,nil会转为null,NSNull像普通的NSObject一样返回一个指针。在JSPatch的过程中,参数是通过数组从JS传给OC的,这样所有传给OC的null/undefined都会变成NSNull,而真正的NSNull对象也是NSNull,与JS无法区分。问题是,需要有某种方法来区分两者。我考虑过在JS中用一个特殊的对象来表示nil,而null/undefined只是用来表示NSNull。后来觉得NSNull是一个很少手动传递的变量,而null/undefined和OCnil很常见。带来极大的不便。所以反过来,在JS中用一个特殊的变量nsnull来表示NSNull,其他的null/undefined来表示nil,这样传入OC就可以区分nil和NSNull。具体使用方法:@implementationJPObject+(void)testNil:(id)obj{NSLog(@"%@",obj);}@endrequire("JPObject").testNil(null)//output:nilrequire("JPObject").testNil(nsnull)//output:NSNull这种方式有一个小坑,就是说明NSNull.null()作为参数调用时,到达OC后会变成nil:require("JPObject").testNil(require("NSNull").null())//output:nil只需要注意这个就用nsnull代替,OC返回的NSNull在传回来的时候还是可以识别的。链式调用的第二个问题是nil在JS中用null/undefined表示。导致无法用nil调用方法,链式调用的安全性无法保证:@implementationJPObject+(void)returnNil{returnnil;}@end[[JPObjectreturnNil]hash]//it'sOKrequire("JPObject")。returnNil().hash()//crash的原因是null/undefined在JS中不是对象,不能调用任何方法,包括我们给所有对象添加的__c()方法。解决方案曾经觉得解决这个问题的唯一方法就是回到上面,用一个特殊的对象来表示nil。但是用一个特殊的对象来表示nil,结果就是js判断是否为nil的时候会很啰嗦://假设用一个_nil对象变量来表示OC()返回的nil//没有特殊处理后的问题if(!obj||obj==_nil){//判断对象是否为nil,还要额外判断是否等于_nil}这种用法不能接受,继续寻找解决方案,发现true/false在JS中是一个对象,可以调用方法。如果用false表示nil,可以调用方法,可以直接通过if(!obj)判断是否为nil,所以沿着这个方向,解决使用False的问题意味着nil带来的各种坑差不多彻底解决这个问题。具体实现细节我就不多说了。说“差不多***”是因为还有一个小坑,给OC上参数类型为NSNumber*的方法传false,OC会得到nil而不是NSNumber对象:@implementationJPObject+(void)passNSNumber:(NSNumber*)num{NSLog(@"%@",num);}@endrequire("JPObject").passNSNumber(false)//输出:如果OC方法的参数类型为BOOL,则为nil,否则为true传进来的/0就可以了,这个小坑无伤大雅。顺便说一句,神奇JS中false的this不再是原来的false,而是另一个布尔对象,太特殊了:Object.prototype.c=function(){console.log(this===false)};false.c()//outputfalse#p#新添加的方法将OC中未定义的方法添加到JS中的类中。实现上有两个变化:1.原来的方法流程是将新添加的方法指向一个静态的IMP,脱离了方法替换的流程。这样做的好处是调用new方法时不需要经过forwardInvocation的过程,提高了性能。但缺点是它不能遵循与方法替换相同的过程,需要额外的代码。二是参数。数量有限。由于不能通过va_list的可变参数来定义(详见上一篇文章),所以需要为每个参数号单独定义一个方法。原实现中定义了5个方法,所以add方法最多只能支持5个参数。权衡一下,为了去掉参数个数的限制,最好把forwardInvocation的过程改成和replacement方法一样。2.ProtocolJSPatch现在支持Protocol,所以在添加Protocol中定义的方法时,参数类型会按照Protocol中的定义来实现,Protocol的定义方式和OC上的写法一致:defineClass("JPViewController:UIViewController
