[转]hybird原理简介

注:本文原创乃58集团杜同学,首先发表于58集团企业号:https://qy.weixin.qq.com/cgi-bin/wap_getnewsmsg?action=get&__biz=MzA3NDc2Mjg3Nw==&mixuin=MjI0NjI2OTE0Njc0MjQ4OTc4NQ==&mid=10000545&idx=1&sn=6d04a17e1144d9788fefe58c38fe5123&pass_ticket=191jYHevUo4BUdYXnbiOSdf047sVEN89aje9ToTcCijALboWYiw4gr77guGRfPCj


移动端app是一种C/S模式的软件,需要通过发布新版本来改变app中的内容,而iOS的审核周期比较长,会造成更新不及时的局面。Hybrid app是混合native app和webapp的一种混合应用,可以使native和web互补,在某些比较稳定的界面使用native开发,保持良好的体验和性能,在变动比较大的页面使用web页代替,保证灵活性,并且可以跨平台开发。目前很多主流的移动应用都是采用Hybrid的方式开发的。在混合模式中,如何使web页的javascript脚本程序与native程序进行通信是一个很重要的话题。

原理
在iOS中,系统为我们封装了UIWebView类用于展示web内容。UIWebView的stringByEvaluatingJavaScriptFromString:方法使得native可以直接调用javascript。该方法接收javascript脚本字符串,当native调用该方法时,web view对象会直接在自己的web页面中执行传入的脚本指令。stringByEvaluatingJavaScriptFromString:方法让native调用javascript方法变得十分简单直接。

然而,UIWebView并没有提供可以让web页面直接调用native方法的接口。我们无法像native调用web的方式那样简单直接地实现web对native的调用,只能采用一些“曲线救国”的方式。实际应用最多的方案是web页面向web view发送带标识的url请求,native拦截到url以后进行检查,将带标识的url请求当作对native功能的调用,不带标识的url当作正常的请求处理。

交互方案
目前,通过发送请求的方式实现javascript调用native的常用的方案有两个:ajax和iFrame。

使用Ajax的方案是通过创建一个XMLHttpRequest对象,使用该对象发送ajax请求实现的。发送请求的javascript代码如下:

使用iFrame的方案是通过向dom tree中一个iFrame对象触发加载请求。发送请求的javascript代码如下:

除了这两种常用的方法之外,其实任何可以触发请求的方式都可以做hybrid交互的一种方案。如下面的这三种:超链接、图片源文件和加载新窗口。

谈到发送请求,最容易想到的方式就是超链接。Javascript可以通过为window.location.href赋一个url触发一个超链接请求。相应的javascript代码如下:

通过设置image对象的src属性也可以触发请求,通过测试发现,在iOS中,只要创建了img元素,并且设置其src元素就会触发请求。使用img的src属性发送请求相应的javascript代码如下:

另外,window.open()方法也可以发送请求。Window.open()方法是在浏览器中打开一个新的窗口,窗口内加载一个新的页面。使用window.open()方法发送请求的javascript代码如下:

理论上来说,上面的几种方法都可以作为javascript调用native功能的方案。但是这些方法并不是为此而设计的,所以可能会在某些方面有局限。下面就逐一分析每一种方案的一些优势和不足。不过在此之前,需要先介绍一下native拦截web请求的两种方式以及关于同步异步的一些说明。

URL拦截方式
Native拦截web view发送的url有两种方式,一个是通过UIWebView的代理方法,另一个是通过NSURLCache。

UIWebView为我们提供了一些代理方法,每当web页面发生一些事件时,系统会调用web view代理对象的相应代理方法,并将一些需要的参数传入。我们只需将web view对象的代理设置为自定义的一个类,并在自定义类中实现这些代理方法,就能监听到相应的web事件。在UIWebView的代理方法中,有一个名为webView: shouldStartLoadWithRequest: navigationType:方法,传入发起请求的web view对象、请求的request对象和一个UIWebViewNavigationType对象,返回一个BOOL值,该方法的作用是告知web view是否允许本次请求。每当web页面企图发送url请求新页面时,该代理方法就会被调用,如果该方法返回YES,webview会触发请求操作,后续的代理方法会一一被调起,否则web view终止本次请求。webView: shouldStartLoadWithRequest: navigationType:代理方法很合适作为区分native功能调用和真实url请求的地方。

NSURLCache是iOS中管理缓存的类,该类的作用是决定app的网络请求是否使用缓存数据。每个app中都存在一个NSURLCache的单例对象,该单例对象可以拦截app中发起的绝大部分网络请求,UIWebView发出的所有网络请求都会被NSURLCache的cachedResponseForRequest方法拦截到。

