ios热更新审核通关攻略(ios热更新解决方法)
一、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];
这篇文章参考了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方法。
<html> <body> <script type="text/javascript"> var nativeCallJS = function(parameter) { alert (parameter); }; </script> <button type="button" onclick = "jsCallNative('jsParameter')"/>调用OC代码</button> </body> </html>
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]; }