概述

在iOS6之前,native只能调用webiew里的js代码,官方没有提供js调用native方法的接口。到了iOS7,官方提供了JSContext用来与js交互,native和js可以双向调用。iOS8之后,提供了WKWebview,开放了很多接口来处理H5和native之间的交互,H5与native之间的交互更加的便利了。为了兼容各个版本的js与native的交互,大多数app都没有使用官方提供的与js与native交互的方法,而是使用第三方框架,通过拦截URL的方式实现交互。现在比较流行的一个交互框架是WebViewJavascriptBridge,一直有人维护。而目前财富宝iOS中使用的交互框架是EasyJSWebView,此框架是一个非常老的框架,已经无人维护了,最近一次维护是5年前。财富宝iOS也还没使用WKWebView,使用的是老的UIWebVeiw,下面我们就分析一下在UIWebVeiw中此框架实现js跟native交互的原理。

具体分析

我们先来熟悉一下native中跟js关系比较密切的方法。

当UIWebview在加载一个url的时候,ios系统提供了几个方法,使得我们可以介入加载过程做我们想做的事情,其中两个比较重要的方法是:

1.-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{

}

这个方法的语言是oc,不熟悉oc的同学看起来比较费劲,我们改造一下变成与js类似的方法就是

function webViewshouldStartLoad(webView,request,navigationType){

}

为了下面表述方便,我们将此方法称为方法1。这个方法可以理解为是一个钩子函数,当UIWebVeiw将要加载一个url时就会调用此方法,并把要加载的url作为入参(方法里的request)传给此函数,其实就是在将要加载一个url时询问我们是否允许加载此url,如果我们在函数体里返回YES,则UIWebView就加载此url,如果我们返回NO,则UIWebView就不加载此url。

2.-(void)webViewDidFinishLoad:(UIWebView *)webView{

}

用js的写法就是

function webViewDidFinishLoad(webView){

}

为了下面表述方便,我们将此方法称为方法2。这个方法是在html加载完成之后触发。

为什么说这两个方法重要呢,因为js要调用native的方法主要就是利用这两个方法来实现。我们上面说到过,为了兼容各个版本,第三方框架主要是通过拦截url的方式实现,简单来说就是当js想调用native某个方法时,就先按照约定好的格式生成一个url,把方法名和参数拼到url里,然后让webView加载这个url,一旦加载某个url,方法1就会被调用,我们在方法1里就可以捕获这个url,然后对这个url进行解析,取出方法名和参数,利用取到的信息生成native方法并执行,就完成了js对native方法的调用。这就是大概的思路,但是还需要处理很多细节,比如返回值怎么获取,回调函数怎么执行等等。

