天天看点

Native 与 H5 交互的那些事

hybrid开发模式目前几乎每家公司都有涉及和使用,这种开发模式兼具良好的native用户交互体验的优势与webapp跨平台的优势,而这种 模式,在android中必然需要webview作为载体来展示h5内容和进行交互,而webview的各种安全性、兼容性的问题,我想大多数人与它友谊 的小床已经翻了,特别是4.2版本之前的addjavascriptinterface接口引起的漏洞,可能导致恶意网页通过js方法遍历刚刚通过 addjavascriptinterface注入进来的类的所有方法从中获取到getclass方法,然后通过反射获取到runtime对象,进而调用 runtime对象的exec方法执行一些操作,恶意的js代码如下:

为了避免这个漏洞,即需要限制js代码能够调用到的native方法,官方于是在从4.2开始的版本可以通过为可以被js调用的方法添加@javascriptinterface注解来解决,而之前的版本虽然不能通过这种方法解决,但是可以使用js的prompt方法进行解决,只不过需要和前端协商好一套公共的协议,除此之外,为了避免webview加载任意url,也需要对url进行白名单检测,由于android碎片化太严重,webview也存在兼容性问题,webview的内核也在4.4版本进行了改变,由webkit改为chromium,此外webview还有一个非常明显的问题,就是内存泄露,根本原因就是activity与webview关联后,webview内部的一些操作的执行在新线程中,这些时间无法确定,而可能导致webview一直持有activity的引用,不能回收。下面就谈谈怎样正确安全的让native与h5交互

native与h5怎样安全的进行交互?

要使得h5内的js与native之间安全的相互进行调用,我们除了可以通过添加@javascriptinterface注解来解决(>=4.2),还有通过prompt的方式,不过如果使用官方的方式,这就需要对4.2以下做兼容了,这样使得我们一个app中有两套js与native交互的方式,这样极其不好维护,我们应该只需要一套js与native交互的方式,所以,我们借助js中的prompt方法来实现一套安全的js与native交互的jsbridge框架

1.1 js与native代码相互调用

我们知道如果native需要调用js中的方法,只需要使用webview:loadurl();方法即可直接调用指定js代码,如:

这样就直接调用了js中的setusername方法并把zhengxiaoyong这个名字传到这个方法中去了,接下来就是js自己处理了

而如果js要调用native中的java方法呢?这就需要我们自己实现了,因为我们不采取javascriptinterface的方式,而采取prompt方式

对webview熟悉的同学们应该都知道js中对应的window.alert()、window.confirm()、window.prompt()这三个方法的调用在webchromeclient中都有对应的回调方法,分别为:

onjsalert()、onjsconfirm()、onjsprompt(),对于它们传入的message,都可以在相应的回调方法中接收到,所以,对于js调native方法,我们可以借助这个信道,和前端协定好一段特定规则的message,这个规则中应至少包含这些信息:

所调用native方法所在类的类名

所调用native的方法名

js调用native方法所传入的参数

所以基于这些信息,很容易想到使用http协议的格式来协定规则,如下格式:

对应的我们协定prompt传入message的格式为:

这样以来,前端和app端协商好后,以后前端需要通过js调用native方法来获取一些信息或功能,就只需要按照协议的格式把需要调用的类名、方法名、参数放入对应得位置即可,而我们会在onjsprompt方法中接受到,所以我们根据与前端协定好的协议来进行解析,我们可以用一个uri来包装这段协议,然后通过uri:gethost、getpath、getquery方法获取对应的类名,方法名,参数数据,最后通过反射来调用指定类中指定的方法

而此时会有人问?port是用来干嘛的?params格式是kv还是什么格式?

当然,既然和前端协定好了协议的格式了,那么params肯定也是需要协定好的,可以用kv格式,也可以用一串json字符串表示,为了解析方便,还是建议使用json格式

而port是用来干嘛的呢?

