JSPatch源碼剖析(一)

項目中使用到了JSPatch來實現線上APP bug的hot fix,使用後覺得JSPatch短小精悍並且功能強大,於是想往裏窺探下其實現機制。本文從JSPatch的使用角度去分析源碼。

JavaScript運行環境建立

JavaScript運行環境的建立很簡單,只需要調用一個API即可:

[JPEngine startEngine];

+(void)startEngineAPI的主要工作是建立一個JSContext類型的全局_context變量,後續JS和OC的互調都是在這個上下文中進行。由於JSContext實現了下面兩個方法,從而可以進行下標操作訪問:

- (JSValue *)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;

_contenxt的初始化中,就大量的運用了下標操作符,就是下面這種方式:

context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };
...

通過這種方式,以後就可以在JS中直接使用_OC_defineClass,從而調用到OC的defineClass函數。
在初始化最後,直接在_context中運行下jspatch.jsJS代碼:

    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
    NSAssert(path, @"can't find JSPatch.js");
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];

    if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
        [_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
    } else {
        [_context evaluateScript:jsCore];
    }

jspatch.js中在JS全局上下文中定義了defineClassdefineProtocol等。

解析JS代碼

下面是JSPatch Demo裏的一段JS源碼,該demo通過JS寫了個UITableViewContorller的示例:

// demo.js
defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', {
  dataSource: function() {
    var data = self.getProp('data')
    ...
    self.setProp_forKey(data, 'data')
    return data;
  },
  numberOfSectionsInTableView: function(tableView) {},
  tableView_cellForRowAtIndexPath: function(tableView, indexPath) {},
  ...
})