3.-(NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script

用js的写法是

stringByEvaluatingJavaScriptFromString(script)

为了下面表述方便,我们将此方法称为方法3。这个方法的入参是一段js脚本,native可以调用此方法来执行js代码,并返回js执行后的返回值。

native和js的交互基本就是依赖于上面3个方法进行。下面我们就来具体的剖析一下EasyJSWebView框架的具体实现。

第一步,利用native方法3执行一段js脚本,脚本代码如下:

window.EasyJS = {
__callbacks: {},//用于保存回调函数
invokeCallback: function (cbID, removeAfterExecute){
var args = Array.prototype.slice.call(arguments);
args.shift();
args.shift();
for (var i = 0, l = args.length; i < l; i++){
args[i] = decodeURIComponent(args[i]);
}
//取出对应函数
var cb = EasyJS.__callbacks[cbID];
if (removeAfterExecute){
EasyJS.__callbacks[cbID] = undefined;
}
return cb.apply(null, args);//执行取出的函数
},
call: function (obj, functionName, args){
var formattedArgs = [];
//遍历传入的参数
for (var i = 0, l = args.length; i < l; i++){
//如果入参是函数,往formattedArgs数组里插入一个f标记
if (typeof args[i] == "function"){
formattedArgs.push("f");
//生成一个唯一标识变量
var cbID = "__cb" + (+new Date);
//以此唯一标识变量为键,以传入的函数入参为值保存在__callbacks对象中
EasyJS.__callbacks[cbID] = args[i];
//将唯一标识变量页加入formattedArgs数组中,紧跟在f标识之后
formattedArgs.push(cbID);
}else{
//如果入参不是函数,则用s标记,加入formattedArgs数组
formattedArgs.push("s");
//把入参紧跟在s后面计入formattedArgs数组
formattedArgs.push(encodeURIComponent(args[i]));
}
}
//以上处理后生成的formattedArgs数组类似于[f,唯一标识1,s,非函数入参1,s,非函数入参2]
var argStr = (formattedArgs.length > 0 ? ":" + encodeURIComponent(formattedArgs.join(":")) : "");//处理后生成类似于":f:唯一标识1:s:非函数入参1:s:非函数入参2"的字符串
var iframe = document.createElement("IFRAME");
//把方法名和入参转化成一个url赋给iframe的src
//会生成类似于src="easy-js:Native:showMsg::f:唯一标识1:s:非函数入参1:s:非函数入参2"
iframe.setAttribute("src", "easy-js:" + obj + ":" + encodeURIComponent(functionName) + argStr);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
//执行native方法后如果有返回值会把返回值赋给EasyJS.retValue
var ret = EasyJS.retValue;
EasyJS.retValue = undefined;
if (ret){
return decodeURIComponent(ret);
}
},
inject: function (obj, methods){
//给window加一个obj属性
window[obj] = {};
var jsObj = window[obj];
//遍历方法数组,取出方法名
for (var i = 0, l = methods.length; i < l; i++){
(function (){
var method = methods[i];
//去掉方法名里的冒号(ios的方法名带有冒号)
var jsMethod = method.replace(new RegExp(":", "g"), "");
//把去掉冒号之后的方法名作为obj对象的属性,该属性的值是一个调用call函数的函数,call函数的入参是调用inject函数时传进来的obj,遍历到的当前方法名本身(method)
jsObj[jsMethod] = function (){
return EasyJS.call(obj, method, Array.prototype.slice.call(arguments));
};
})();
}
}
};

这段脚本执行后,window对象下面就有了个EasyJS对象,这个对象有inject函数、call函数、invokeCallback函数、__callbacks对象。

inject函数:用来注入native暴露给js调用的方法;

call函数:将js方法转换成一个特定规则的url

invokeCallback函数:回调js方法

__callbacks:保存js的回调函数

第二步,取出所有的native暴露给js调用的方法(比如showMsg:、getInfo、callBack:),生成一段字符串脚本,例如“EasyJS.inject("Native", ["showMsg:","getInfo","callFunction:"])”。

oc的方法如果有参数都是带冒号的,有几个参数就会带几个冒号。

第三步,在方法2中执行脚本EasyJS.inject("Native", ["showMsg:","getInfo","callBack:"]),就会调用window下EasyJS对象中的inject函数,inject函数将native暴露给js的方法注入到window下,然后js就可以通过形如Native.showMsg("这是条消息")这样的方式调用native的方法了。

这里关键是EasyJS对象下的inject函数和call函数。我们先来看inject函数,这个函数接收两个参数,一个obj和一个methods数组,这个函数首先把obj挂到window对象下,然后遍历methods数组,取出里面的方法名,把方法名里的冒号去掉,然后以去掉冒号的方法名为键,它的值是一个调用了EasyJS对象下call函数的函数。把{方法名:函数}这样的键值对加入到obj对象中。以脚本EasyJS.inject("Native", ["showMsg:","getInfo","callFunction:"])为例,执行后将生成这样的对象:

window.Native={
showMsg:function(){
return EasyJS.call("Native", "showMsg", Array.prototype.slice.call(arguments));
},
getInfo:function(){
return EasyJS.call("Native", "getInfo", Array.prototype.slice.call(arguments));
},
callBack:function(){
return EasyJS.call("Native", "callFunction", Array.prototype.slice.call(arguments));
}
}
 

call函数是js最终能够调用native函数的关键。我们来具体分析下看call方法都做了什么。call方法会遍历传入的参数,判断入参的类型,如果是函数,标记为f,并生成一个唯一标识串,以这个唯一标识串为键,以对应的函数为值,存入__callbacks对象中。如果非函数,标记为s。最后形成一个类似这样的数组[f,唯一标识1,s,非函数入参1,s,非函数入参2]。然后再对这个数组用冒号分离成一个字符串":f:唯一标识1:s:非函数入参1:s:非函数入参2",最后结合函数名等字符串拼接成最终的url,类似于"easy-js:Native:showMsg:f:唯一标识1:s:非函数入参1:s:非函数入参2",这个url里就携带了方法名和参数,把这个url赋给一个临时生成的iframe的src,触发UIWebView的方法1,在方法1里可以捕获到这个url,对其解析后生成相应的native方法并执行,就可完成native方法的调用。如果native方法执行后有返回值,调用方法3执行EasyJS.retValue=返回值脚本,就可把返回值存到EasyJS对象的retValue变量里,call方法在最后取到这个值再return出来给调用方。

EasyJS对象下的__callbacks对象中存储了需要native回调的js方法,native在方法1中解析url时如果遇到f标识符,就知道需要回调js的方法,会取出跟在f之后的唯一标识符,生成EasyJS.invokeCallback("cbID", "removeAfterExecute")脚本,利用native方法3执行脚本,脚本执行后,EasyJS对象下的invokeCallback函数被调用,函数传入的cbID唯一标识符就可以在__callbacks对象中找到对应的回调函数,执行该函数就可完成对js方法的回调。

总结

通过拦截URL的方式实现js与native交互是业界通用做法,能兼容各种版本。EasyJSWebView是个非常古老的框架了,已经无人维护,使用此框架的项目也比较少,里面可能埋有一些深坑,希望通过以上的分析大家能够了解其中的细节,为解决以后可能出现的交互问题打下坚实的基础。

EasyJSWebView原理分析的更多相关文章

  1. Handler系列之原理分析

    上一节我们讲解了Handler的基本使用方法,也是平时大家用到的最多的使用方式.那么本节让我们来学习一下Handler的工作原理吧!!! 我们知道Android中我们只能在ui线程(主线程)更新ui信 ...

  2. Java NIO使用及原理分析(1-4)(转)

    转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...

  3. 原子类java.util.concurrent.atomic.*原理分析

    原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...

  4. Android中Input型输入设备驱动原理分析(一)

    转自:http://blog.csdn.net/eilianlau/article/details/6969361 话说Android中Event输入设备驱动原理分析还不如说Linux输入子系统呢,反 ...

  5. 转载:AbstractQueuedSynchronizer的介绍和原理分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  6. Camel运行原理分析

    Camel运行原理分析 以一个简单的例子说明一下camel的运行原理,例子本身很简单,目的就是将一个目录下的文件搬运到另一个文件夹,处理器只是将文件(限于文本文件)的内容打印到控制台,首先代码如下: ...

  7. NOR Flash擦写和原理分析

    NOR Flash擦写和原理分析 1. NOR FLASH 的简单介绍 NOR FLASH 是很常见的一种存储芯片,数据掉电不会丢失.NOR FLASH支持Execute On Chip,即程序可以直 ...

  8. 使用AsyncTask异步更新UI界面及原理分析

    概述: AsyncTask是在Android SDK 1.5之后推出的一个方便编写后台线程与UI线程交互的辅助类.AsyncTask的内部实现是一个线程池,所有提交的异步任务都会在这个线程池中的工作线 ...

  9. (转)Android 系统 root 破解原理分析

    现在Android系统的root破解基本上成为大家的必备技能!网上也有很多中一键破解的软件,使root破解越来越容易.但是你思考过root破解的 原理吗?root破解的本质是什么呢?难道是利用了Lin ...

随机推荐

  1. PHP的高效率写法

    1.尽量静态化: 如果一个方法能被静态,那就声明它为静态的,速度可提高1/4,甚至我测试的时候,这个提高了近三倍. 当然了,这个测试方法需要在十万级以上次执行,效果才明显. 其实静态方法和非静态方法的 ...

  2. H5测试(转载)

    可能有些朋友不明白啥是H5,但其实生活中我们经常会碰到. 比如,你经常收到的朋友虐狗第一式—结婚请贴. 你的朋友圈,可能会经常看到宝妈们虐狗第二式—晒可爱宝宝的相册. 你有可能也收到过这样,非常直观, ...

  3. consonant_摩擦音_咬舌音

    consonant_摩擦音_咬舌音_[θ]和[ð].[h] 咬舌音:咬住舌尖发音. [θ]:牙齿咬住舌尖,送气,气流摩擦发出声音,声带不振动: faith.thank.healthy.both.th ...

  4. fiddler请求报文的headers属性详解

    fiddler请求报文的headers属性详解 headers的属性包含以下几部分. (1)Cache头域 在Cache头域中,通常会出现以下属性. 1. Cache-Control 用来指定Resp ...

  5. 浅谈ConcurrentHashMap实现原理

    我们都知道HashTable是线程安全的类,因为使用了Synchronized来锁整张Hash表来实现线程安全,让线程独占: ConcurrentHashMap的锁分离技术就是用多个锁来控制对Hash ...

  6. LeetCode二叉树实现

    LeetCode二叉树实现 # 定义二叉树 class TreeNode: def __init__(self, x): self.val = x self.left = None self.righ ...

  7. C++中引用的本质分析

    引用的意义 引用作为变量别名而存在,因此在一些场合可以代替指针 引用相对于指针来说具有更好的可读性和实用性 swap函数的实现对比: void swap(int* a, int* b) { int t ...

  8. PHP.47-TP框架商城应用实例-后台22-权限管理-角色和管理员的关系

    角色和管理员的关系 角色功能 管理员功能 角色与管理的关联要通过管理-角色表进行{多对多} /********* 管理-角色表 *********/ drop if exists p39_admin_ ...

  9. PostgreSQL 使用总结

    1. USING的使用 USING是个缩写的概念:它接收一个用逗号分隔的字段名字列表, 这些字段必须是连接表共有的,最终形成一个连接条件,表示这些字段对必须相同. USING (a, b, c) 等效 ...

  10. CC3200底板测试-烧写CC3200-LAUNCHXL

    1. 拿到板子,先研究一下几个跳线帽的作用.我在底板上测到VCC_DCDC_3V3和VCC_BRD之间应该有一个跳线帽的,但是在原理上找不到. 2. LED灯的用途,测试的时候,发现这个灯有时候亮,有 ...