port我们并不会直接操作它,它是由js代码自动生成的,port的作用是为了标识js中的回调function,当js调用native方法时,我们会得到本次调用的port号,我们需要在native方法执行完毕后再把该port、执行的后结果、是否调用成功、调用失败的msg等信息通过调用js的oncomplete方法传入,这时候js凭什么知道你本次返回的信息是哪次调用的结果呢?就是通过port号,因为在js调用native方法时我们会把自动生成的port号和此次回调的function绑定在一起,这样以来native方法返回结果时把port也带过来,就知道是哪次回调该用哪个function方法来处理

自动生成port和绑定function回调的js代码如下:

js代码上已经注释的很清楚了,就不多解释了。

经过上面介绍,那么在native方法执行完成后,当然就需要把结果返回给js了,那么结果的格式又是什么呢?返回给js方法又是什么呢?

没错,还是需要和前端进行协定,建议数据的返回格式为json字符串,基本格式为:

其中定义了一个status,这样的好处是无论在native方法调用成功与否、native方法是否有返回值,js中都可以收到返回的信息,而这个json字符串至少都会包含一个statusjson对象来描述native方法调用的状况

而返回给js的方法自然是上面的oncomplete方法:

ps:rainbowbridge是我的jsbridge框架的名字

至此js调用native的流程就分析完成了,一切都看起来那么美妙,因为,我们自己实现一套js invoke native的主要目的是让js调用native更加安全,同时也只维护一套jsbridge框架更加方便,那么这个安全性表现在哪里了?

我们知道之前原生的方式漏洞就是恶意js代码可能会调用native中的其它方法,那么答案出来了,如果需要让js invoke native保证安全性,只需要限制我们通过反射可调用的方法,所以,在jsbridge框架中,我们需要对js能调用的native方法给予一定的规则,只有符合这些规则js才能调用,而我的规则是:

1、native方法包含public static void 这些修饰符(当然还可能有其它的,如:synchronized)

2、native方法的参数数量和类型只能有这三个:webview、jsonobject、jscallback。为什么要传入这三个参数呢?

2.1、第一个参数是为了提供一个webview对象,以便获取对应context和执行webview的一些方法

2.2、第二个参数就是js中传入过来的参数,这个肯定要的

2.3、第三个参数就是当native方法执行完毕后,把执行后的结果回调给js对应的方法中

所以符合js调用的native方法格式为:

判断js调用的方法是否符合该格式的代码为,符合则存入一个map中供js调用:

对于有返回值的方法,并不需要设置它的返回值,因为方法的结果最后我们是通过jscallback.invokejscallback来进行对js层的回调,比如我贴一个符合该格式的native方法:

js调native代码执行耗时操作情况处理

一般情况下,比如我们通过js调用native方法来获取appname、ossdk版本、imsi号、用户信息等都不会有问题,但是,假如该native方法需要执行一些耗时操作,如:io、sp、bitmap decode、sqlite等,这时为了保护ui的流畅性,我们需要让这些操作执行在异步线程中,待执行完毕再把结果回调给js,而我们可以提供一个线程池来专门处理这些耗时操作,如:

【注】:对于webview,它的方法的调用只能在主线程中调用,当设计到webview的方法调用时,切记不可以放在异步线程中调用,否则就gg了.

js调native流程图

Native 与 H5 交互的那些事

jsbridge效果图

Native 与 H5 交互的那些事

1.2 白名单check

上面我们介绍了jsbridge的基本原理,实现了js与native相互调用,而且还避免了恶意js代码调用native方法的安全问题,通过这样我们保证了js调用native方法的安全性,即js不能随意调用任意native方法,不过,对于webview容器来说,它并不关心所加载的url是js代码还是网页地址,它所做的工作就是执行我们传入的url,而webview加载url的方式有两种:get和post,方式如下:

对于这两种方式,也有不同的应用点,一般get方式用于查,也就是传入的数据不那么重要,比如:商品列表页、商品详情页等,这些传入的数据只是一些商品类的信息。而post方式一般用于改,post传入的数据往往是比较私密的,比如:订单界面、购物车界面等,这些界面只有在把用户的信息post给服务器后,服务器才能正确的返回相应的信息显示在界面上。所以,对于post方式涉及到用户的私密信息,我们总不能给一个url就把私密数据往这个url里面发吧,当然不可能的,这涉及到安全问题,那么就需要一个白名单机制来检查url是否是我们自己的,是我们自己的那么即可以post数据,不是我们自己的那就不post数据,而白名单的定义通常可以以我们自己的域名来判断,搞一个正则表达式,所以我们可以重写webview的posturl方法:

