繼續上篇文章,iOS開發中熱修復很重要,這篇講如何用Aspects進行熱修復。
一、Aspects為什麼可以熱更新
要達到修復,Native 層只要透出兩種能力就基本可以了:
- 在任意方法前後注入代碼、替換代碼 的能力。
- 調用任意類/實例方法的能力。
第 2 點不難,只要把 [NSObject performSelector:...] 那一套通過 JSContext 暴露出來即可。難的是第 1 點。而Aspects是可以滿足的,只要把它的幾個方法通過 JSContext 暴露給 JS 就可以了。
Aspects 是可以通過 AppStore 的審核。
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<aspectinfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
/<aspectinfo>
這篇文章參考了limboy的文章,現在網上的熱更新也基本都是在他的基礎上改來改去,我們這次講的是limboy開源的代碼,github地址:https://github.com/lzyy/felix。
下面我們寫段崩潰的代碼:
然後我們修復一下:
如果想修改一個ViewController裡面的UItableView的代理方法(例如tableView: numberOfRowsInSection:),上面的字符串替換成:
fixInstanceMethodReplace("MyTableViewController", "tableView:numberOfRowsInSection:", function(instance, invocation){
// 這裡就是新的實現
})
熱更新過程:
首先通過網絡請求再結合一些加密 獲取下發的js 字符串。然後執行[Felix evalString:js字符串]方法 就可以了。
為了讀源代碼,我們先來溫習一下JavaScriptCore。
如果對這塊比較熟悉的話就可以跳過這一小節。
JavaScriptCore
JavaScriptCore是webkit的一個重要組成部分,主要是對JS進行解析和提供執行環境。
我們可以脫離webview直接運行我們的js。iOS7以前我們對JS的操作只有webview裡面一個函數 stringByEvaluatingJavaScriptFromString,JS對OC的回調都是基於URL的攔截進行的操作。
JSContext是JS執行的環境。一個 Context 就是一個 JavaScript 代碼執行的環境,也叫作用域。
JSValue:我們對JS的操作都是通過它。每個JSValue都是強引用一個context。OC和JS對象之間的轉換也是通過它。
OC和JS之間的通信
1、OC中執行JS:
self.context = [[JSContext alloc] init];
NSString *js = @"function add(a,b) {return a+b}";
[self.context evaluateScript:js];
JSValue *n = [self.context[@"add"] callWithArguments:@[@2, @3]];
NSLog(@"---%@", @([n toInt32]));//---5
2、JS調用OC
self.context = [[JSContext alloc] init];
self.context[@"add"] = ^(NSInteger a, NSInteger b) {
NSLog(@"---%@", @(a + b));
};
[self.context evaluateScript:@"add(2,3)"];
我們定義一個block,然後保存到context裡面,其實就是轉換成了JS的function。然後我們直接執行這個function,調用的就是我們的block裡面的內容了。
實際中的簡單例子:
OC調用JS的nativeCallJS方法。
JS調用OC的jsCallNative方法。
<button>調用OC代碼
OC中的代碼:
- (void)doSomeJsThings{
self.jsContext = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"出現異常,異常信息:%@",exception);
};
//oc調用js
JSValue * nativeCallJS = self.jsContext[@"nativeCallJS"];
[nativeCallJS callWithArguments:@[@"hello word"]];//調用了js中方法"nativeCallJS",並且傳參數@"hello word"
//在本地生成js方法,供js調用
self.jsContext[@"jsCallNative"] = ^(NSString *paramer){
JSValue *currentThis = [JSContext currentThis];
JSValue *currentCallee = [JSContext currentCallee];
NSArray *currentParamers = [JSContext currentArguments];
dispatch_async(dispatch_get_main_queue(), ^{
// js調起OC代碼,代碼在子線程,更新OC中的UI,需要回到主線程
NSLog(@"js傳過來:%@",paramer);
});
NSLog(@"JS paramer is %@",paramer);
NSLog(@"currentThis is %@",[currentThis toString]);
NSLog(@"currentCallee is %@",[currentCallee toString]);
NSLog(@"currentParamers is %@",currentParamers);
};//生成native的js方法,方法名:@"jsCallNative",js可直接調用此方法
}
三、分析felix原理
felix的github地址已經在上面給出了。
我們看上面熱更新修復的代碼,第一句是:
[Felix fixIt];
下面我們看下這個方法:
第一句:
JSContext *tempContext = [self context];
先初始化了一個JSContext單例。
然後執行:
tempContext[@"fixInstanceMethod"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:NO aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
調用的fixInstanceMethod方法:
代碼裡,先根據傳過來的instanceName 實例化一個對象(或者類)。然後根據傳過來的方法名 初始化SEL,然後調用Aspects的aspect_hookSelector方法。傳入的是AspectPositionInstead,表示替換之前的方法。
然後在usingBlock裡回調方法:
[fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
回到剛開始我們解決崩潰問題的代碼:
fixImpl對應的是:
function(instance, originInvocation, originArguments){
if (originArguments[0] == 0) {
console.log('zero goes here');
} else {
runInvocation(originInvocation);
}
});
這樣就解決了問題。
上圖中,最後執行JavaScriptCore的evaluateScript方法:
[Felix evalString:fixJsStr];
裡面具體的實現:
+ (void)evalString:(NSString *)javascriptString
{
[[self context] evaluateScript:javascriptString];
}
閱讀更多 雲端夢想科技 的文章