在ANDROID中,WebView控件有setJavaScriptEnable接口,这里大概的意思就是让客户端能够响应来自WebView的回调,还有一个接口是addJavaScriptInterface(obj, "external"),这个接口的大概意思是给obj开一个叫"external"的口子,这样前端通过window.external.func(param1,param2...)这样的方式就可以直接调用obj中名叫"func"的方法了。
在IOS中,要想实现这样的WebView需要经过一段周章,下面开始简要说明一下前端能够调用到客户端的代码的基本原理:客户端不管是根据本地的html加载网页还是url动态加载网页,实际上都已经接管了网页上的源码,然而这个源码是用JavaScript写的,这种源码是不能直接对IOS的OC代码进行调用的,我们要做的就是这样的一个转换,让JS通过一个bridge间接调用OC。
;(function() { var messagingIframe, bridge = 'external', CUSTOM_PROTOCOL_SCHEME = 'jscall'; if (window[bridge]) { return } function _createQueueReadyIframe(doc) { messagingIframe = doc.createElement('iframe'); messagingIframe.style.display = 'none'; doc.documentElement.appendChild(messagingIframe); } window[bridge] = {}; var methods = [%@]; for (var i=0;i<methods.length;i++){ var method = methods[i]; var code = "(window[bridge])[method] = function " + method + "() {messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ':' + arguments.callee.name + ':' + encodeURIComponent(JSON.stringify(arguments));}"; eval(code); } //创建iframe,必须在创建external之后,否则会出现死循环 _createQueueReadyIframe(document); //通知js开始初始化 //initReady(); })();
我们通常使用IOS的WebView控件都是通过实现shouldStartLoadWithRequest等相关代理来截获网页url变化这个通知,在url中通常就隐含了我们需要的参数,然而这种方式并不够人性化,前端要是能够直接通过函数调用的方法来call OC的native是比较合理的方式。
shouldStartLoadWithRequest什么时候会被调用?是否一定要url变化才会调用?
shouldStartLoadWithRequest不仅在url变化的时候调用,而且只要网页内容变化的时候也能调用
上面的JS代码
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ':' + arguments.callee.name + ':' + encodeURIComponent(JSON.stringify(arguments));
就是对网页内容进行改变,通过在webview中植入这样的代码,就可以调到shouldStartLoadWithRequest,shouldStartLoadWithRequest是OC的代码,这样就实现了从JS到OC的调用。和Java的反射有点类似。
接下来解决如何在webview中植入这样的代码
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if (webView != _webView) { return; }
//is js insert
if (![[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"typeof window.%@ == 'object'", kBridgeName]] isEqualToString:@"true"]) {
//get class method dynamically
unsigned int methodCount = 0;
Method *methods = class_copyMethodList([self class], &methodCount);
NSMutableString *methodList = [NSMutableString string];
for (int i=0; i<methodCount; i++) {
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(methods[i])) encoding:NSUTF8StringEncoding];
//防止隐藏的系统方法名包含“.”导致js报错
if ([methodName rangeOfString:@"."].location!=NSNotFound) {
continue;
}
[methodList appendString:@"\""];
[methodList appendString:[methodName stringByReplacingOccurrencesOfString:@":" withString:@""]];
[methodList appendString:@"\","];
}
if (methodList.length>0) {
[methodList deleteCharactersInRange:NSMakeRange(methodList.length-1, 1)];
}
free(methods);
NSBundle *bundle = _resourceBundle ? _resourceBundle : [NSBundle mainBundle];
NSString *filePath = [bundle pathForResource:@"WebViewJsBridge" ofType:@"js"];
NSString *js = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:js, methodList]];
}
}
webViewDidFinishLoad这个代理在webview加载完成后调用。
stringByEvaluatingJavaScriptFromString
相当于在webview的尾部追加一段代码,这里不仅追加进去了js代码,还有本地的函数列表,也就是OC暴露给前端可以调用的函数列表,当我们点击webview中的某个按钮触发前端执行了window.external.func(param1, param2)这样的代码,而这个代码因为我们注入了上面那段JS代码,不仅触发了shouldStartLoadWithRequest的执行,还把前端调用的函数名和参数传了回来,接下来就是在shouldStartLoadWithRequest中对这些参数进行整合,变成OC可以识别的代码,就能够正确调用到OC的native方法了
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
NSString *requestString = [[request URL] absoluteString];
if ([requestString hasPrefix:kCustomProtocolScheme]) {
NSArray *components = [[url absoluteString] componentsSeparatedByString:@":"];
NSString *function = (NSString*)[components objectAtIndex:1];
NSString *argsAsString = [(NSString*)[components objectAtIndex:2]
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSData *argsData = [argsAsString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *argsDic = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:argsData options:kNilOptions error:NULL];
//convert js array to objc array
NSMutableArray *args = [NSMutableArray array];
for (int i=0; i<[argsDic count]; i++) {
[args addObject:[argsDic objectForKey:[NSString stringWithFormat:@"%d", i]]];
}
//ignore warning
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
SEL selector = NSSelectorFromString([args count]>0?[function stringByAppendingString:@":"]:function);
if ([self respondsToSelector:selector]) {
[self performSelector:selector withObject:args];
}
return NO;
}else {
return YES;
}
这里的request和真实的url改变带回来的参数组成不太一样,这个值是在JS代码中拼接的,所以这里解析也要按照那个规则逆向解析,后面用到了selector,将函数名function转换成selector,在run-time时就会调到了那个OC中的同名函数了
- (void)writeTopic:(NSArray *)params
{
NSLog(@"writeTopic called");
}
这里整合成一个参数,params数组,可以通过objectAtIndex来取出每个参数,进行后面的相关操作。
总结:
1 通过注入JS代码到webview
2 注入的JS代码在能改变webview的内容,实现网页的跳转(这里用的是一个空白的什么都没有的不可见的网页)
3 根据注入的JS中的规则在shouldStartLoadWithRequest中反向解析,并通过SEL动态调用。