这样就对不是我们自己的url进行了拦截,不把数据发送到不是我们自己的服务器中

至此,白名单的check还没有完成,因为这只是对webview加载url时候做的检查,而在webview内各中链接的跳转、其中有些url还可能被运营商劫持注入了广告,这就有可能在webview容器内的跳转到某些界面后,该界面的url并不是我们自己的,但是它里面有js代码调用native方法来获取一些数据,虽然说js并不能随便调我们的native方法,但是有些我们指定可以被调用的native方法可能有一些获取设备信息、读取文件、获取用户信息等方法,所以,我们也应该在js调用native方法时做一层白名单check,这样才能保证我们的信息安全

所以,白名单检测需要在两个地方进行检测:

1、webview:posturl()前检测url的合法性

2、js调用native方法前检测当前界面url的合法性

具体代码如下:

1.3 移除默认内置接口

webview内置默认也注入了一些接口,如下:

这些接口虽然不会影响用prompt方式实现的js与native交互,但是在使用addjavascriptinterface方式时,有可能有安全问题,最好移除

webview相关

2.1 webview的配置

下面给出webview的通用配置:

其中有一项配置,是在4.4以上版本时设置网页内图片可以自动加载,而4.4以下版本则不可自动加载,原因是4.4webview内核的改变,使得webview的性能更优,所以在4.4以下版本不让图片自动加载,而是先让webview加载网页的其它静态资源:js、css、文本等等,待网页把这些静态资源加载完成后,在onpagefinished方法中再把图片自动加载打开让网页加载图片:

2.2 webview的独立进程

通常来说,webview的使用会带来诸多问题,内存泄露就是最常见的问题,为了避免webview内存泄露,目前最流行的有两种做法:

1、独立进程,简单暴力,不过可能涉及到进程间通信

2、动态添加webview,对传入webview中使用的context使用弱引用,动态添加webview意思在布局创建个viewgroup用来放置webview,activity创建时add进来,在activity停止时remove掉

个人推荐独立进程,好处主要有两点,一是在webviewactivity使用完毕后直接干掉该进程,防止了内存泄露,二是为我们的app主进程减少了额外的内存占用量

使用独立进程还需注意一点,这个进程中在有多个webviewactivity,不能在activity销毁时就干掉进程,不然其它activity也会蹦了,此时应该在该进程创建一个activity的维护集合,集合为空时即可干掉进程

关于webview的销毁,如下:

2.3 webview的兼容性

2.3.1 不同版本硬件加速的问题

2.3.2 不同设备点击webview输入框键盘的不弹起

2.3.3 三星手机硬件加速关闭后导致h5弹出的对话框出现不消失情况

2.3.4 不同版本shouldoverrideurlloading的回调时机

对于shouldoverrideurlloading的加载时机,有些同学经常与onprogresschanged这个方法的加载时机混淆,这两个方法有两点不同:

1、shouldoverrideurlloading只会走get方式的请求,post方式的请求将不会回调这个方法,而onprogresschanged对get和post都会走

2、shouldoverrideurlloading都知道在webview内部点击链接(get)会触发,它在get请求打开界面时也会触发,shouldoverrideurlloading还有一点特殊,就是在按返回键返回到上一个页面时时不会触发的,而onprogresschanged在只要界面更新了都会触发

对于shouldoverrideurlloading的返回值,返回true为剥夺webview对该此请求的控制权,交给应用自己处理,所以webview也不会加载该url了,返回false为webview自己处理

对于shouldoverrideurlloading的调用时机,也会有不同,在3.0以上是会正常调用的,而在3.0以下,并不是每次都会调用,可以在onpagestarted方法中做处理,也没必要了,现在应该都适配4.0以上了

