前端直接调用OC的native方法

  在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
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值