IOS源码解析:JSPatch

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容