文档章节

前端直接调用OC的native方法

大巴黎
 大巴黎
发布于 2015/09/10 11:36
字数 1249
阅读 63
收藏 1

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

© 著作权归作者所有

大巴黎
粉丝 3
博文 19
码字总数 13049
作品 0
广州
私信 提问
Weex 是如何在 iOS 客户端上跑起来的

前言 2016年4月21日,阿里巴巴在Qcon大会上宣布跨平台移动开发工具Weex开放内测邀请。Weex能够完美兼顾性能与动态性,让移动开发者通过简捷的前端语法写出Native级别的性能体验,并支持iOS、...

一缕殇流化隐半边冰霜
2017/03/20
0
0
react-native自定义原生组件

此文已由作者王翔授权网易云社区发布。 欢迎访问网易云社区,了解更多网易技术产品运营经验。 使用react-native的时候能够看到不少函数调用式的组件,像LinkIOS用来呼起url请求 LinkIOS.open...

网易云
2018/10/18
0
0
WKWebView的使用和各种坑的解决方法(OC+Swift)

虽然是在的随和出来的,是为了解决加载速度慢、占用内存大的问题。但是由于之前还要适配,又不想做两套加载页面(主要是因为懒),所以就没有使用。现在项目都适配以上了,所以就开始使用了,...

winann
2016/07/01
0
0
iOS进阶补完计划-WebViewJavascriptBridge实现原理

提及其原理、所有用过它的童鞋都会说他在js和Native(原生)之间搭建了一个桥梁。通过这个桥、使他们相互通信。但具体怎么通信呢?这个桥如何工作?十有八九说却不清。 JSBridge的逻辑简而言...

kirito_song
2017/12/19
0
0
IOS中 使用JavaScriptCore 实现OC与JS的交互(第二篇)

一、说明 这篇文章记录自己在研究OC与JS交互中的所得,以及遇到的问题与解决 由于苹果的审核时间太漫长,一次审核不过,那又将进入另一个漫长的审核期。为了能在开发中方便更新,公司要求在项目...

HeroHY
2016/09/26
1
0

没有更多内容

加载失败,请刷新页面

加载更多

Eureka应用注册与集群数据同步源码解析

在之前的EurekaClient自动装配及启动流程解析一文中我们提到过,在构造DiscoveryClient类时,会把自身注册到服务端,本文就来分析一下这个注册流程 客户端发起注册 boolean register() t...

Java学习录
8分钟前
1
0
Java描述设计模式(15):责任链模式

本文源码:GitHub·点这里 || GitEE·点这里 一、生活场景描述 1、请假审批流程 公司常见的请假审批流程:请假天数 当 day<=3 天,项目经理审批当 3<day<=5 天,部门经理审批当 day>5 天...

知了一笑
18分钟前
3
0
总结:数组与链表

1、内存申请:数组在内存上是连续的空间;链表,内存地址上可以是不连续的。 2、查询速度:数组可以随机访问,链表必须顺序访问,即从首个元素开始遍历,逐个查找,所以数组查询很快。 3、写入...

浮躁的码农
27分钟前
3
0
HashMap源码分析

read

V丶zxw
45分钟前
5
0
Python字符串或JSON字符串转字典dict、列表list

有3种方法 1、使用ast模块 >>> import ast>>> s = '["test",1]'>>> ast.literal_eval(s)['test',1]>>> s = '{"test":1}'>>> ast.literal_eval(s){'test': 1} 2、eval函数,这个......

编程老陆
今天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部