通過調用+ (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURLAPI將該JS代碼放在之前建立的 _context中運行;在正式運行該JS代碼前先對JS字符串進行了預處理,通過正則表達式 \\.\\s*(\\w+)\\s*\\(找到JS腳本中的所有函數調用,比如demo.js中的self.getProp('data'),將函數名替換成 self.__c("getProp")('data'),即添加一層__c(arg)JS調用,這樣調用任何方法前,先調用__c(arg)方法,這個方法在JSPatch.js中定義,後續詳細講解這個函數的定義。

    NSString *formatedScript = [NSString stringWithFormat:@"try{%@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];

函數替換完成後調用 下面方法執行得到的JS腳本formatedScript。

- (JSValue *)evaluateScript:(NSString *)script;

JSPatch.jsdefineClass函數可以動態添加OC類,也可以給類添加類方法、實例方法、屬性。defineClass函數定義如下,主要對傳入的實例方法和類方法調用_formatDefineMethods函數進行js格式化,然後再調用_OC_defineClass函數處理前者的結果,進行更深入的類定義、類方法以及實例方法解析:

// JSPatch.js
global.defineClass = function(declaration, instMethods, clsMethods) {
    var newInstMethods = {}, newClsMethods = {}
    _formatDefineMethods(instMethods, newInstMethods)
    _formatDefineMethods(clsMethods, newClsMethods)
    var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
    return require(ret["cls"])
  }

其中_formatDefineMethods函數定義如下,該函數對傳入的methods字典轉化成一個新的字典,key依然是原來的方法名,value變成一個包含兩個元素的數組,第一個元素是方法參數個數(不包括self_cmd參數),第二個元素是基於原js函數重新定義的js函數;重新定義的js函數先保存原來的全局self對象,然後將原js方法的第一個參數(即self)賦值給全局的self對象,並將self參數從參數列表裏移除,利用剩下的參數調用原來js方法,並將執行結果返回。

// JSPatch.js
  var _formatDefineMethods = function(methods, newMethods) {
    for (var methodName in methods) {
      (function(){
       var originMethod = methods[methodName]
        newMethods[methodName] = [originMethod.length, function() {
          var args = _formatOCToJS(Array.prototype.slice.call(arguments))
          var lastSelf = global.self
          var ret;
          try {
            global.self = args[0]
            args.splice(0,1)
            ret = originMethod.apply(originMethod, args)
            global.self = lastSelf
          } catch(e) {
            _OC_catch(e.message, e.stack)
          }
          return ret
        }]
      })()
    }
  }

前面例子返回的字典爲:

{"dataSource" : [0,func], "numberOfSectionsInTableView" : [1, func], "tableView_cellForRowAtIndexPath" : [2, func]}.

實例方法和類方法字典格式化後,調用_OC_defineClass進行進一步解析,_OC_defineClass函數其實是調用了OC函數static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
,定義如下:

   context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };

defineClass函數做具體的解析工作, defineClass函數的具體解析步驟如下:

  • 掃描classDeclaration,提取出 classNamesuperClassName以及 protocolNames
  • 如果className類不存在,則通過運行時方法 objc_allocateClassPairobjc_registerClassPair創建並註冊className
  • 解析實例方法和類方法,
    • 將方法名中的雙下劃線__替換成單連字符-
    • 將單下劃線_替換成冒號:
    • 將單連字符-替換成單下劃線_,這裏其實就是告訴開發者如果方法名中實際包含下劃線,則需要使用雙下劃線
  • 比較替換後的方法中冒號:的個數以及_formatDefineMethods返回的方法參數個數,決定替換後的方法名後是否需要增加一個:
  • 通過調用 class_respondsToSelector判斷className類是否已經定義該方法,如果定義了該方法,則調用 overrideMethod重寫該方法,調用該方法不需要傳入參數描述。
  • 如果className類沒有定義該方法,調用 methodTypesInProtocol方法查詢繼承的protocol中有沒有定義該方法,如果定義了該方法,則返回該方法的參數描述,然後將這個參數描述作爲參數調用overrideMethod
  • 如果繼承的protocol裏沒有定義該方法,則根據參數個數構建一個參數描述,這裏的參數個數需要加上self_cmd參數,並且所有其它參數都是id類型,然後將這個生成的參數描述作爲參數調用overrideMethod
  • 調用運行時方法 class_addMethodclassName類添加了 getPropsetProp:forKey:兩個方法,這兩個方法通過關聯對象方式給類實例動態添加屬性,代碼如下。
  • 最後返回字典 {@"cls": className}require(‘className’)JS方法,該方法將類信息記錄在JS全局變量global["className"] = {__isCls : 1, __clsNmae : "className"};
// defineClass函數
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");

static id getPropIMP(id slf, SEL selector, NSString *propName) {
    return objc_getAssociatedObject(slf, propKey(propName));
}
static void setPropIMP(id slf, SEL selector, id val, NSString *propName) {
    objc_setAssociatedObject(slf, propKey(propName), val, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

OC的defineClass函數調用了 methodTypesInProtocol方法獲取方法的參數描述符,這個函數通過運行時方法 objc_getProtocol得到協議相關信息,

    Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);

然後通過運行時方法 protocol_copyMethodDescriptionList獲取到協議中定義的可選(必須)的實例方法或者類方法列表,

    struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, isRequired, isInstanceMethod, &selCount);

最後遍歷得到的列表,找到selectorName對應的objc_method_description,並將其參數描述符返回。

從上面分析可知OC方法 defineClass最終其實調用overrideMethod將實例方法instanceMethods和類方法classMethods添加到className類的,overrideMethod具體的實現步驟如下:

//overrideMethod函數原型
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
  1. 如果傳入的參數描述符 typeDescriptionnull,則說明這個方法已經定義過,通過 class_getInstanceMethodcls類獲取到selectorName對應的Method,再通過 method_getTypeEncoding得到Method的參數描述符。
  2. 如果cls類已經實現過selectorName方法,則獲取到原來方法對應的函數實現IMP:

    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
    
  3. 根據typeDescription可知道方法的返回值是不是結構體,建立selectorName選擇子和 _objc_msgForward(IMP)或者 _objc_msgForward_stret(IMP)的映射關係,

    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
  4. @selector(forwardInvocation:)的IMP設置爲 JPForwardInvocation,將@selector(ORIGforwardInvocation:)的IMP設置爲@selector(forwardInvocation:)原來的實現

    //overridMethod
    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
        class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
    }
  5. 如果cls類原本實現過selectorName方法,則將原來的方法重新命名爲ORIGselectorName,即在原來方法名加前綴ORIG,

    if (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(!class_respondsToSelector(cls, originalSelector)) {
            class_addMethod(cls, originalSelector, originalImp, typeDescription);
        }
    }
  6. 通過全局字典數組保存cls_JPselectorNamefunction之間的映射關係:

    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function;
  7. cls添加_JPselectorName方法,映射到前面得到的 msgForwardIMP
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章