这两种拦截方式看似都可以满足我们的需要,但是它们还是有区别的。主要区别如下:

可截获的请求类型不同。UIWebView的代理方法webView: shouldStartLoadWithRequest: navigationType:只能截获web页面的load请求,即只有在web view的url请求是要载入一个新页面时才能被该代理方法截获,并不能截获所有的url请求,但是NSURLCache可以截获UIWebView发起的所有url请求。

可获得的参数不同。注意到UIWebView的代理方法webView: shouldStartLoadWithRequest: navigationType:有一个参数是 web view,即发起请求的web view对象。而NSURLCache则没有这个参数,只能获得request对象。

同步异步
网络上各种资料上都说iFrame是异步请求,无法实现同步,而XMLHttpRequest的同步异步是可以设置的。事实确实是这样,在浏览器中,发起iFrame和异步XMLHttpRequest请求后,javascript代码的执行并不会停止,而是继续向下执行,而发起同步XMLHttpRequest请求后,javascript的代码会停止执行,直到得到请求的结果后才会继续向下执行。这是前端的同步异步的机制。

但是在app中,如果我们在UIWebView的代理方法里拦截了请求,我们会发现,javascript代码被阻塞了,直到处理请求处理完成返回后,javascript才会继续执行,感觉就像是同步执行的一样。这其实是一种错觉,因为app在收到javascript的请求后,如果在主线程进行处理,在处理的这段时间内,主线程会被阻塞,web view并不会继续执行javascript代码。所以虽然发送的是异步请求,但是在UIWebView中调起app代码执行后,由于主线程被阻塞,web view对javascript的执行依然会停止,只有在主线程不被阻塞的情况下,web view才会继续执行javascript代码。而NSURLCache的cachedResponseForRequest方法本身就不在非主线程执行,所以使用NSURLCache进行拦截处理,不会阻塞javascript的执行。如果需要使用UIWebView的代理方法拦截请求,并且不想阻塞javascript的执行,可以采用使用子线程处理请求的方式。

方案分析

Ajax方案分析
在主观理解上,采用ajax的XMLHttpRequest对象发送请求是最合理的方式,因为这个类的的存在就是为了发送自定义请求,而其他的几种方式,虽然都能发送请求,但发送的请求都带有特殊意义。在实验中发现,XMLHttpRequest确实是比较理想的一种方案,它的同步异步方式可控,并且在任意时刻都可以发送请求。但这种方案有两个比较大的缺点,一个是拦截方式问题,另一个是内存消耗问题。由于XMLHttpRequest发送的请求并不是页面加载请求,所以无法使用UIWebView的代理方法进行拦截,导致的问题就是native在收到请求后不易判定是哪个web view发起的请求。另外XMLHttpRequest对象发起请求后无法释放,所以随着请求数量的增加,内存会不断消耗。
iFrame方案分析
iFrame是一种页面加载请求,可以使用UIWebView的代理方法拦截,免去了不易定位web view的麻烦,而且每次发起请求后可以直接释放掉,不会造成内存问题,各方面的表现都比较不错,所以是目前主流的hybrid交互方案。但是在实验中也发现iFrame有一些缺点,首先就是iFrame都是异步请求,无法实现同步的发送请求。另外,使用iFrame发送请求必须在dom ready事件之后。

Domready是javascript中的一个事件,浏览器在得到html文件到将html完全展示在用户面前这段时间内做了很多事情。首先就是要解析html文件,html是一种xml格式的文件,内部包含很多节点,称为dom节点,每一个dom节点都对应dom tree中的一个元素,web页面展示的内容就是由这些节点指定的。浏览器必须在解析完html的body节点后才能知道整个页面的结构,才可以生成对应的dom对象。在浏览器解析完body节点,并且将所有的节点对象创建后组成树状的dom tree时,会触发dom ready事件。在dom ready事件之后,浏览器再去对每一个节点做细节渲染,比如设置样式,加载网络图片等等。

