美文网首页
IOS源码解析:JSPatch

IOS源码解析:JSPatch

作者: 时光啊混蛋_97boy | 来源:发表于2021-06-06 12:27 被阅读0次

    原创:知识探索型文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、HotFix 方案介绍
      • 1、Dynamic Framework
      • 2、React Native
      • 3、JSPatch
    • 二、JSPatch 简介
      • 1、实际用途
      • 2、使用方式
      • 3、修复举例
      • 4、基本原理
      • 5、方案对比
      • 6、致命缺陷
    • 三、JSPatch的核心原理
      • 1、预加载部分
      • 2、脚本运行
      • 3、require
      • 4、defineClass
      • 5、对象持有/转换

    一、HotFix 方案介绍

    相信HotFix大家应该都很熟悉了,今天主要对于最近调研的一些方案做一些总结。iOS中的HotFix方案大致可以分为三种:Dynamic Framework(Apple)React Native(Facebook)JSPatch(Tencent)

    1、Dynamic Framework

    动态的Framework,其实就是动态库。首先我介绍一下关于动态库和静态库的一些特性以及区别。不管是静态库还是动态库,本质上都是一种可执行的二进制格式,可以被载入内存中执行。iOS上的静态库可以分为.a文件和.framework,动态库可以分为.dylib(.tdb)和.framework

    静态库链接时可以完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。动态库在链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

    静态库和动态库是相对编译期和运行期的。静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要修改静态库,而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

    总而言之,同一个静态库在不同程序中使用时,每一个程序中都得导入一次,打包时也被打包进去,形成一个程序。而动态库在不同程序中,打包时并没有被打包进去,只在程序运行使用时,才链接载入(如系统的框架如UIKitFoundation等),所以程序体积会小很多。


    2、React Native

    React Native支持用JavaScript进行开发,所以可以通过更改JS文件实现App的HotFix,但是这种方案的明显的缺点在于它只适合用于使用了React Native这种方案的应用。


    3、JSPatch

    JSPatch是只需要在项目中引入极小的JSPatch引擎,就可以使用JavaScript语言调用Objective-C的原生接口,获得脚本语言的能力:动态更新iOS APP,替换项目原生代码、快速修复bug


    二、JSPatch 简介

    JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug

    1、实际用途

    由于Apple严格的审核标准和低效率,iOS应用的发版速度极慢,稍微大型的app发版基本上都在一个月以上,所以代码热更新(HotfixPatch)对于iOS应用来说就显得尤其重要。

    新版本上线后发现有个严重的bug,可能会导致crash率激增,可能会使网络请求无法发出,这时能做的只是赶紧修复bug然后提交等待漫长的AppStore审核,再盼望用户快点升级,才能完成此次bug的修复。

    使用JSPatch可以解决这样的问题,只需在项目中引入JSPatch,就可以在发现bug时下发JS脚本补丁,替换原生方法,无需更新APP即时修复bug


    2、使用方式

    安装框架:

    pod 'JSPatch'
    

    OC代码:

    @implementation AppDelegate
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
    {
        [JPEngine startEngine];
        NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
        NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
        [JPEngine evaluateScript:script];
        
        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        [self.window addSubview:[self genView]];
        [self.window makeKeyAndVisible];
        
        return YES;
    }
    
    - (UIView *)genView
    {
        return [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 320)];
    }
    
    @end
    

    demo.js文件中相应部分代码如下:

    require('UIView, UIColor, UILabel')
    defineClass('AppDelegate', {
      // replace the -genView method
      genView: function() {
        var view = self.ORIGgenView();
        view.setBackgroundColor(UIColor.greenColor())
        var label = UILabel.alloc().initWithFrame(view.frame());
        label.setText("JSPatch");
        label.setTextAlignment(1);
        view.addSubview(label);
        return view;
      }
    });
    

    3、修复举例

    以下代码中取数组元素处可能会超出数组范围导致crash

    (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
        NSString *content = self.dataSource[[indexPath row]]; //可能会超出数组范围导致crash
        JPViewController *ctrl = [[JPViewController alloc] initWithContent:content];
        [self.navigationController pushViewController:ctrl];
    }
    

    如果在项目里引用了JSPatch

    #import "AppDelegate.h"
    #import "JPEngine.h"
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [JPEngine startEngine];
        NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
        NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
        [JPEngine evaluateScript:script];
    .......
    }
    @end
    

    通过JS脚本修复这个bug

    //JS
    defineClass("JPTableViewController", {
        //instance method definitions
        tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
            var row = indexPath.row()
            if (self.dataSource().length > row) { //加上判断越界的逻辑
                var content = self.dataArr()[row];
                var ctrl = JPViewController.alloc().initWithContent(content);
                self.navigationController().pushViewController(ctrl);
            }
        }
    }, {})
    

    4、基本原理

    JSPatch用iOS内置的JavaScriptCore.framework作为JS引擎,但没有用它JSExport的特性进行JS-OC函数互调,而是通过Objective-C Runtime,从JS传递要调用的类名和函数名到Objective-C,再使用NSInvocation动态调用对应的OC方法。

    JSPatch 能做到通过 JS 调用和改写 OC 方法最根本的原因是 Objective-C 是动态语言,OC 上所有方法的调用/类的生成都通过 Objective-C 的Runtime在运行时进行。

    我们可以通过类名/方法名反射得到相应的类和方法:

    Class class = NSClassFromString("UIViewController");
    id viewController = [[class alloc] init];
    SEL selector = NSSelectorFromString("viewDidLoad");
    [viewController performSelector:selector];
    

    也可以替换某个类的方法为新的实现:

    static void newViewDidLoad(id slf, SEL sel) {}
    class_replaceMethod(class, selector, newViewDidLoad, @"");
    

    还可以新注册一个类,为类添加方法:

    Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
    objc_registerClassPair(cls);
    class_addMethod(cls, selector, implement, typedesc);
    

    理论上你可以在运行时通过类名/方法名调用到任何 OC 方法,替换任何类的实现以及新增任意类。所以 JSPatch 的基本原理就是:JS 传递字符串给 OC,OC 通过 Runtime 接口调用和替换 OC 方法。这是最基础的原理,实际实现过程还有很多怪要打。


    5、方案对比

    方案一:Dynamic Framework

    不管是静态库还是动态库,本质上都是一种可执行的二进制格式,可以被载入内存中执行。

    静态库:

    • 链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
    • 在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库。
    • .a.framework为文件后缀名。

    动态库:

    • 在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
    • 系统只加载一次,多个程序共用,节省内存。
    • .tbd(之前叫.dylib) 和.framework为文件后缀名。
    • 系统直接提供给我们的framework都是动态库。

    静态库和动态库是相对编译期和运行期而言的,同一个静态库在不同程序中使用时,每一个程序中都得导入一次,打包时也被打包进去,形成一个程序。而动态库在不同程序中,打包时并没有被打包进去,只在程序运行使用时,才链接载入(如系统的框架如UIKitFoundation等),所以程序体积会小很多。

    Dynamic Framework其实就是我们可以通过更新App所依赖的Framework方式,来实现对于BugHotFix,但是这个方案的缺点也是显而易见的它不符合Apple3.2.2的审核规则,使用了这种方式是上不了Apple Store的,它只能适用于公司内部的一些项目使用,同时这种方案其实并不适用于BugFix,更适合App线上的大更新,因为其实我们项目中的引入的那些第三方的Framework都是静态库。我们可以通过file这个命令来查看我们的framework到底是属于static还是dynamic

    方案二:React Native

    React Native支持用JavaScript进行开发,所以可以通过更改JS文件实现App的HotFix,但是这种方案的明显的缺点在于它只适合用于使用了React Native这种方案的应用。

    方案三:JSPatch

    已经有一些方案可以实现动态打补丁,JSPatch的优势是:

    • JS语言:目前前端开发和终端开发有融合的趋势,作为扩展的脚本语言,JS在应用开发领域有更广泛的应用。
    • 符合Apple规则:JSPatch更符合Apple的规则。iOS Developer Program License Agreement里3.3.2提到不可动态下发可执行代码,但通过苹果JavaScriptCore.frameworkWebKit执行的代码除外,JS正是通过JavaScriptCore.framework执行的。
    • 小巧:使用系统内置的JavaScriptCore.framework,无需内嵌脚本引擎,体积小巧。
    • 风险:JSPatch让脚本语言获得调用所有原生OC方法的能力,不像web前端把能力局限在浏览器,使用上会有一些安全风险:若在网络传输过程中下发明文JS,可能会被中间人篡改JS脚本,执行任意方法,盗取APP里的相关信息。当然,这可以对传输过程进行加密,或用直接使用https解决。

    6、致命缺陷

    理论上还可以完全用JSPatch实现一个业务模块,甚至整个APP,但不推荐这么做。

    • JSPatch是通过Objective-C Runtime的接口通过字符串反射找到对应的类和方法进行调用,这中间的字符串处理会损耗一定的性能,另外两种语言间的类型转换也有性能损耗,若用来做一个完整的业务模块,大量的频繁来回互调,可能有性能问题。
    • 开发过程中需要用OC的思维写JS,丧失了脚本语言自己的特性。
    • JSPatch没有IDE支持,开发效率低。

    若想动态为APP添加模块,目前React Native \ Flutter等给出了很好的方案,解决了上述三个问题。

    • JS/OC不会频繁通信,会在事件触发时批量传递,提高效率。
    • 开发过程无需考虑OC的感受,遵从React框架的思想进行纯JS开发就行,剩下的事情React Native帮你处理好了。
    • React Native连IDE都准备好了。

    React Native并不会提供原生OC接口的反射调用和方法替换,无法做到修改原生代码,JSPatch以小巧的引擎补足这个缺口,配合React Native用统一的JS语言让一个原生APP时刻处于可扩展可修改的状态。

    虽说2017年被苹果封杀了,但是大家还是偷偷摸摸混淆一下、改改类名继续在使用。毕竟bug还是不可避免的,有了JSPatch万一出了问题我们还是能够抢救一下的。


    三、JSPatch的核心原理

    1、预加载部分

    关于核心原理的讲解,网上有不少,但是几乎都是差不多,很多都还是引用了作者bang自己写的文档的内容,所以我采用一个例子方式进行讲解JSPatch的主要运行流程,其实当然也会引用一些作者的简述,大家可以参照我写的流程讲述,在配合源码或者官方文档的介绍,应该就可以了解JSPatch

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // 环境的初始化
        [JPEngine startEngine];
        
        // js脚本的调用
        NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
        NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
        [JPEngine evaluateScript:script];
    
        ......
    }
    

    首先是运行[JPEngine startEngine]启动JSPatch,启动过程分为以下两部分。首先通过JSContext,声明了一些JS方法到内存,这样我们之后就可以在JS中调用这些方法,主要常用到的包括以下几个方法,同时会监听一个内存告警的通知。

    + (void)startEngine
    {
        JSContext *context = [[JSContext alloc] init];
    
        context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
            return defineClass(classDeclaration, instanceMethods, classMethods);
        };
        
        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);
        };
        
        context[@"_OC_formatJSToOC"] = ^id(JSValue *obj) {
            return formatJSToOC(obj);
        };
        
        context[@"_OC_formatOCToJS"] = ^id(JSValue *obj) {
            return formatOCToJS([obj toObject]);
        };
        
        context[@"_OC_getCustomProps"] = ^id(JSValue *obj) {
            id realObj = formatJSToOC(obj);
            return objc_getAssociatedObject(realObj, kPropAssociatedObjectKey);
        };
        
        context[@"_OC_setCustomProps"] = ^(JSValue *obj, JSValue *val) {
            id realObj = formatJSToOC(obj);
            objc_setAssociatedObject(realObj, kPropAssociatedObjectKey, val, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        };
        
        .....
    }
    

    其次加载JSPatch.js文件。JSPatch文件的主要内容在于定义一些我们之后会用在的JS函数,数据结构以及变量等信息,之后我会在用到的时候详细介绍。


    2、脚本运行

    我们定义如下的脚本。

    require('UIAlertView')
    defineClass('AppDelegate',['name', 'age', 'temperatureDatas'],
     {
       testFuncationOne: function(index) {
                self.setName('xiejiapei')
                self.setAge(24)
                self.setTemperatureDatas(new Array(37.10, 36.78, 36.56))
                var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(
                        "title", self.name(), self, "OK", null)
                alertView.show()
       }
      },
      {
        testFuncationTwo: function(datas) {
        var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(
                            "title", "DiDiChuXing", self, "OK", null)
                    alertView.show()
        }
      });
    

    然后就是执行我们上面说的[JPEngine evaluateScript:script]了,程序开始执行我们的脚本,但是在这之前,JSpatch会对我们的脚本做一些处理,这一步同样也包括两个方面。首先需要给我们的程序加上try catch的部分代码,主要目的是当我们的JS脚本有错误的时候,可以catch到错误信息。其次将所有的函数都改成通过__c原函数的形式进行调用,也就是最后我们调用的脚本已经变成如下的形式了。

    (function(){try{require('UIAlertView')
    defineClass('AppDelegate',['name', 'age', 'temperatureDatas'],
     {
       testFuncationOne: function(index) {
                self.__c("setName")('xiejiapei')
                self.__c("setAge")(24)
                self.__c("setTemperatureDatas")(new Array(37.10, 36.78, 36.56))
                var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")(
                        "title", self.__c("name")(), self, "OK", null)
                alertView.__c("show")()
       }
      },
      {
                testFuncationTwo: function(datas) {
                    var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")(
                            "title", "didichuxing", self, "OK", null)
                    alertView.__c("show")()
                }
      });
    

    那么为什么需要用函数__c来替换我们的函数呢,因为JS语法的限制。对于没有定义的函数JS是无法调用的,也就是调用UIAlertView.alloc()其实是非法的,因为它采用的并不是消息转发的形式,所以作者原来是想把一个类的所有函数都定义在JS上,也就是如下形式。

    {
        __clsName: "UIAlertView",
        alloc: function() {…},
        beginAnimations_context: function() {…},
        setAnimationsEnabled: function(){…},
        ...
    }
    

    但是这种形式就必须要遍历当前类的所有方法,还要循环找父类的方法直到顶层,这种方法直接导致的问题就是内存暴涨,所以是不可行的,所以最后作者采用了消息转发的思想,定义了一个_c的原函数,所有的函数都通过_c来转发,这样就解决了我们的问题。

    值得一提的是,我们的__c函数就是在我们执行JSPatch.js的时候声明到js里的Object方法里面去的,也就是下面这个函数。_customMethods里面声明了很多需要追加到Object上的函数。

    ;(function() {
      ......
      // 给 JS 对象基类 Object 的 prototype 加上 __c 成员
      for (var method in _customMethods) {
        if (_customMethods.hasOwnProperty(method)) {
          Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})
        }
      }
      ......
    }
    

    3、require

    调用 require('UIAlertView') 后,就可以直接使用UIAlertView这个变量去调用相应的类方法了,require做的事很简单,就是在JS全局作用域上创建一个同名变量,变量指向一个对象,对象属性 __clsName 保存类名,同时表明这个对象是一个 OC Class

    var _require = function(clsName) {
      if (!global[clsName]) {
        // 在JS全局作用域上创建一个同名变量
        global[clsName] = {
          // 变量指向一个对象,对象属性__isCls表明这是一个OC Class
          __isCls: 1,
          // __clsName保存类名,在调用方法时会用到这两个属性
          __clsName: clsName
        }
      }
      return global[clsName]
    }
    

    这样我们在接下来调用UIAlertView.__c()方法的时候系统就不会报错了,因为它已经是JS中一个全局的Object对象了。

    {
      __clsName: "UIAlertView"
    }
    

    4、defineClass

    接下来我们就要执行defineClass函数了。defineClass函数可接受四个参数。第一个参数是字符串,表示需要替换或者新增的类名:继承的父类名 <实现的协议1,实现的协议2>。

    // [属性]、{实例方法}、{类方法}
    global.defineClass = function(declaration, properties, instMethods, clsMethods) 
    

    当我们调用这个函数以后主要是做三件事情。首先执行_formatDefineMethods方法,主要目的是修改传入的function函数的的格式,以及在原来实现上追加了从OC回调回来的参数解析。然后执行_OC_defineClass方法,也就是调用OC的方法,解析传入类的属性、实例方法、类方法,里面会调用overrideMethod方法,进行method swizzing操作,也就是方法的重定向。最后执行_setupJSMethod方法,在js中通过_ocCls记录类实例方法、类方法。

    a、_formatDefineMethods

    可以发现,具体实现是遍历方法列表对象的属性(方法名),然后往js空对象中添加相同的属性,它的值对应的是一个数组,数组的第一个值是方法名对应实现函数的参数个数,第二个值是一个函数(也就是方法的具体实现)。

    // 参数一:一个方法列表js对象
    // 参数二:一个新的js空对象
    var _formatDefineMethods = function(methods, newMethods, realClsName) {
      for (var methodName in methods) {
        if (!(methods[methodName] instanceof Function)) return;
        (function(){
          var originMethod = methods[methodName]
          newMethods[methodName] = [originMethod.length, function() {
            try {
              // 通过OC回调回来执行,获取参数
              var args = _formatOCToJS(Array.prototype.slice.call(arguments))
              var lastSelf = global.self
              global.self = args[0]
              if (global.self) global.self.__realClsName = realClsName
              // 删除前两个参数:在OC中进行消息转发的时候,前两个参数是self和selector
              // 我们在实际调用js的具体实现的时候,需要把这两个参数删除
              args.splice(0,1)
              var ret = originMethod.apply(originMethod, args)
              global.self = lastSelf
              return ret
            } catch(e) {
              _OC_catch(e.message, e.stack)
            }
          }]
        })()
      }
    }
    

    _formatDefineMethods作用,简单的说,它把defineClass中传递过来的js对象进行了修改。

    原来的形式是:
    {
        testFuncationOne:function(){...}
    }
    修改之后是:
    {
        testFuncationOne: [argCount, function (){...新的实现}]
    }
    

    传递参数个数的目的是,runtime在修复类的时候,无法直接解析原始的js实现函数,那么就不知道参数的个数,特别是在创建新的方法的时候,需要根据参数个数生成方法签名,也就是还原方法名字,所以只能在js端拿到js函数的参数个数,传递到OC端。

    // js 方法
    initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles
    
    // oc 方法
    initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
    

    b、_OC_defineClass
    ❶ 使用NSScanner分离classDeclaration,分离成三部分
    • 类名:className
    • 父类名:superClassName
    • 实现的协议名:protocalNames
    ❷ 使用NSClassFromString(className)获得该Class对象

    若该Class对象为nil,则说明JS端要添加一个新的类,使用objc_allocateClassPairobjc_registerClassPair注册一个新的类。若该Class对象不为nil,则说明JS端要替换一个原本已存在的类。

    ❸ 根据从JS端传递来的实例方法与类方法参数,为这个类对象添加/替换实例方法与类方法

    添加实例方法时,直接使用上一步得到class对象。添加类方法时需要调用objc_getMetaClass方法获得元类。如果要替换的类已经定义了该方法,则直接对该方法替换和实现消息转发。

    否则根据以下两种情况进行判断。遍历protocalNames,通过objc_getProtocol方法获得协议对象,再使用protocol_copyMethodDescriptionList来获得协议中方法的typename。匹配JS中传入的selectorName,获得typeDescription字符串,对该协议方法的实现消息转发。

    若不是上述两种情况,则js端请求添加一个新的方法。构造一个typeDescription@@:\@*IMP,将这个IMP添加到类中。返回类型为id,参数值根据JS定义的参数个数来决定。新增方法的返回类型和参数类型只能为id类型,因为在JS端只能定义对象。

    ❹ 为该类添加setProp:forKey和getProp:方法

    使用objc_getAssociatedObjectobjc_setAssociatedObjectJS脚本拥有设置property的能力。

    ❺ 返回{className:cls}JS脚本

    不过其中还包括一个overrideMethod方法,不管是替换方法还是新增方法,都是使用overrideMethod方法。它的目的主要在于进行method swizzing操作,也就是方法的重定向。

    我们把所有的消息全部都转发到ForwardInvocation函数里去执行(不知道的同学请自行补消息转发机制),这样做的目的在于,我们可以在NSInvocation中获取到所有的参数,这样就可以实现一个通用的IMP,任意方法任意参数都可以通过这个IMP中转,拿到方法的所有参数回调JS的实现。于是overrideMethod其实就是做了如下这件事情。

    具体实现,以替换 UIViewController-viewWillAppear:方法为例。把UIViewController-viewWillAppear:方法通过class_replaceMethod()接口指向_objc_msgForward这个全局 IMP,OC 调用方法不存在时都会转发到这个IMP上,这里直接把方法替换成这个IMP,这样调用这个方法时就会走到-forwardInvocation:

    UIViewController添加-ORIGviewWillAppear:-_JPviewWillAppear:两个方法,前者指向原来的IMP实现,后者是新的实现,稍后会在这个实现里回调JS函数。

    改写UIViewController-forwardInvocation:方法为自定义实现。一旦OC里调用 UIViewController-viewWillAppear:方法,经过上面的处理会把这个调用转发到-forwardInvocation:,这时已经组装好了一个NSInvocation,包含了这个调用的参数。在这里把参数从 NSInvocation反解出来,带着参数调用上述新增加的方法-JPviewWillAppear:,在这个新方法里取到参数传给JS,调用JS的实现函数。


    c、_setupJSMethod

    最后的一步是把之前所有的方法以及属性放入 _ocCls中保存起来,最后再调用require把类保存到全局变量中。到这一步为止,我们的JS脚本中的所有对象已经通过runtime替换到我们的程序中去了,也就是说,剩下的就是如何在我们触发函数以后,能正确的去执行JS中函数的内容。

    var _setupJSMethod = function(className, methods, isInst, realClsName) {
      for (var name in methods) {
        var key = isInst ? 'instMethods': 'clsMethods',
            func = methods[name]
        _ocCls[className][key][name] = _wrapLocalMethod(name, func, realClsName)
      }
    }
    

    Demo

    Demo在我的Github上,欢迎下载。
    SourceCodeAnalysisDemo

    相关文章

      网友评论

          本文标题:IOS源码解析:JSPatch

          本文链接:https://www.haomeiwen.com/subject/cqfceltx.html