JSPatch原始碼解讀

Yang1492955186752發表於2017-12-13

原理

JSPatch 能做到通過 JS 呼叫和改寫 OC 方法最根本的原因是 Objective-C 是動態語言,OC 上所有方法的呼叫/類的生成都通過 Objective-C Runtime 在執行時進行,我們可以通過類名/方法名反射得到相應的類和方法。

執行過程

當客戶端從伺服器下載一段JS程式碼後: 1.客戶端呼叫[JPEngine StartEngine],這時會建立一個JSContext *_context,然後給_context創造各種方法:

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]);
};

......
複製程式碼

這些方法會通過JavaScriptCore暴露給JS進行呼叫。 2.進行完_context初始化後,會通過_context執行JSPatch自帶的JS語句,進行JS環境下的全域性變數的初始化,比如將JS中方法,引數轉成OC格式語句的核心方法:

//_methodFunc是JS呼叫OC的入口函式
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
var selectorName = methodName
//判斷是否是直接呼叫performSelector,若不是則進行方法名加工
if (!isPerformSelector) {
methodName = methodName.replace(/__/g, "-")
selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
var marchArr = selectorName.match(/:/g)
var numOfArgs = marchArr ? marchArr.length : 0
if (args.length > numOfArgs) {
selectorName += ":"
}
}
var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
_OC_callC(clsName, selectorName, args)
//將OC執行後的返回值轉化為JS格式
return _formatOCToJS(ret)
}
複製程式碼

還有JS中所有呼叫方法的元函式__c:

__c: function(methodName) {
var slf = this

if (slf instanceof Boolean) {
return function() {
return false
}
}
if (slf[methodName]) {
//將從prototype獲取的函式指定繫結作用域
return slf[methodName].bind(slf);
}

if (!slf.__obj && !slf.__clsName) {
throw new Error(slf + '.' + methodName + ' is undefined')
}
if (slf.__isSuper && slf.__clsName) {
slf.__clsName = _OC_superClsName(slf.__obj.__realClsName ? slf.__obj.__realClsName: slf.__clsName);
}
var clsName = slf.__clsName
//假如是下發JS中存在的方法,直接返回JS中的方法
if (clsName && _ocCls[clsName]) {
var methodType = slf.__obj ? 'instMethods': 'clsMethods'
if (_ocCls[clsName][methodType][methodName]) {
slf.__isSuper = 0;
return _ocCls[clsName][methodType][methodName].bind(slf)
}
}
//否則返回一個匿名函式,匿名函式的返回值作為最終的返回值進行接下來的JS方法呼叫
return function(){
var args = Array.prototype.slice.call(arguments)
return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)
}
}
複製程式碼

還有其他的全域性變數宣告,只把這兩個最核心的函式進行展示,就不一一贅述了。

3.進行完所有前期工作後,接下來就是進行下發JS語句的執行,首先利用正規表示式進行JS中的方法替換,比如var tableViewCtrl = JPTableViewController.alloc().init()會轉化成 var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()

4.接下來就會通過[_context evaluateScript:script withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];執行替換後的程式碼。 呼叫global.defineClass = function(declaration, properties, instMethods, clsMethods) {} 這個函式內部會判斷properties是不是陣列

if (!(properties instanceof Array)) {
clsMethods = instMethods
instMethods = properties
properties = null
}
複製程式碼

然後呼叫_formatDefineMethods(instMethods, newInstMethods, realClsName),這個函式主要是構建之前宣告的newInstMethodsnewClsMethods:

newMethods : {
method1: [function(param1,param2...).length ,function(){}],
method2: [function(param1,param2...).length ,function(){}]
}
複製程式碼

陣列內的function是匿名函式(1),如下:

function() {
try {
var args = _formatOCToJS(Array.prototype.slice.call(arguments))
var lastSelf = global.self
global.self = args[0]
if (global.self) global.self.__realClsName = realClsName
args.splice(0,1)
//呼叫原始的JS方法
var ret = originMethod.apply(originMethod, args)
global.self = lastSelf
return ret
} catch(e) {
_OC_catch(e.message, e.stack)
}
}
複製程式碼

