前端直接调用OC的native方法

来源:互联网 发布:一事无成 知乎 编辑:程序博客网 时间:2024/05/14 14:23

  在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动态调用。

前端直接调用OC的native方法

0 0
原创粉丝点击