OCEval
需求
目前流行的 JSPatch/RN 基于JavaScriptCore提供了iOS的熱修復(fù)和動(dòng)態(tài)化方案撒顿。但是都必須通過下發(fā)Javascript腳本來調(diào)用Objective-C活合。 尤其是JSPatch婿屹,編寫大量的JS代碼來調(diào)用OC的方法菩掏,開發(fā)效率較低(目前可以借助語法轉(zhuǎn)換器)霎烙,運(yùn)行效率也會(huì)打折扣募寨。
更好的方案是直接編寫Objective-C代碼,來實(shí)現(xiàn)熱修復(fù)或者動(dòng)態(tài)化方案审葬。開發(fā)效率更高深滚,代碼的執(zhí)行效率也更高。
在python和javascript等腳本語言里涣觉,有類似eval()函數(shù)來直接動(dòng)態(tài)執(zhí)行代碼痴荐。所以我實(shí)現(xiàn)了OCEval 這個(gè)庫,讓我們能直接動(dòng)態(tài)執(zhí)行Objective-C代碼官册。例子如下:
NSString *inputStr = @"return 1 + 3 <= 4 && [NSString string] != nil;";
NSNumber *result = [OCEval eval:inputStr]; // result: @(YES)
為了實(shí)現(xiàn)跟JSPatch類似的熱修復(fù)功能生兆,增加了方法替換。我們就可以通過下發(fā)Objective-C代碼進(jìn)行現(xiàn)有App的方法替換膝宁,來進(jìn)行熱修復(fù)的功能鸦难。
//在新的imp里直接調(diào)用舊的方法實(shí)現(xiàn)
NSString *viewDidLoad2 = @"{\
[originalInvocation invoke];\
";
[OCEval hookClass:@"ViewController"
selector:@"viewDidLoad"
argNames:@[]
isClass:NO
implementation:viewDidLoad2];
OCEval甚至可以用來完整的編寫一個(gè)頁面或者App,并動(dòng)態(tài)下發(fā)员淫。我在iOS的Demo里實(shí)現(xiàn)了一個(gè)簡單的頁面合蔽,具體見源碼。
實(shí)現(xiàn)原理
C和C++的性能高满粗,是因?yàn)榫幾g型語言在編譯期就已經(jīng)生成了機(jī)器碼,運(yùn)行時(shí)只需要執(zhí)行機(jī)器碼所以執(zhí)行效率高愚争,但是動(dòng)態(tài)性差映皆。
js的性能差挤聘,是因?yàn)閖s的runtime引擎通常是在實(shí)際執(zhí)行前進(jìn)行的編譯的。優(yōu)點(diǎn)是動(dòng)態(tài)性好捅彻。
像Dart和python等等都可以編譯打包執(zhí)行或者JIT(Just in time)執(zhí)行组去。
不同于C和C++,Objective-C是動(dòng)態(tài)化的語言步淹,Objective-C的runtime利用消息發(fā)送和轉(zhuǎn)發(fā)可以動(dòng)態(tài)地執(zhí)行任何方法从隆。
同時(shí)Objective-C又不同于javascript等完全動(dòng)態(tài)化的語言。 因?yàn)榇蠖鄶?shù)調(diào)用是在編譯期就已經(jīng)決定的缭裆,編譯出可執(zhí)行文件(mach-O)键闺。
所以在OCEval里實(shí)現(xiàn)了一個(gè)輕量級的解釋器,動(dòng)態(tài)地解釋Objective-C代碼澈驼,同時(shí)利用OC的runtime消息轉(zhuǎn)發(fā)來動(dòng)態(tài)執(zhí)行Objective-C的代碼辛燥,就可以實(shí)現(xiàn)類似eval()函數(shù)的完全動(dòng)態(tài)化方式。
解釋器
Objective-C在LLVM下的編譯過程:
源碼 -> AST -> LLVM IR(中間語言) -> LLVM Bytecode -> ASM -> Native
LLVM的前端是Clang缝其,Clang的工作是把源碼變成AST語法生成樹挎塌。
Clang的前端編譯過程:
-
Preprocesser
: 包括#include #import等預(yù)處理, #if,#ifdef 等條件,#define等宏定義 -
Lexer
:詞法分析内边,把文本變成token(Tokenizer) -
Parser
:語法分析榴都,把token變成AST
但是在OCEval里,沒有做得那么復(fù)雜漠其,因?yàn)橹皇菫榱四軌驁?zhí)行嘴高。所以只實(shí)現(xiàn)了詞法分析和語法分析,得到語法生成樹AST辉懒。
Runtime
執(zhí)行的時(shí)候遞歸下降地執(zhí)行每一條指令阳惹。這里利用的runtime主要是NSInvocation,利用methodSignature封裝方法的調(diào)用慣例眶俩,跟JSPatch/RN的最終調(diào)用方式如出一轍莹汤。
+ (id)invokeWithCaller:(id)caller selectorName:(NSString *)selectorName argments:(NSArray *)arguments
{
SEL selector = NSSelectorFromString(selectorName);
NSInvocation *invocation;
NSMethodSignature *methodSignature = [caller methodSignatureForSelector:selector];
invocation= [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:caller];
[invocation setSelector:selector];
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
NSInteger inputArguments = [arguments count];
if (inputArguments > numberOfArguments - 2) {
id result = invokeVariableParameterMethod(arguments, methodSignature, caller, selector); //轉(zhuǎn)而調(diào)用objc_msgsend
return result;
}
return [self invokeWithInvocation:invocation argments:arguments];
}
參考JSPatch,在類似[NSString stringWithFormat:]
這樣可變參數(shù)的方法里使用objc_msgsend
颠印。因?yàn)?code>NSInvocation不支持不確定的參數(shù)個(gè)數(shù)的情況纲岭。
性能
因?yàn)槭∪チ烁鶭avascriptCore進(jìn)行參數(shù)傳遞的過程,單個(gè)方法調(diào)用比JSPatch/RN快100%线罕,耗時(shí)只有JSPatch一半止潮,多個(gè)方法調(diào)用優(yōu)勢更大,耗時(shí)可能只有30%以下钞楼。
NSInvocation的調(diào)用跟Native速度差不多喇闸。但是因?yàn)閯?dòng)態(tài)調(diào)用很麻煩,入?yún)⒊鰠⒑驼{(diào)用慣例都需要?jiǎng)討B(tài)定義,同時(shí)上下文參數(shù)在內(nèi)存的傳遞也比較慢燃乍,所以整體是比原生慢很多(動(dòng)態(tài)化必要的犧牲)唆樊。
審核
我沒有嘗試過提交AppStore審核,但是鑒于JSPatch屢次被拒絕刻蟹,被拒絕的可能性極大逗旁。我們的App確實(shí)也沒有熱修復(fù)的需求。
感謝
感謝JSPatch,libff,Aspect
Github 鏈接在 OCEval