然後通過_OC_defineClassdeclarationnewInstMethodsnewClsMethods暴露給OC。

var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
複製程式碼

最終在OC中呼叫defineClass,將JS中傳過來的classDeclaration進行解析,得到當前類和父類以及協議。 當沒有找到當前類時,就在父類中註冊一個新當前類。給當前類新增完協議,將覆蓋或者新建的方法通過runtime將imp指標統一替換成JPForwardInvocation的函式指標。 當在OC中呼叫JS方法會通過JPForwardInvocation,給這個匿名函式傳入OC的引數,獲取原始JS函式的返回值。

if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
//class_replaceMethod這個方法交換不存在的方法時會等同於addMethod,返回nil
IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
if (originalForwardImp) {
class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
}
}
複製程式碼

利用runtime進行方法新增或交換imp然後呼叫overrideMethod,將上述的匿名函式(1)儲存到_JSOverideMethods。 當defineClass在OC環境中執行完畢後,會將Class和SuperClass返回到JS環境下,為了在JS環境中模仿子類能都呼叫父類的方法,會將父類中被JS重寫的方法會同樣儲存在子類中。而所有重寫類的所有類方法和例項方法都放在_ocCls中,大致結構是

_ocCls : {
className: {
instMethods: {
//這裡的function就是下發的指令碼中對應的方法對應的js函式
methodName: function(){},
methodName1: function(){}
},
clsMethods: {
methodName: function(){},
methodName1: function(){}
},
},
className1: {
instMethods: {
methodName: function(){},
methodName1: function(){}
},
clsMethods: {
methodName: function(){},
methodName1: function(){}
},
}
}
複製程式碼

在JS中呼叫元函式時,會判斷_ocCls中是否存在對應的方法,若有,就呼叫_ocCls[clsName][methodType][methodName].bind(slf),將呼叫者slf通過bind傳到對應函式中,等待__c呼叫。

到此,在執行完下載的指令碼後,所需覆蓋的OC方法的imp指標已經指向了JPForwardInvocation

static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
{
//精簡前面部分程式碼
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
//1.替換當前類的forwardInvocation為JPForwardInvocation
IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
if (originalForwardImp) {
class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
}

}

[cls jp_fixMethodSignature];
if (class_respondsToSelector(cls, selector)) {
//2.假如selector有實現,則新增一個新的Selector指向原始的imp,這時候原始的seletor也是指向原始的imp
NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
SEL originalSelector = NSSelectorFromString(originalSelectorName);
if(!class_respondsToSelector(cls, originalSelector)) {
class_addMethod(cls, originalSelector, originalImp, typeDescription);
}
}

NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];

_initJPOverideMethods(cls);
_JSOverideMethods[cls][JPSelectorName] = function;

//3.在最後才進行原始selector的imp替換,防止有可能的呼叫overrideMethod時同時呼叫這個方法導致的多執行緒問題。
//再將原始的Selector指向原始的_objc_msgForward,這樣只要在外界呼叫這個selector直接跳過imp查詢,
//先後執行resolveInstanceMethod,forwardingTargetForSelector,methodSignatureForSelector,forwardInvocation
//當在呼叫forwardInvocation就會跳到JPForwardInvocation
class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);

}
複製程式碼

到此,所有前期配置都已完成,接下來就是等待覆寫的方法被呼叫時,觸發JPForwardInvocation,進行OC和JS的來回傳值。

