您好,欢迎访问三七文档
当前位置:首页 > 商业/管理/HR > 质量控制/管理 > JSPatch实现原理详解让JS调用替换任意OC方法
网址:edu.51CTO.comJSPatch实现原理详解:让JS调用/替换任意OC方法JSPatch以小巧的体积做到了让JS调用/替换任意OC方法,让iOSAPP具备热更新的能力,在实现JSPatch过程中遇到过很多困难也踩过很多坑,有些还是挺值得分享的。本篇文章从基础原理、方法调用和方法替换三块内容介绍整个JSPatch的实现原理,并把实现过程中的想法和碰到的坑也尽可能记录下来。基础原理能做到通过JS调用和改写OC方法最根本的原因是Objective-C是动态语言,OC上所有方法的调用/类的生成都通过Objective-CRuntime在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:1.Classclass=NSClassFromString(UIViewController);2.idviewController=[[classalloc]init];3.SELselector=NSSelectorFromString(viewDidLoad);4.[viewControllerperformSelector:selector];也可以替换某个类的方法为新的实现:1.staticvoidnewViewDidLoad(idslf,SELsel){}2.class_replaceMethod(class,selector,newViewDidLoad,@);还可以新注册一个类,为类添加方法:1.Classcls=objc_allocateClassPair(superCls,JPObject,0);2.objc_registerClassPair(cls);3.class_addMethod(cls,selector,implement,typedesc);对于Objective-C对象模型和动态消息发送的原理已有很多文章阐述得很详细,例如这篇,这里就不详细阐述了。理论上你可以在运行时通过类名/方法名调用到任何OC方法,替换任何类的实现以及新增任意类。所以JSPatch的原理就是:JS传递字符串给OC,OC通过Runtime接口调用和替换OC方法。这是最基础的原理,实际实现过程还有很多怪要打,接下来看看具体是怎样实现的。方法调用1.require('UIView')2.varview=UIView.alloc().init()3.view.setBackgroundColor(require('UIColor').grayColor())网址:edu.51CTO.com4.view.setAlpha(0.5)引入JSPatch后,可以通过以上JS代码创建了一个UIView实例,并设置背景颜色和透明度,涵盖了require引入类,JS调用接口,消息传递,对象持有和转换,参数转换这五个方面,接下来逐一看看具体实现。1.require调用require(‘UIView’)后,就可以直接使用UIView这个变量去调用相应的类方法了,require做的事很简单,就是在JS全局作用域上创建一个同名变量,变量指向一个对象,对象属性__isCls表明这是一个Class,__clsName保存类名,在调用方法时会用到这两个属性。1.var_require=function(clsName){2.if(!global[clsName]){3.global[clsName]={4.__isCls:1,5.__clsName:clsName6.}7.}8.returnglobal[clsName]9.}所以调用require(‘UIView’)后,就在全局作用域生成了UIView这个变量,指向一个这样一个对象:1.{2.__isCls:1,3.__clsName:UIView4.}2.JS接口接下来看看UIView.alloc()是怎样调用的。旧实现对于这个调用的实现,一开始我的想法是,根据JS特性,若要让UIView.alloc()这句调用不出错,唯一的方法就是给UIView这个对象添加alloc方法,不然是不可能调用成功的,JS对于调用没定义的属性/变量,只会马上抛出异常,而不像OC/Lua/ruby那样会有转发机制。所以做了一个复杂的事,就是在require生成类对象时,把类名传入OC,OC网址:edu.51CTO.com通过Runtime方法找出这个类所有的方法返回给JS,JS类对象为每个方法名都生成一个函数,函数内容就是拿着方法名去OC调用相应方法。生成的UIView对象大致是这样的:1.{2.__isCls:1,3.__clsName:UIView,4.alloc:function(){…},5.beginAnimations_context:function(){…},6.setAnimationsEnabled:function(){…},7....8.}实际上不仅要遍历当前类的所有方法,还要循环找父类的方法直到顶层,整个继承链上的所有方法都要加到JS对象上,一个类就有几百个方法,这样把方法全部加到JS对象上,碰到了挺严重的问题,引入几个类就内存暴涨,无法使用。后来为了优化内存问题还在JS搞了继承关系,不把继承链上所有方法都添加到一个JS对象,避免像基类NSObject的几百个方法反复添加在每个JS对象上,每个方法只存在一份,JS对象复制了OC对象的继承关系,找方法时沿着继承链往上找,结果内存消耗是小了一些,但还是大到难以接受。新实现当时继续苦苦寻找解决方案,若按JS语法,这是唯一的方法,但若不按JS语法呢?突然脑洞开了下,CoffieScript/JSX都可以用JS实现一个解释器实现自己的语法,我也可以通过类似的方式做到,再进一步想到其实我想要的效果很简单,就是调用一个不存在方法时,能转发到一个指定函数去执行,就能解决一切问题了,这其实可以用简单的字符串替换,把JS脚本里的方法调用都替换掉。最后的解决方案是,在OC执行JS脚本前,通过正则把所有方法调用都改成调用__c()函数,再执行这个JS脚本,做到了类似OC/Lua/Ruby等的消息转发机制:1.UIView.alloc().init()2.-3.UIView.__c('alloc')().__c('init')()给JS对象基类Object的prototype加上__c成员,这样所有对象都可以调用到__c,根据当前对象类型判断进行不同操作:1.Object.prototype.__c=function(methodName){网址:edu.51CTO.com2.if(!this.__obj&&!this.__clsName)returnthis[methodName].bind(this);3.varself=this4.returnfunction(){5.varargs=Array.prototype.slice.call(arguments)6.return_methodFunc(self.__obj,self.__clsName,methodName,args,self.__isSuper)7.}8.}_methodFunc()就是把相关信息传给OC,OC用Runtime接口调用相应方法,返回结果值,这个调用就结束了。这样做不用去OC遍历对象方法,不用在JS对象保存这些方法,内存消耗直降99%,这一步是做这个项目最爽的时候,用一个非常简单的方法解决了严重的问题,替换之前又复杂效果又差的实现。3.消息传递解决了JS接口问题,接下来看看JS和OC是怎样互传消息的。这里用到了JavaScriptCore的接口,OC端在启动JSPatch引擎时会创建一个JSContext实例,JSContext是JS代码的执行环境,可以给JSContext添加方法,JS就可以直接调用这个方法:1.JSContext*context=[[JSContextalloc]init];2.context[@hello]=^(NSString*msg){3.NSLog(@hello%@,msg);4.};5.[_contextevaluateScript:@hello('word')];//outputhelloword6.JS通过调用JSContext定义的方法把数据传给OC,OC通过返回值传会给JS。调用这种方法,它的参数/返回值JavaScriptCore都会自动转换,OC里的NSArray,NSDictionary,NSString,NSNumber,NSBlock会分别转为JS端的数组/对象/字符串/数字/函数类型。上述_methodFunc()方法就是这样把要调用的类名和方法名传递给OC的。4.对象持有/转换网址:edu.51CTO.comUIView.alloc()通过上述消息传递后会到OC执行[UIViewalloc],并返回一个UIView实例对象给JS,这个OC实例对象在JS是怎样表示的呢?怎样可以在JS拿到这个实例对象后可以直接调用它的实例方法(UIView.alloc().init())?对于一个自定义id对象,JavaScriptCore会把这个自定义对象的指针传给JS,这个对象在JS无法使用,但在回传给OC时OC可以找到这个对象。对于这个对象生命周期的管理,按我的理解如果JS有变量引用时,这个OC对象引用计数就加1,JS变量的引用释放了就减1,如果OC上没别的持有者,这个OC对象的生命周期就跟着JS走了,会在JS进行垃圾回收时释放。传回给JS的变量是这个OC对象的指针,如果不经过任何处理,是无法通过这个变量去调用实例方法的。所以在返回对象时,JSPatch会对这个对象进行封装。首先,告诉JS这是一个OC对象:1.staticNSDictionary*toJSObj(idobj)2.{3.if(!obj)returnnil;4.return@{@__isObj:@(YES),@cls:NSStringFromClass([objclass]),@obj:obj};5.}用__isObj表示这是一个OC对象,对象指针也一起返回。接着在JS端会把这个对象转为一个JSClass实例:1.varJSClass2.var_getJSClass=function(className){3.if(!JSClass){4.JSClass=function(obj,className,isSuper){5.this.__obj=obj6.this.__isSuper=isSuper7.this.__clsName=className8.}9.}10.returnJSClass11.}12.var_toJSObj=function(meta){varJSClass=_getJSClass()returnnewJSClass(meta[obj],meta[cls])}网址:edu.51CTO.comJS端如果发现返回是一个OC对象,会传入_toJSObj(),生成一个JSClass实例,这个实例保存着OC对象指针,类名等。这个实例就是OC对象在JSPatch对应的JS对象,生命周期是一样的。回到我们第二点说的JS接口,这个JSClass实例对象同样有__c函数,调用这个对象的方法时,同样走到__c函数,__c函数会把JSClass实例对象里的OC对象指针以及要调用的方法名和参数回传给OC,这样OC就可以调用这个对象的实例方法了。接着看看对象是怎样回传给OC的。上述例子中,view.setBackgroundColor(require(‘UIColor’).grayColor()),这里生成了一个UIColor实例对象,并作为参数回传给OC。根据上面说的,这个UIColor实例在JS中的表示是一个JSClass实例,所以不能直接回传给OC,这里的参数实际上会在__c函数进行处理,会把对象的.__obj原指针回传给OC。最后一点,OC对象可能会存在于NSDictionary/NSArray等容器里,所以需要遍历容器挑出OC对象进行格式化,OC需要把对象都替换成JS认得
本文标题:JSPatch实现原理详解让JS调用替换任意OC方法
链接地址:https://www.777doc.com/doc-2882164 .html