項目中使用到了JSPatch來實現線上APP bug的hot fix,使用後覺得JSPatch短小精悍並且功能強大,於是想往裏窺探下其實現機制。本文從JSPatch的使用角度去分析源碼。
JavaScript運行環境建立
JavaScript運行環境的建立很簡單,只需要調用一個API即可:
[JPEngine startEngine];
+(void)startEngine
API的主要工作是建立一個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.js
JS代碼:
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全局上下文中定義了defineClass
、defineProtocol
等。
解析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 *)resourceURL
API將該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.js
中defineClass
函數可以動態添加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
,提取出className
,superClassName
以及protocolNames
- 如果
className
類不存在,則通過運行時方法objc_allocateClassPair
和objc_registerClassPair
創建並註冊className
類 - 解析實例方法和類方法,
- 將方法名中的雙下劃線
__
替換成單連字符-
- 將單下劃線
_
替換成冒號:
- 將單連字符
-
替換成單下劃線_
,這裏其實就是告訴開發者如果方法名中實際包含下劃線,則需要使用雙下劃線
- 將方法名中的雙下劃線
- 比較替換後的方法中冒號
:
的個數以及_formatDefineMethods
返回的方法參數個數,決定替換後的方法名後是否需要增加一個:
- 通過調用
class_respondsToSelector
判斷className
類是否已經定義該方法,如果定義了該方法,則調用overrideMethod
重寫該方法,調用該方法不需要傳入參數描述。 - 如果
className
類沒有定義該方法,調用methodTypesInProtocol
方法查詢繼承的protocol
中有沒有定義該方法,如果定義了該方法,則返回該方法的參數描述,然後將這個參數描述作爲參數調用overrideMethod
- 如果繼承的
protocol
裏沒有定義該方法,則根據參數個數構建一個參數描述,這裏的參數個數需要加上self
和_cmd
參數,並且所有其它參數都是id
類型,然後將這個生成的參數描述作爲參數調用overrideMethod
- 調用運行時方法
class_addMethod
給className
類添加了getProp
和setProp: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)
- 如果傳入的參數描述符
typeDescription
爲null
,則說明這個方法已經定義過,通過class_getInstanceMethod
從cls
類獲取到selectorName
對應的Method
,再通過method_getTypeEncoding
得到Method
的參數描述符。 如果cls類已經實現過
selectorName
方法,則獲取到原來方法對應的函數實現IMP:IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
根據
typeDescription
可知道方法的返回值是不是結構體,建立selectorName
選擇子和_objc_msgForward(IMP)
或者_objc_msgForward_stret(IMP)
的映射關係,class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
將
@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@:@"); }
如果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); } }
通過全局字典數組保存
cls
、_JPselectorName
、function
之間的映射關係:_initJPOverideMethods(cls); _JSOverideMethods[cls][JPSelectorName] = function;
- 給
cls
添加_JPselectorName
方法,映射到前面得到的msgForwardIMP
。