static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{
BOOL deallocFlag = NO;
id slf = assignSlf;
NSMethodSignature *methodSignature = [invocation methodSignature];
NSInteger numberOfArguments = [methodSignature numberOfArguments];

NSString *selectorName = NSStringFromSelector(invocation.selector);
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
//在_JSOverideMethods中獲取之前儲存的JSFunc,用於呼叫原始的JS函式
JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName);
if (!jsFunc) {
JPExecuteORIGForwardInvocation(slf, selector, invocation);
return;
}
//下面是將引數傳入到JS的程式碼
//將呼叫者資訊儲存到argList
NSMutableArray *argList = [[NSMutableArray alloc] init];
if ([slf class] == slf) {
[argList addObject:[JSValue valueWithObject:@{@"__clsName": NSStringFromClass([slf class])} inContext:_context]];
} else if ([selectorName isEqualToString:@"dealloc"]) {
[argList addObject:[JPBoxing boxAssignObj:slf]];
deallocFlag = YES;
} else {
[argList addObject:[JPBoxing boxWeakObj:slf]];
}
//methodSignatured的前兩個引數固定是呼叫者和seletor
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
switch(argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {
//將引數依次取出,判斷型別,新增到arglist
case '@': {
__unsafe_unretained id arg;
[invocation getArgument:&arg atIndex:i];
if ([arg isKindOfClass:NSClassFromString(@"NSBlock")]) {
[argList addObject:(arg ? [arg copy]: _nilObj)];
} else {
[argList addObject:(arg ? arg: _nilObj)];
}
break;
}
default: {
NSLog(@"error type %s", argumentType);
break;
}
}
}

//將普通物件陣列轉換為字典物件陣列,裡面有引數的例項和類名 @[{__obj:obj,__cls:NSObject},{__obj:obj2,__cls:NSObject}]
NSArray *params = _formatOCToJSList(argList);
//下面是處理返回值型別傳入到JS
char returnType[255];
strcpy(returnType, [methodSignature methodReturnType]);

// Restore the return type
if (strcmp(returnType, @encode(JPDouble)) == 0) {
strcpy(returnType, @encode(double));
}
if (strcmp(returnType, @encode(JPFloat)) == 0) {
strcpy(returnType, @encode(float));
}
//當呼叫例項方法,且引數是id型別時呼叫順序: jsFunc->originJSMethod->__c->_methodFunc->_OC_callI->callSelector->獲取返回值,一步步回傳到jsVal
//如此就完成了JS和OC的通訊,整個過程是序列執行的。

switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
#define JP_FWD_RET_CALL_JS \
JSValue *jsval; \
[_JSMethodForwardCallLock lock];   \
jsval = [jsFunc callWithArguments:params]; \
[_JSMethodForwardCallLock unlock]; \
case 'v': {
JP_FWD_RET_CALL_JS
break;
}
default: {
break;
}
}

if (_pointersToRelease) {
for (NSValue *val in _pointersToRelease) {
void *pointer = NULL;
[val getValue:&pointer];
CFRelease(pointer);
}
_pointersToRelease = nil;
}

if (deallocFlag) {
slf = nil;
Class instClass = object_getClass(assignSlf);
Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc"));
void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));
}
}
複製程式碼

關於block傳遞

當JSPatch掃描下發指令碼時,會將所有的block標記,還是以呼叫例項方法為例,當呼叫鏈走到callSelector時,對JS引數進行解析的時候,假如發現有block

if ([(JSValue *)arguments[i-2] hasProperty:@"__isBlock"]) {
JSValue *blkJSVal = arguments[i-2];
Class JPBlockClass = NSClassFromString(@"JPBlock");
if (JPBlockClass && ![blkJSVal[@"blockObj"] isUndefined]) {
__autoreleasing id cb = [JPBlockClass performSelector:@selector(blockWithBlockObj:) withObject:[blkJSVal[@"blockObj"] toObject]];
[invocation setArgument:&cb atIndex:i];
Block_release((__bridge void *)cb);
} else {
__autoreleasing id cb = genCallbackBlock(arguments[i-2]);
[invocation setArgument:&cb atIndex:i];
}
}
複製程式碼

將傳入的block通過libffi生成一個函式指標,替換invocation原來的引數。

相關文章