iFrame的方案是通过向dom tree中添加一个iFrame节点触发请求的,在dom tree未生成之前,不可能向其中添加iFrame对象,所以无法通过添加iFrame的方式发送请求。虽然一般情况下web view解读html文档的速度比较快,从收到html页面数据到生成dom tree耗时很短,但某些情况下还是会需要在dom tree形成之前就调用native的功能。面对这种情况,iFrame无法满足需求。
Window.location.href方案分析
Window.location.href是一个超链接请求,可以用UIWebView的代理方法拦截,内存占用也不高,但是这个方法有一个致命的缺点就是,无法同时发送多个请求。在实验中发现,在一次执行中如果发送多条href请求,只有最后一条href请求能被native收到,前面的请求全部丢失。href是网页重定向的实现基础,一般在页面头部加上使用href请求另一个页面的逻辑就可以完成重定向,所以href是不需要等待dom ready后才能发送请求的。
Img.src方案分析
在iOS中,只要创建一个img元素,并且为该元素的src属性赋值,就会触发一个网络请求,但是该请求不是页面加载请求,无法被UIWebView代理方法拦截。在实验中发现,如果在同一次执行中请求了两次完全相同的url,则native只能收到一次请求。这可能是UIWebView对图片元素的请求作了特殊处理所致。
Window.open方案分析
Window.open的表现基本与iFrame的表现一致。可以使用UIWebView代理拦截,并且依赖于dom ready事件。Window.open方法的作用是打开一个新窗口,并在这个新窗口中载入指定的url页面。通过这个方法发送请求实质上与iFrame的方法很像。iFrame本身就是一个新窗口,当iFrame被添加到dom tree上来后,就会主动请求src指定的页面。所以两者的表现基本上是一样的。
对比
下面的表格列举了上述五种方案在拦截方式,是否依赖于domready事件的信息。

总结

综合上面五种方法的表现看来,目前这些使用发送请求的方式实现web对native调用的方案,都有自己的不足之处,而iFrame确实是一个相对比较优秀的方案,可以满足绝大部分的业务需求。目前比较成熟的Hybrid框架基本都是采用的iFrame方式。
WebViewJavascriptBridge是一个轻量级的开源iOS平台下的Hybrid项目,它对javascript与webview之间的相互调用进行了封装,原理分别是UIWebView的stringByEvaluatingJavaScriptFromString方法和iFrame。WebViewJavascriptBridge对两者进行了对等的封装,javascript和native端都持有一个bridge对象,并且可以像bridge中注册方法,另一端通过向bridge中传入方法需要调用的方法名、参数和回调方法,bridge就会自动完成调用的操作。引入了WebViewJavascriptBridge以后,我们无需关注两端相互调用的细节,只需要向bridge中注册相应的方法,并在另一端按规定的格式调用即可,不用关心交互的细节,方便了开发者进行两端通信。但是native端只能向WebViewJavascriptBridge中注册block,并不能注册对象方法。

PhoneGap 是一个免费且开源的开发环境,使开发者可以开发出在Android、Palm、黑莓、iPhone、iTouch及iPad等设备上运行的App。其使用的是HTML和JavaScript等标准的Web开发语言。开发者使用PhoneGap进行开发,可调用加速计、GPS/定位、照相机、声音等功能。PhoneGap的底层原理也是UIWebView的stringByEvaluatingJavaScriptFromString方法和iFrame, phoneGap本身是一个比较完整的hybrid框架,定义了一套Hybrid交互规范,并且封装了很多插件供javascript调用的。使用phoneGap进行开发可以脱离iOS的部分,直接使用前端开发语言开发出iOS的应用程序,phoneGap已经为我们完成了iOS的部分。

PhoneGap是为前端开发人员独立开发移动端app搭建的平台,并不是针对iOS开发者的。另外还有很多类似的开发平台,如appCan、ionic、weX5等等。这些开发平台在两端交互上很多都直接使用了phoneGap,它们关注的焦点都是如何让前端开发者更容易的独立开发出hybrid app,这里就不做过多介绍了。

在iOS 7.0以后,苹果添加了javascriptCore.framework框架,这是一个专门用于javascript与native交互的框架。Javasctip可以直接调用遵从JSExport协议的子协议的类的方法和属性,我们不用再采用发请求的方式来间接完成javascript对native的调用。当下比较火热的reactive native就是在javascriptCore.framework的基础上开发的。目前看来,javascriptCore.framework有着很大的吸引力,但是想在工程中运用,势必要重新构建hybrid框架,带来很大的工作量。这是目前javascriptCore.framework没有普及的一个很重要的原因。相信在不久的将来,javascriptCore.framework必会取代iFrame和ajax这种通过发送请求来调用native方法的交互方式,给我们带来极大的便利。

参考资料
http://blog.devtang.com/2012/03/24/talk-about-uiwebview-and-phonegap/
http://www.cnblogs.com/yswdarren/p/3615458.html
http://www.cocoachina.com/industry/20140623/8919.html
http://www.ionic.wang/article-index-id-57.html
https://github.com/marcuswestin/WebViewJavascriptBridge

你可能感兴趣的:(iOS)