2.3.5 页面重定向导致webview:goback()无效的处理

像一些界面有重定向,比如:淘宝等,需要按多次(>1)才能正常返回,一般都是二次,所以可以把那些具有重定向的界面存入一个集合中,在拦截返回事件中这样处理:

这里处理是在按返回键时,如果上一个界面是重定向界面,则直接调用goback,或者也可以finish当前activity

2.3.6 webview无法加载不信任网页ssl错误的处理

有时我们的webview会加载一些不信任的网页,这时候默认的处理是webview停止加载了,而那些不信任的网页都不是由ca机构信任的,这时候你可以选择继续加载或者让手机内的浏览器来加载:

2.3.7 自定义webview加载出错界面

出错的界面的显示,可以在这个方法中控制:

你可以重新加载一段html专门用来显示错误界面,或者用布局显示一个出错的view,这时候需要把出错的webview内容清除,可以使用:

2.3.8 获取位置权限的处理

如果在webview中有获取地理位置的请求,那么可以直接在代码中默认处理了,没必要弹出一个框框让用户每次都确认:

2.4 打造一个通用的webviewactivity界面

一个通用的webviewactivity当然是样式和webview内部处理的策略都统一样,这里只对样式进行说明,因为webview内部的处理各个公司都不一样,但应该都需要包含这么几点吧:

1、白名单检测

2、url的跳转

3、出错的处理

4、…

一个webviewactivity界面,最主要的就是toolbar标题栏的设计了,因为不同的app的webviewactivity界面toolbar上有不同的icon和操作,比如:分享按钮、刷新按钮、更多按钮,都不一样,既然需要通用,即可让调用者传入某个参数来动态改变这些东西吧,比如传一个toolbarstyle来标识此webviewactivity的风格是什么样的,背景色、字体颜色、图标等,包括点击时的动画效果,作为通用的界面,必须是让调用者简单操作,不可能调用时传入一个图标id还是一个drawable,所以,主要需要用到tint,来对字体、图标的颜色动态改变,代码如下:

h5与native界面互相唤起

对于h5界面,有些操作往往是需要唤起native界面的,比如:h5中的登录按钮,点击后往往唤起native的登录界面来进行登录,而不是直接在h5登录,这样一个app就只需要一套登录了,而我们所做的便是拦截登录按钮的url:

这个规则我们可以在native的activity的intent-filter中的data来定义,如下:

解析url过程是判断scheme、host、path的是否有完全与之匹配的,有则唤起

而native唤h5,其实也是一个url的解析过程,只不过需要配置webviewactivity的intent-filter的data,webviewactivity的scheme配置为http和https

上面说到了h5与native互相调起,其实这个可以在app内做成一套界面跳转的方式,摒弃startactivity,为什么原生的跳转方式不佳?

1、因为原生的跳转需要确定该activity是已经存在的,否则编译将报错,这样带来的问题是不利于协同开发,如:a、b同学分别正在开发项目的两个不同的模块,此时b刚好需要跳a同学的某一个界面,如商品列表页跳商品详情页,这时候b就必须写个todo,待b完成该模块后再写了。而通过url跳转,只需要传入一串url即可

2、原生的跳转activity与目标activity是耦合的,跳转activity完全依赖于目标activity

3、原生的跳转方式不利于管理所传递来的参数,获取参数时需要在跳转activity的地方确定传递了几个参数、什么类型的参数,这样以来跳转的方式多了,就比较混乱了。当然一个原生跳转良好的设计是在目的activity实现一个静态的start方法,其它界面要跳直接调用即可

4、最后一个就是在有参数传递的情况下,每次跳转都要写好多代码啊

而urlrouter框架的实现原理,一种实现是可以维护一套activity与url的映射表,这种方式还是没有摆脱不利于协同开发这个毛病,另外一种是通过一串指定规则的url与manifest中配置的data匹配,具体跳转则是通过intent.setdata()来设置跳转的url,这种方式比较好,不过需要处理下匹配到多个activity时优先选择的问题

====================================分割线================================