[转]带你深入认识webview的交互

目录
文章目录隐藏
  1. 一、适用场景
  2. 二、与 App native 的交互
  3. 三、webview 的进化
  4. 四、任重而道远
  5. 三、webview 的进化
  6. 四、任重而道远
  7. 三、webview 的进化
  8. 四、任重而道远
  9. 三、webview 的进化
  10. 四、任重而道远
  11. 三、webview 的进化
  12. 四、任重而道远
  13. 三、webview 的进化
  14. 四、任重而道远

Webview 是我们前端开发从 PC 端演进到移动端的一个重要载体,现在大家每天使用的 App,webview 都发挥着它的重要性。接下来让我们从 webview 看世界。

一、适用场景

提到应用场景,大家最直观的能想到一些 App 内嵌的页面,为我们提供各种各样的交互,就像下面图片里的这样:

webview 使用场景

其实 webview 的应用场景远远不止这些,其实在一些 PC 的软件里,和我们交互的也是我们的 html 页面,只是穿着 webview 的衣服,衣服太美而我们没有发现他们的真谛。

另外,还有一些网络机顶盒里的交互,也是 webview 在和我们打交道,比如一些早期的 IPTV 里的 EPG 都是运行在 webview 里的,它们基于 webkit 内核,尽管我们使用的交互方式是遥控器。

当然,今天我们会从 native 的角度切入,带大家认识真正的 webview。

二、与 App native 的交互

说了这么多,其实目前使用频率最多的,还是客户端内嵌的 webview,小到我们地铁里用手机看的一篇公众号文章,大到我们使用 App 中的一些重要交互流程,其实都是 webview 打开 m 页去承接的。那么,到底 m 页怎么和 native 去交互的呢?

目前 javascript 和客户端(后面统称 native)交互的常见方式有两种,一种是通过 JSBridge 的方式,另一种是通过 schema 的方式。

1.JSBridge

首先,我们来说说 JSBridge。体现的形式其实就是,当我们在 native 内打开 m 页,native 会在全局的 window 下,为我们注入一个 Bridge。这个 Bridge 里面,会包含我们与 native 交互的各种方法、比如判断第三方 App 是否安装、获取网络信息等等功能。

举个例子:

在 IOS 端内,会将 schema 作为参数传入一个提前定义好的回调函数内,然后执行该回调函数。此回调函数,可以通过得到的 schema 去进行解析,然后定向到 app 内的固定的某个页面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{    
    // 参数 url 即为获取的 schema
    // to do
}

Android 端

在 Android 端内,会稍微麻烦一些,在外部的 m 页,会发起一个 schema 的伪协议链接,系统会去根据这个 schema 去检索,需要被拉起的 App 需要有一个配置文件,大致如下:

<activity android:name =".activity.StartActivity" android:exported = "true">    
    <intent-filter>  
        <action android:name = "android.intent.action.VIEW"/>      
        <category android:name = 
        "android.intent.category.DEFAULT"/>     
        <category android:name = 
        "android.intent.category.BROWSABLE"/>       
        <data android:scheme = "zhuanzhuan"/>    
    </intent-filter>
</activity>

以上面的代码为例,在上面配置中 scheme 为 zhuanzhuan,只要是”zhuanzhuan://”开头的 schema 的链接都会调起配置该 schema 的 Activity(类似上面代码的 StartActivity),此 Activity 会对这个 schema url 做处理,例如:

public class StartActivity extends TempBaseActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        Uri uri = intent.getData();
    }
}

例如上面的代码,可以在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息,如下图:

在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息

这也是我们在第三方 app 内,可以调起自己 app 的原理。当然现在市场上一些 app,为了怕有流量流失,会对 schema 进行限制,只有 plist 白名单里的 schema 才能对应拉起,否则会被直接过滤掉。比如我们的 wx 爸爸,开通白名单后,才可以使用更多的 jsApiList,通过 schema 的拉起就是其中之一,在此不做赘述……:)

三、webview 的进化

对于 webview,要说进化、或者蜕变,让我第一想到的就是 IOS 的 WKWebView 了,每一个事物存在都有它的必然,让我们一起看看这个 super 版的 webview。

1.WKWebView 的出现

目前混合开发已然成为了主流,为了提高体验,WKWebView 在 IOS8 发布时,也随之一起诞生。在这之前 IOS 端一直使用的是 UIWebView。

从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。目前一些一线互联网 app 在 IOS 已经切换到了 WKWebView,所以感觉我们无法拒绝。

整个 WKWebView 的初始化也很简单:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://mybj123.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和 UIWebView 的很像。

2.WKWebView 与 UIWebView 的对比

上面有提到性能的提升,为什么 app 接入 WKWebView 之后,相对比 UIWebView 内存占用小那么多,主要是因为网页的载入和渲染这些耗内存和性能的过程都是由 WKWebView 进程去实现的(WKWebView 是独立于 app 的进程)。如下图:

WKWebView 是独立于 app 的进程

这样,互相进程独立相当于把整个 App 的进程对内存的占用量减少,App 进程会更为稳定。况且,即使页面进程崩溃,体现出来的就是页面白屏或载入失败,不会影响到整个 App 进程的崩溃。

除了上面说的性能以外,WKWebView 会比 UIWebView 多了一个询问过程。在服务器完成响应之后,会询问获取内容是否载入到容器内,在控制上会比 UIWebView 更细粒度一点,也可以在一些通信上更好的和 m 页进行交互。大概流程如下图:

WKWebView 会比 UIWebView 多了一个询问过程

WKWebView 的代理协议为 WKNavigationDelegate,对比 UIWebDelegate 首先跳转询问,就是载入 URL 之前的一次调用,询问开发者是否下载并载入当前 URL,UIWebView 只有一次询问,就是请求之前的询问,而 WKWebView 在 URL 下载完毕之后还会发一次询问,让开发者根据服务器返回的 Web 内容再次做一次确定。

四、任重而道远

前面说到 WKWebView 这么赞,其实开发中也有一些痛点。不同于 UIWebView,WKWebView 很多交互都是异步的,所以在很大程度上,在和 m 页通信的时候,提高了开发成本。

1.cookie

首先就是 cookie 问题,这个目前我认为也是 WKWebView 在业界的一个坑。之前出现过一个问题,就是在 IOS 登陆完成后,马上进入 m 页,会有登录态的 cookie 获取不到的问题。这个问题在 UIWebView 中是不存在的。

经过调研发现,主要问题是 UIWebView 对 cookie 是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,然后在下次请求时,在请求头里会带上相应的 cookie,来做到 m 页和 native 共享 cookie 的值。

但是在 WKWebView 中,则不然。它虽然也会对NSHTTPCookieStorage来写入 cookie,但却不是实时存储的。而且从实际的测试中发现,不同的 IOS 版本,延迟的时间还不一样,无意对 m 页的开发者是一种挑战。同样,发起请求时,也不是实时读取,无法做到和 native 同步,导致页面逻辑出错。

针对这个问题,目前我们转转的解决方法是需要客户端手动干预一下 cookie 的存储。将服务响应的 cookie,持久化到本地,在下次 webview 启动时,读取本地的 cookie 值,手动再去通过 native 往 webview 写入。大致流程如下图:

转转的解决方法是需要客户端手动干预一下 cookie 的存储

当然这也不是很完美的解决方案,因为偶尔还有 spa 的页面路由切换的时候丢失 cookie 的问题。cookie 的问题还需要我们和客户端的同学继续去探索解决。在这里,如果大家有什么好的建议和处理方法欢迎留言,大家一起学习进步。

2.缓存

除了 cookie 以外,WKWebView 的缓存问题,最近我们也在关注。由于 WKWebView 内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是 IOS8 版本,也许是当时刚诞生 WKWebView 的缘故,还很不完善,根本没法操作(当然相信 IOS8 很快会退出历史舞台)。对于一些 m 页的静态资源,偶尔会出现缓存不更新的情况,着实让人头疼。

但在 IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
                                            WKWebsiteDataTypeDiskCache,
                                            WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
                                           modifiedSince:date
                                       completionHandler:^{
}];

至于 IOS8,就只能通过删除文件来解决了,一般 WKWebView 的缓存数据会存储在这个目录里:

~/Library/Caches/BundleID/WebKit/

可通过删除该目录来实现清理缓存。

另外,以上我们说的痛点以外,还有 webview 的通病,就是我们每次首次打开 m 页时,都要有 webview 初始化的过程,那么如何减少初始化 webview 的时间,也是我们可以提高页面打开速度的一个重要环节。

当然,为了提高页面的打开速度,咱们 m 页也可以跟 native 去结合,做一些离线方案,目前转转内部也有一些离线页面的项目有上线,今天就不在此展开。

讲到这里,我们也进入尾声了,也许不久的将来各种新兴的技术会掩盖一些 webview 的光环,像 react-native、小程序、安卓的轻应用开发等等,但是不可否认的是,webview 不会轻易退出历史舞台,我们会把交互做的更好,我们也有情怀。哪有什么岁月静好,只不过有人负重前行

以 UIWebView 为例,在 IOS 中,UIWebView 内发起网络请求时,可以通过 delegate 在 native 层来拦截,然后将捕获的 schema 进行触发对应的功能或业务逻辑(利用 shouldStartLoadWithRequest)。代码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    //获取 scheme url 后自行进行处理
    NSURL *url = [request URL];
    NSString
    *requestString = [[request URL] absoluteString]; 
    return YES;
}

Android 端

在 Android 中,可以使用 shouldoverrideurlloading 来捕获 schema url。代码如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
    //读取到 url 后自行进行分析处理
    //这里注意:如果返回 false,则 WebView 处理链接 url,如果返回 true,代表 WebView 根据程序来执行 url
    return true;
}

上面分别是 IOS 和 Android 简单的 schema 捕获代码,可以在函数中根据自己的需求,执行对应的业务逻辑,来达到想要的功能。


当然,刚才我们提到通过 schema 的方式可以进行跨端交互,那具体如何操作呢?

其实对于 JavaScript,在 webview 里基本是一样的,也是发起一个 schema 的请求,只不过在 native 侧会有些许变化。

首先,给大家普及一个小知识,就是在 natvie 中(包括 IOS 和 Android),会通过 schema 找到相匹配的 App。其中 IOS 不可以重复,就像 appId 一样;安卓可以重复,遇到重复情况时,会弹窗让用户选择其中之一。

那么,有了这个知识点做铺垫,就可以理解,当我们在其他 app 中,像这个 schema 发起请求时,系统底层(IOS&Android)会通过 schema 去找到所匹配的 app,然后将此 App 拉起。拉起 app 后,对应处理如下:

IOS 端

在 IOS 端内,会将 schema 作为参数传入一个提前定义好的回调函数内,然后执行该回调函数。此回调函数,可以通过得到的 schema 去进行解析,然后定向到 app 内的固定的某个页面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{    
    // 参数 url 即为获取的 schema
    // to do
}

Android 端

在 Android 端内,会稍微麻烦一些,在外部的 m 页,会发起一个 schema 的伪协议链接,系统会去根据这个 schema 去检索,需要被拉起的 App 需要有一个配置文件,大致如下:

<activity android:name =".activity.StartActivity" android:exported = "true">    
    <intent-filter>  
        <action android:name = "android.intent.action.VIEW"/>      
        <category android:name = 
        "android.intent.category.DEFAULT"/>     
        <category android:name = 
        "android.intent.category.BROWSABLE"/>       
        <data android:scheme = "zhuanzhuan"/>    
    </intent-filter>
</activity>

以上面的代码为例,在上面配置中 scheme 为 zhuanzhuan,只要是”zhuanzhuan://”开头的 schema 的链接都会调起配置该 schema 的 Activity(类似上面代码的 StartActivity),此 Activity 会对这个 schema url 做处理,例如:

public class StartActivity extends TempBaseActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        Uri uri = intent.getData();
    }
}

例如上面的代码,可以在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息,如下图:

在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息

这也是我们在第三方 app 内,可以调起自己 app 的原理。当然现在市场上一些 app,为了怕有流量流失,会对 schema 进行限制,只有 plist 白名单里的 schema 才能对应拉起,否则会被直接过滤掉。比如我们的 wx 爸爸,开通白名单后,才可以使用更多的 jsApiList,通过 schema 的拉起就是其中之一,在此不做赘述……:)

三、webview 的进化

对于 webview,要说进化、或者蜕变,让我第一想到的就是 IOS 的 WKWebView 了,每一个事物存在都有它的必然,让我们一起看看这个 super 版的 webview。

1.WKWebView 的出现

目前混合开发已然成为了主流,为了提高体验,WKWebView 在 IOS8 发布时,也随之一起诞生。在这之前 IOS 端一直使用的是 UIWebView。

从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。目前一些一线互联网 app 在 IOS 已经切换到了 WKWebView,所以感觉我们无法拒绝。

整个 WKWebView 的初始化也很简单:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://mybj123.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和 UIWebView 的很像。

2.WKWebView 与 UIWebView 的对比

上面有提到性能的提升,为什么 app 接入 WKWebView 之后,相对比 UIWebView 内存占用小那么多,主要是因为网页的载入和渲染这些耗内存和性能的过程都是由 WKWebView 进程去实现的(WKWebView 是独立于 app 的进程)。如下图:

WKWebView 是独立于 app 的进程

这样,互相进程独立相当于把整个 App 的进程对内存的占用量减少,App 进程会更为稳定。况且,即使页面进程崩溃,体现出来的就是页面白屏或载入失败,不会影响到整个 App 进程的崩溃。

除了上面说的性能以外,WKWebView 会比 UIWebView 多了一个询问过程。在服务器完成响应之后,会询问获取内容是否载入到容器内,在控制上会比 UIWebView 更细粒度一点,也可以在一些通信上更好的和 m 页进行交互。大概流程如下图:

WKWebView 会比 UIWebView 多了一个询问过程

WKWebView 的代理协议为 WKNavigationDelegate,对比 UIWebDelegate 首先跳转询问,就是载入 URL 之前的一次调用,询问开发者是否下载并载入当前 URL,UIWebView 只有一次询问,就是请求之前的询问,而 WKWebView 在 URL 下载完毕之后还会发一次询问,让开发者根据服务器返回的 Web 内容再次做一次确定。

四、任重而道远

前面说到 WKWebView 这么赞,其实开发中也有一些痛点。不同于 UIWebView,WKWebView 很多交互都是异步的,所以在很大程度上,在和 m 页通信的时候,提高了开发成本。

1.cookie

首先就是 cookie 问题,这个目前我认为也是 WKWebView 在业界的一个坑。之前出现过一个问题,就是在 IOS 登陆完成后,马上进入 m 页,会有登录态的 cookie 获取不到的问题。这个问题在 UIWebView 中是不存在的。

经过调研发现,主要问题是 UIWebView 对 cookie 是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,然后在下次请求时,在请求头里会带上相应的 cookie,来做到 m 页和 native 共享 cookie 的值。

但是在 WKWebView 中,则不然。它虽然也会对NSHTTPCookieStorage来写入 cookie,但却不是实时存储的。而且从实际的测试中发现,不同的 IOS 版本,延迟的时间还不一样,无意对 m 页的开发者是一种挑战。同样,发起请求时,也不是实时读取,无法做到和 native 同步,导致页面逻辑出错。

针对这个问题,目前我们转转的解决方法是需要客户端手动干预一下 cookie 的存储。将服务响应的 cookie,持久化到本地,在下次 webview 启动时,读取本地的 cookie 值,手动再去通过 native 往 webview 写入。大致流程如下图:

转转的解决方法是需要客户端手动干预一下 cookie 的存储

当然这也不是很完美的解决方案,因为偶尔还有 spa 的页面路由切换的时候丢失 cookie 的问题。cookie 的问题还需要我们和客户端的同学继续去探索解决。在这里,如果大家有什么好的建议和处理方法欢迎留言,大家一起学习进步。

2.缓存

除了 cookie 以外,WKWebView 的缓存问题,最近我们也在关注。由于 WKWebView 内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是 IOS8 版本,也许是当时刚诞生 WKWebView 的缘故,还很不完善,根本没法操作(当然相信 IOS8 很快会退出历史舞台)。对于一些 m 页的静态资源,偶尔会出现缓存不更新的情况,着实让人头疼。

但在 IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
                                            WKWebsiteDataTypeDiskCache,
                                            WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
                                           modifiedSince:date
                                       completionHandler:^{
}];

至于 IOS8,就只能通过删除文件来解决了,一般 WKWebView 的缓存数据会存储在这个目录里:

~/Library/Caches/BundleID/WebKit/

可通过删除该目录来实现清理缓存。

另外,以上我们说的痛点以外,还有 webview 的通病,就是我们每次首次打开 m 页时,都要有 webview 初始化的过程,那么如何减少初始化 webview 的时间,也是我们可以提高页面打开速度的一个重要环节。

当然,为了提高页面的打开速度,咱们 m 页也可以跟 native 去结合,做一些离线方案,目前转转内部也有一些离线页面的项目有上线,今天就不在此展开。

讲到这里,我们也进入尾声了,也许不久的将来各种新兴的技术会掩盖一些 webview 的光环,像 react-native、小程序、安卓的轻应用开发等等,但是不可否认的是,webview 不会轻易退出历史舞台,我们会把交互做的更好,我们也有情怀。哪有什么岁月静好,只不过有人负重前行

如果说 Bridge 的方式是只能在 native 内部交互,那么 schame url 的不紧可以在 native 内交互,也是可以跨 app 来交互的。schema 也是目前我们转转使用的主要方式,它类似一个伪协议的链接(也可以叫做统跳协议),比如:

schema://path?param=abc

在 webview 里,当 m 页发起 schema 请求时,native 端会去进行捕获。这里可以顺带给大家普及一下 IOS 和 Android 的知识,具体如下:

IOS 端

以 UIWebView 为例,在 IOS 中,UIWebView 内发起网络请求时,可以通过 delegate 在 native 层来拦截,然后将捕获的 schema 进行触发对应的功能或业务逻辑(利用 shouldStartLoadWithRequest)。代码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    //获取 scheme url 后自行进行处理
    NSURL *url = [request URL];
    NSString
    *requestString = [[request URL] absoluteString]; 
    return YES;
}

Android 端

在 Android 中,可以使用 shouldoverrideurlloading 来捕获 schema url。代码如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
    //读取到 url 后自行进行分析处理
    //这里注意:如果返回 false,则 WebView 处理链接 url,如果返回 true,代表 WebView 根据程序来执行 url
    return true;
}

上面分别是 IOS 和 Android 简单的 schema 捕获代码,可以在函数中根据自己的需求,执行对应的业务逻辑,来达到想要的功能。


当然,刚才我们提到通过 schema 的方式可以进行跨端交互,那具体如何操作呢?

其实对于 JavaScript,在 webview 里基本是一样的,也是发起一个 schema 的请求,只不过在 native 侧会有些许变化。

首先,给大家普及一个小知识,就是在 natvie 中(包括 IOS 和 Android),会通过 schema 找到相匹配的 App。其中 IOS 不可以重复,就像 appId 一样;安卓可以重复,遇到重复情况时,会弹窗让用户选择其中之一。

那么,有了这个知识点做铺垫,就可以理解,当我们在其他 app 中,像这个 schema 发起请求时,系统底层(IOS&Android)会通过 schema 去找到所匹配的 app,然后将此 App 拉起。拉起 app 后,对应处理如下:

IOS 端

在 IOS 端内,会将 schema 作为参数传入一个提前定义好的回调函数内,然后执行该回调函数。此回调函数,可以通过得到的 schema 去进行解析,然后定向到 app 内的固定的某个页面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{    
    // 参数 url 即为获取的 schema
    // to do
}

Android 端

在 Android 端内,会稍微麻烦一些,在外部的 m 页,会发起一个 schema 的伪协议链接,系统会去根据这个 schema 去检索,需要被拉起的 App 需要有一个配置文件,大致如下:

<activity android:name =".activity.StartActivity" android:exported = "true">    
    <intent-filter>  
        <action android:name = "android.intent.action.VIEW"/>      
        <category android:name = 
        "android.intent.category.DEFAULT"/>     
        <category android:name = 
        "android.intent.category.BROWSABLE"/>       
        <data android:scheme = "zhuanzhuan"/>    
    </intent-filter>
</activity>

以上面的代码为例,在上面配置中 scheme 为 zhuanzhuan,只要是”zhuanzhuan://”开头的 schema 的链接都会调起配置该 schema 的 Activity(类似上面代码的 StartActivity),此 Activity 会对这个 schema url 做处理,例如:

public class StartActivity extends TempBaseActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        Uri uri = intent.getData();
    }
}

例如上面的代码,可以在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息,如下图:

在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息

这也是我们在第三方 app 内,可以调起自己 app 的原理。当然现在市场上一些 app,为了怕有流量流失,会对 schema 进行限制,只有 plist 白名单里的 schema 才能对应拉起,否则会被直接过滤掉。比如我们的 wx 爸爸,开通白名单后,才可以使用更多的 jsApiList,通过 schema 的拉起就是其中之一,在此不做赘述……:)

三、webview 的进化

对于 webview,要说进化、或者蜕变,让我第一想到的就是 IOS 的 WKWebView 了,每一个事物存在都有它的必然,让我们一起看看这个 super 版的 webview。

1.WKWebView 的出现

目前混合开发已然成为了主流,为了提高体验,WKWebView 在 IOS8 发布时,也随之一起诞生。在这之前 IOS 端一直使用的是 UIWebView。

从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。目前一些一线互联网 app 在 IOS 已经切换到了 WKWebView,所以感觉我们无法拒绝。

整个 WKWebView 的初始化也很简单:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://mybj123.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和 UIWebView 的很像。

2.WKWebView 与 UIWebView 的对比

上面有提到性能的提升,为什么 app 接入 WKWebView 之后,相对比 UIWebView 内存占用小那么多,主要是因为网页的载入和渲染这些耗内存和性能的过程都是由 WKWebView 进程去实现的(WKWebView 是独立于 app 的进程)。如下图:

WKWebView 是独立于 app 的进程

这样,互相进程独立相当于把整个 App 的进程对内存的占用量减少,App 进程会更为稳定。况且,即使页面进程崩溃,体现出来的就是页面白屏或载入失败,不会影响到整个 App 进程的崩溃。

除了上面说的性能以外,WKWebView 会比 UIWebView 多了一个询问过程。在服务器完成响应之后,会询问获取内容是否载入到容器内,在控制上会比 UIWebView 更细粒度一点,也可以在一些通信上更好的和 m 页进行交互。大概流程如下图:

WKWebView 会比 UIWebView 多了一个询问过程

WKWebView 的代理协议为 WKNavigationDelegate,对比 UIWebDelegate 首先跳转询问,就是载入 URL 之前的一次调用,询问开发者是否下载并载入当前 URL,UIWebView 只有一次询问,就是请求之前的询问,而 WKWebView 在 URL 下载完毕之后还会发一次询问,让开发者根据服务器返回的 Web 内容再次做一次确定。

四、任重而道远

前面说到 WKWebView 这么赞,其实开发中也有一些痛点。不同于 UIWebView,WKWebView 很多交互都是异步的,所以在很大程度上,在和 m 页通信的时候,提高了开发成本。

1.cookie

首先就是 cookie 问题,这个目前我认为也是 WKWebView 在业界的一个坑。之前出现过一个问题,就是在 IOS 登陆完成后,马上进入 m 页,会有登录态的 cookie 获取不到的问题。这个问题在 UIWebView 中是不存在的。

经过调研发现,主要问题是 UIWebView 对 cookie 是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,然后在下次请求时,在请求头里会带上相应的 cookie,来做到 m 页和 native 共享 cookie 的值。

但是在 WKWebView 中,则不然。它虽然也会对NSHTTPCookieStorage来写入 cookie,但却不是实时存储的。而且从实际的测试中发现,不同的 IOS 版本,延迟的时间还不一样,无意对 m 页的开发者是一种挑战。同样,发起请求时,也不是实时读取,无法做到和 native 同步,导致页面逻辑出错。

针对这个问题,目前我们转转的解决方法是需要客户端手动干预一下 cookie 的存储。将服务响应的 cookie,持久化到本地,在下次 webview 启动时,读取本地的 cookie 值,手动再去通过 native 往 webview 写入。大致流程如下图:

转转的解决方法是需要客户端手动干预一下 cookie 的存储

当然这也不是很完美的解决方案,因为偶尔还有 spa 的页面路由切换的时候丢失 cookie 的问题。cookie 的问题还需要我们和客户端的同学继续去探索解决。在这里,如果大家有什么好的建议和处理方法欢迎留言,大家一起学习进步。

2.缓存

除了 cookie 以外,WKWebView 的缓存问题,最近我们也在关注。由于 WKWebView 内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是 IOS8 版本,也许是当时刚诞生 WKWebView 的缘故,还很不完善,根本没法操作(当然相信 IOS8 很快会退出历史舞台)。对于一些 m 页的静态资源,偶尔会出现缓存不更新的情况,着实让人头疼。

但在 IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
                                            WKWebsiteDataTypeDiskCache,
                                            WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
                                           modifiedSince:date
                                       completionHandler:^{
}];

至于 IOS8,就只能通过删除文件来解决了,一般 WKWebView 的缓存数据会存储在这个目录里:

~/Library/Caches/BundleID/WebKit/

可通过删除该目录来实现清理缓存。

另外,以上我们说的痛点以外,还有 webview 的通病,就是我们每次首次打开 m 页时,都要有 webview 初始化的过程,那么如何减少初始化 webview 的时间,也是我们可以提高页面打开速度的一个重要环节。

当然,为了提高页面的打开速度,咱们 m 页也可以跟 native 去结合,做一些离线方案,目前转转内部也有一些离线页面的项目有上线,今天就不在此展开。

讲到这里,我们也进入尾声了,也许不久的将来各种新兴的技术会掩盖一些 webview 的光环,像 react-native、小程序、安卓的轻应用开发等等,但是不可否认的是,webview 不会轻易退出历史舞台,我们会把交互做的更好,我们也有情怀。哪有什么岁月静好,只不过有人负重前行

在 Android 中,需要通过 addJavascriptInterface 来注册

class JSBridge
{    
    @JavascriptInterface //注意这里的注解。出于安全的考虑,4.2 之后强制要求,不然无法从 Javascript 中发起调用
    public void getNetInfomation(){  
        // to do
    };
}
webView.addJavascriptInterface(new JSBridge();, "JSBridge");

2.Schema url

如果说 Bridge 的方式是只能在 native 内部交互,那么 schame url 的不紧可以在 native 内交互,也是可以跨 app 来交互的。schema 也是目前我们转转使用的主要方式,它类似一个伪协议的链接(也可以叫做统跳协议),比如:

schema://path?param=abc

在 webview 里,当 m 页发起 schema 请求时,native 端会去进行捕获。这里可以顺带给大家普及一下 IOS 和 Android 的知识,具体如下:

IOS 端

以 UIWebView 为例,在 IOS 中,UIWebView 内发起网络请求时,可以通过 delegate 在 native 层来拦截,然后将捕获的 schema 进行触发对应的功能或业务逻辑(利用 shouldStartLoadWithRequest)。代码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    //获取 scheme url 后自行进行处理
    NSURL *url = [request URL];
    NSString
    *requestString = [[request URL] absoluteString]; 
    return YES;
}

Android 端

在 Android 中,可以使用 shouldoverrideurlloading 来捕获 schema url。代码如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
    //读取到 url 后自行进行分析处理
    //这里注意:如果返回 false,则 WebView 处理链接 url,如果返回 true,代表 WebView 根据程序来执行 url
    return true;
}

上面分别是 IOS 和 Android 简单的 schema 捕获代码,可以在函数中根据自己的需求,执行对应的业务逻辑,来达到想要的功能。


当然,刚才我们提到通过 schema 的方式可以进行跨端交互,那具体如何操作呢?

其实对于 JavaScript,在 webview 里基本是一样的,也是发起一个 schema 的请求,只不过在 native 侧会有些许变化。

首先,给大家普及一个小知识,就是在 natvie 中(包括 IOS 和 Android),会通过 schema 找到相匹配的 App。其中 IOS 不可以重复,就像 appId 一样;安卓可以重复,遇到重复情况时,会弹窗让用户选择其中之一。

那么,有了这个知识点做铺垫,就可以理解,当我们在其他 app 中,像这个 schema 发起请求时,系统底层(IOS&Android)会通过 schema 去找到所匹配的 app,然后将此 App 拉起。拉起 app 后,对应处理如下:

IOS 端

在 IOS 端内,会将 schema 作为参数传入一个提前定义好的回调函数内,然后执行该回调函数。此回调函数,可以通过得到的 schema 去进行解析,然后定向到 app 内的固定的某个页面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{    
    // 参数 url 即为获取的 schema
    // to do
}

Android 端

在 Android 端内,会稍微麻烦一些,在外部的 m 页,会发起一个 schema 的伪协议链接,系统会去根据这个 schema 去检索,需要被拉起的 App 需要有一个配置文件,大致如下:

<activity android:name =".activity.StartActivity" android:exported = "true">    
    <intent-filter>  
        <action android:name = "android.intent.action.VIEW"/>      
        <category android:name = 
        "android.intent.category.DEFAULT"/>     
        <category android:name = 
        "android.intent.category.BROWSABLE"/>       
        <data android:scheme = "zhuanzhuan"/>    
    </intent-filter>
</activity>

以上面的代码为例,在上面配置中 scheme 为 zhuanzhuan,只要是”zhuanzhuan://”开头的 schema 的链接都会调起配置该 schema 的 Activity(类似上面代码的 StartActivity),此 Activity 会对这个 schema url 做处理,例如:

public class StartActivity extends TempBaseActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        Uri uri = intent.getData();
    }
}

例如上面的代码,可以在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息,如下图:

在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息

这也是我们在第三方 app 内,可以调起自己 app 的原理。当然现在市场上一些 app,为了怕有流量流失,会对 schema 进行限制,只有 plist 白名单里的 schema 才能对应拉起,否则会被直接过滤掉。比如我们的 wx 爸爸,开通白名单后,才可以使用更多的 jsApiList,通过 schema 的拉起就是其中之一,在此不做赘述……:)

三、webview 的进化

对于 webview,要说进化、或者蜕变,让我第一想到的就是 IOS 的 WKWebView 了,每一个事物存在都有它的必然,让我们一起看看这个 super 版的 webview。

1.WKWebView 的出现

目前混合开发已然成为了主流,为了提高体验,WKWebView 在 IOS8 发布时,也随之一起诞生。在这之前 IOS 端一直使用的是 UIWebView。

从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。目前一些一线互联网 app 在 IOS 已经切换到了 WKWebView,所以感觉我们无法拒绝。

整个 WKWebView 的初始化也很简单:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://mybj123.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和 UIWebView 的很像。

2.WKWebView 与 UIWebView 的对比

上面有提到性能的提升,为什么 app 接入 WKWebView 之后,相对比 UIWebView 内存占用小那么多,主要是因为网页的载入和渲染这些耗内存和性能的过程都是由 WKWebView 进程去实现的(WKWebView 是独立于 app 的进程)。如下图:

WKWebView 是独立于 app 的进程

这样,互相进程独立相当于把整个 App 的进程对内存的占用量减少,App 进程会更为稳定。况且,即使页面进程崩溃,体现出来的就是页面白屏或载入失败,不会影响到整个 App 进程的崩溃。

除了上面说的性能以外,WKWebView 会比 UIWebView 多了一个询问过程。在服务器完成响应之后,会询问获取内容是否载入到容器内,在控制上会比 UIWebView 更细粒度一点,也可以在一些通信上更好的和 m 页进行交互。大概流程如下图:

WKWebView 会比 UIWebView 多了一个询问过程

WKWebView 的代理协议为 WKNavigationDelegate,对比 UIWebDelegate 首先跳转询问,就是载入 URL 之前的一次调用,询问开发者是否下载并载入当前 URL,UIWebView 只有一次询问,就是请求之前的询问,而 WKWebView 在 URL 下载完毕之后还会发一次询问,让开发者根据服务器返回的 Web 内容再次做一次确定。

四、任重而道远

前面说到 WKWebView 这么赞,其实开发中也有一些痛点。不同于 UIWebView,WKWebView 很多交互都是异步的,所以在很大程度上,在和 m 页通信的时候,提高了开发成本。

1.cookie

首先就是 cookie 问题,这个目前我认为也是 WKWebView 在业界的一个坑。之前出现过一个问题,就是在 IOS 登陆完成后,马上进入 m 页,会有登录态的 cookie 获取不到的问题。这个问题在 UIWebView 中是不存在的。

经过调研发现,主要问题是 UIWebView 对 cookie 是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,然后在下次请求时,在请求头里会带上相应的 cookie,来做到 m 页和 native 共享 cookie 的值。

但是在 WKWebView 中,则不然。它虽然也会对NSHTTPCookieStorage来写入 cookie,但却不是实时存储的。而且从实际的测试中发现,不同的 IOS 版本,延迟的时间还不一样,无意对 m 页的开发者是一种挑战。同样,发起请求时,也不是实时读取,无法做到和 native 同步,导致页面逻辑出错。

针对这个问题,目前我们转转的解决方法是需要客户端手动干预一下 cookie 的存储。将服务响应的 cookie,持久化到本地,在下次 webview 启动时,读取本地的 cookie 值,手动再去通过 native 往 webview 写入。大致流程如下图:

转转的解决方法是需要客户端手动干预一下 cookie 的存储

当然这也不是很完美的解决方案,因为偶尔还有 spa 的页面路由切换的时候丢失 cookie 的问题。cookie 的问题还需要我们和客户端的同学继续去探索解决。在这里,如果大家有什么好的建议和处理方法欢迎留言,大家一起学习进步。

2.缓存

除了 cookie 以外,WKWebView 的缓存问题,最近我们也在关注。由于 WKWebView 内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是 IOS8 版本,也许是当时刚诞生 WKWebView 的缘故,还很不完善,根本没法操作(当然相信 IOS8 很快会退出历史舞台)。对于一些 m 页的静态资源,偶尔会出现缓存不更新的情况,着实让人头疼。

但在 IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
                                            WKWebsiteDataTypeDiskCache,
                                            WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
                                           modifiedSince:date
                                       completionHandler:^{
}];

至于 IOS8,就只能通过删除文件来解决了,一般 WKWebView 的缓存数据会存储在这个目录里:

~/Library/Caches/BundleID/WebKit/

可通过删除该目录来实现清理缓存。

另外,以上我们说的痛点以外,还有 webview 的通病,就是我们每次首次打开 m 页时,都要有 webview 初始化的过程,那么如何减少初始化 webview 的时间,也是我们可以提高页面打开速度的一个重要环节。

当然,为了提高页面的打开速度,咱们 m 页也可以跟 native 去结合,做一些离线方案,目前转转内部也有一些离线页面的项目有上线,今天就不在此展开。

讲到这里,我们也进入尾声了,也许不久的将来各种新兴的技术会掩盖一些 webview 的光环,像 react-native、小程序、安卓的轻应用开发等等,但是不可否认的是,webview 不会轻易退出历史舞台,我们会把交互做的更好,我们也有情怀。哪有什么岁月静好,只不过有人负重前行

在 IOS 中,主要使用 WebViewJavascriptBridge 来注册,可以参考 Github WebViewJavascriptBridge

jsBridge = [WebViewJavascriptBridge bridgeForWebView:webView];
...
[jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// to do
}];

Android

在 Android 中,需要通过 addJavascriptInterface 来注册

class JSBridge
{    
    @JavascriptInterface //注意这里的注解。出于安全的考虑,4.2 之后强制要求,不然无法从 Javascript 中发起调用
    public void getNetInfomation(){  
        // to do
    };
}
webView.addJavascriptInterface(new JSBridge();, "JSBridge");

2.Schema url

如果说 Bridge 的方式是只能在 native 内部交互,那么 schame url 的不紧可以在 native 内交互,也是可以跨 app 来交互的。schema 也是目前我们转转使用的主要方式,它类似一个伪协议的链接(也可以叫做统跳协议),比如:

schema://path?param=abc

在 webview 里,当 m 页发起 schema 请求时,native 端会去进行捕获。这里可以顺带给大家普及一下 IOS 和 Android 的知识,具体如下:

IOS 端

以 UIWebView 为例,在 IOS 中,UIWebView 内发起网络请求时,可以通过 delegate 在 native 层来拦截,然后将捕获的 schema 进行触发对应的功能或业务逻辑(利用 shouldStartLoadWithRequest)。代码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    //获取 scheme url 后自行进行处理
    NSURL *url = [request URL];
    NSString
    *requestString = [[request URL] absoluteString]; 
    return YES;
}

Android 端

在 Android 中,可以使用 shouldoverrideurlloading 来捕获 schema url。代码如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
    //读取到 url 后自行进行分析处理
    //这里注意:如果返回 false,则 WebView 处理链接 url,如果返回 true,代表 WebView 根据程序来执行 url
    return true;
}

上面分别是 IOS 和 Android 简单的 schema 捕获代码,可以在函数中根据自己的需求,执行对应的业务逻辑,来达到想要的功能。


当然,刚才我们提到通过 schema 的方式可以进行跨端交互,那具体如何操作呢?

其实对于 JavaScript,在 webview 里基本是一样的,也是发起一个 schema 的请求,只不过在 native 侧会有些许变化。

首先,给大家普及一个小知识,就是在 natvie 中(包括 IOS 和 Android),会通过 schema 找到相匹配的 App。其中 IOS 不可以重复,就像 appId 一样;安卓可以重复,遇到重复情况时,会弹窗让用户选择其中之一。

那么,有了这个知识点做铺垫,就可以理解,当我们在其他 app 中,像这个 schema 发起请求时,系统底层(IOS&Android)会通过 schema 去找到所匹配的 app,然后将此 App 拉起。拉起 app 后,对应处理如下:

IOS 端

在 IOS 端内,会将 schema 作为参数传入一个提前定义好的回调函数内,然后执行该回调函数。此回调函数,可以通过得到的 schema 去进行解析,然后定向到 app 内的固定的某个页面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{    
    // 参数 url 即为获取的 schema
    // to do
}

Android 端

在 Android 端内,会稍微麻烦一些,在外部的 m 页,会发起一个 schema 的伪协议链接,系统会去根据这个 schema 去检索,需要被拉起的 App 需要有一个配置文件,大致如下:

<activity android:name =".activity.StartActivity" android:exported = "true">    
    <intent-filter>  
        <action android:name = "android.intent.action.VIEW"/>      
        <category android:name = 
        "android.intent.category.DEFAULT"/>     
        <category android:name = 
        "android.intent.category.BROWSABLE"/>       
        <data android:scheme = "zhuanzhuan"/>    
    </intent-filter>
</activity>

以上面的代码为例,在上面配置中 scheme 为 zhuanzhuan,只要是”zhuanzhuan://”开头的 schema 的链接都会调起配置该 schema 的 Activity(类似上面代码的 StartActivity),此 Activity 会对这个 schema url 做处理,例如:

public class StartActivity extends TempBaseActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        Uri uri = intent.getData();
    }
}

例如上面的代码,可以在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息,如下图:

在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息

这也是我们在第三方 app 内,可以调起自己 app 的原理。当然现在市场上一些 app,为了怕有流量流失,会对 schema 进行限制,只有 plist 白名单里的 schema 才能对应拉起,否则会被直接过滤掉。比如我们的 wx 爸爸,开通白名单后,才可以使用更多的 jsApiList,通过 schema 的拉起就是其中之一,在此不做赘述……:)

三、webview 的进化

对于 webview,要说进化、或者蜕变,让我第一想到的就是 IOS 的 WKWebView 了,每一个事物存在都有它的必然,让我们一起看看这个 super 版的 webview。

1.WKWebView 的出现

目前混合开发已然成为了主流,为了提高体验,WKWebView 在 IOS8 发布时,也随之一起诞生。在这之前 IOS 端一直使用的是 UIWebView。

从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。目前一些一线互联网 app 在 IOS 已经切换到了 WKWebView,所以感觉我们无法拒绝。

整个 WKWebView 的初始化也很简单:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://mybj123.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和 UIWebView 的很像。

2.WKWebView 与 UIWebView 的对比

上面有提到性能的提升,为什么 app 接入 WKWebView 之后,相对比 UIWebView 内存占用小那么多,主要是因为网页的载入和渲染这些耗内存和性能的过程都是由 WKWebView 进程去实现的(WKWebView 是独立于 app 的进程)。如下图:

WKWebView 是独立于 app 的进程

这样,互相进程独立相当于把整个 App 的进程对内存的占用量减少,App 进程会更为稳定。况且,即使页面进程崩溃,体现出来的就是页面白屏或载入失败,不会影响到整个 App 进程的崩溃。

除了上面说的性能以外,WKWebView 会比 UIWebView 多了一个询问过程。在服务器完成响应之后,会询问获取内容是否载入到容器内,在控制上会比 UIWebView 更细粒度一点,也可以在一些通信上更好的和 m 页进行交互。大概流程如下图:

WKWebView 会比 UIWebView 多了一个询问过程

WKWebView 的代理协议为 WKNavigationDelegate,对比 UIWebDelegate 首先跳转询问,就是载入 URL 之前的一次调用,询问开发者是否下载并载入当前 URL,UIWebView 只有一次询问,就是请求之前的询问,而 WKWebView 在 URL 下载完毕之后还会发一次询问,让开发者根据服务器返回的 Web 内容再次做一次确定。

四、任重而道远

前面说到 WKWebView 这么赞,其实开发中也有一些痛点。不同于 UIWebView,WKWebView 很多交互都是异步的,所以在很大程度上,在和 m 页通信的时候,提高了开发成本。

1.cookie

首先就是 cookie 问题,这个目前我认为也是 WKWebView 在业界的一个坑。之前出现过一个问题,就是在 IOS 登陆完成后,马上进入 m 页,会有登录态的 cookie 获取不到的问题。这个问题在 UIWebView 中是不存在的。

经过调研发现,主要问题是 UIWebView 对 cookie 是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,然后在下次请求时,在请求头里会带上相应的 cookie,来做到 m 页和 native 共享 cookie 的值。

但是在 WKWebView 中,则不然。它虽然也会对NSHTTPCookieStorage来写入 cookie,但却不是实时存储的。而且从实际的测试中发现,不同的 IOS 版本,延迟的时间还不一样,无意对 m 页的开发者是一种挑战。同样,发起请求时,也不是实时读取,无法做到和 native 同步,导致页面逻辑出错。

针对这个问题,目前我们转转的解决方法是需要客户端手动干预一下 cookie 的存储。将服务响应的 cookie,持久化到本地,在下次 webview 启动时,读取本地的 cookie 值,手动再去通过 native 往 webview 写入。大致流程如下图:

转转的解决方法是需要客户端手动干预一下 cookie 的存储

当然这也不是很完美的解决方案,因为偶尔还有 spa 的页面路由切换的时候丢失 cookie 的问题。cookie 的问题还需要我们和客户端的同学继续去探索解决。在这里,如果大家有什么好的建议和处理方法欢迎留言,大家一起学习进步。

2.缓存

除了 cookie 以外,WKWebView 的缓存问题,最近我们也在关注。由于 WKWebView 内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是 IOS8 版本,也许是当时刚诞生 WKWebView 的缘故,还很不完善,根本没法操作(当然相信 IOS8 很快会退出历史舞台)。对于一些 m 页的静态资源,偶尔会出现缓存不更新的情况,着实让人头疼。

但在 IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
                                            WKWebsiteDataTypeDiskCache,
                                            WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
                                           modifiedSince:date
                                       completionHandler:^{
}];

至于 IOS8,就只能通过删除文件来解决了,一般 WKWebView 的缓存数据会存储在这个目录里:

~/Library/Caches/BundleID/WebKit/

可通过删除该目录来实现清理缓存。

另外,以上我们说的痛点以外,还有 webview 的通病,就是我们每次首次打开 m 页时,都要有 webview 初始化的过程,那么如何减少初始化 webview 的时间,也是我们可以提高页面打开速度的一个重要环节。

当然,为了提高页面的打开速度,咱们 m 页也可以跟 native 去结合,做一些离线方案,目前转转内部也有一些离线页面的项目有上线,今天就不在此展开。

讲到这里,我们也进入尾声了,也许不久的将来各种新兴的技术会掩盖一些 webview 的光环,像 react-native、小程序、安卓的轻应用开发等等,但是不可否认的是,webview 不会轻易退出历史舞台,我们会把交互做的更好,我们也有情怀。哪有什么岁月静好,只不过有人负重前行

/**
 * 作用域下的 JSBridge,
 * 和实例化后的 getNetInfomation,
 * 均根据实际约定情况而定,
 * 这里只是用来举例说明
 */
const bridge = window.JSBridge;
console.log(bridge.getNetInfomation());

IOS 端

在 IOS 中,主要使用 WebViewJavascriptBridge 来注册,可以参考 Github WebViewJavascriptBridge

jsBridge = [WebViewJavascriptBridge bridgeForWebView:webView];
...
[jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// to do
}];

Android

在 Android 中,需要通过 addJavascriptInterface 来注册

class JSBridge
{    
    @JavascriptInterface //注意这里的注解。出于安全的考虑,4.2 之后强制要求,不然无法从 Javascript 中发起调用
    public void getNetInfomation(){  
        // to do
    };
}
webView.addJavascriptInterface(new JSBridge();, "JSBridge");

2.Schema url

如果说 Bridge 的方式是只能在 native 内部交互,那么 schame url 的不紧可以在 native 内交互,也是可以跨 app 来交互的。schema 也是目前我们转转使用的主要方式,它类似一个伪协议的链接(也可以叫做统跳协议),比如:

schema://path?param=abc

在 webview 里,当 m 页发起 schema 请求时,native 端会去进行捕获。这里可以顺带给大家普及一下 IOS 和 Android 的知识,具体如下:

IOS 端

以 UIWebView 为例,在 IOS 中,UIWebView 内发起网络请求时,可以通过 delegate 在 native 层来拦截,然后将捕获的 schema 进行触发对应的功能或业务逻辑(利用 shouldStartLoadWithRequest)。代码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    //获取 scheme url 后自行进行处理
    NSURL *url = [request URL];
    NSString
    *requestString = [[request URL] absoluteString]; 
    return YES;
}

Android 端

在 Android 中,可以使用 shouldoverrideurlloading 来捕获 schema url。代码如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
    //读取到 url 后自行进行分析处理
    //这里注意:如果返回 false,则 WebView 处理链接 url,如果返回 true,代表 WebView 根据程序来执行 url
    return true;
}

上面分别是 IOS 和 Android 简单的 schema 捕获代码,可以在函数中根据自己的需求,执行对应的业务逻辑,来达到想要的功能。


当然,刚才我们提到通过 schema 的方式可以进行跨端交互,那具体如何操作呢?

其实对于 JavaScript,在 webview 里基本是一样的,也是发起一个 schema 的请求,只不过在 native 侧会有些许变化。

首先,给大家普及一个小知识,就是在 natvie 中(包括 IOS 和 Android),会通过 schema 找到相匹配的 App。其中 IOS 不可以重复,就像 appId 一样;安卓可以重复,遇到重复情况时,会弹窗让用户选择其中之一。

那么,有了这个知识点做铺垫,就可以理解,当我们在其他 app 中,像这个 schema 发起请求时,系统底层(IOS&Android)会通过 schema 去找到所匹配的 app,然后将此 App 拉起。拉起 app 后,对应处理如下:

IOS 端

在 IOS 端内,会将 schema 作为参数传入一个提前定义好的回调函数内,然后执行该回调函数。此回调函数,可以通过得到的 schema 去进行解析,然后定向到 app 内的固定的某个页面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{    
    // 参数 url 即为获取的 schema
    // to do
}

Android 端

在 Android 端内,会稍微麻烦一些,在外部的 m 页,会发起一个 schema 的伪协议链接,系统会去根据这个 schema 去检索,需要被拉起的 App 需要有一个配置文件,大致如下:

<activity android:name =".activity.StartActivity" android:exported = "true">    
    <intent-filter>  
        <action android:name = "android.intent.action.VIEW"/>      
        <category android:name = 
        "android.intent.category.DEFAULT"/>     
        <category android:name = 
        "android.intent.category.BROWSABLE"/>       
        <data android:scheme = "zhuanzhuan"/>    
    </intent-filter>
</activity>

以上面的代码为例,在上面配置中 scheme 为 zhuanzhuan,只要是”zhuanzhuan://”开头的 schema 的链接都会调起配置该 schema 的 Activity(类似上面代码的 StartActivity),此 Activity 会对这个 schema url 做处理,例如:

public class StartActivity extends TempBaseActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        intent = getIntent();
        Uri uri = intent.getData();
    }
}

例如上面的代码,可以在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息,如下图:

在此 Activity 中,通过 intent 中的 getData 方法,获取到传入的 schema 的相关信息

这也是我们在第三方 app 内,可以调起自己 app 的原理。当然现在市场上一些 app,为了怕有流量流失,会对 schema 进行限制,只有 plist 白名单里的 schema 才能对应拉起,否则会被直接过滤掉。比如我们的 wx 爸爸,开通白名单后,才可以使用更多的 jsApiList,通过 schema 的拉起就是其中之一,在此不做赘述……:)

三、webview 的进化

对于 webview,要说进化、或者蜕变,让我第一想到的就是 IOS 的 WKWebView 了,每一个事物存在都有它的必然,让我们一起看看这个 super 版的 webview。

1.WKWebView 的出现

目前混合开发已然成为了主流,为了提高体验,WKWebView 在 IOS8 发布时,也随之一起诞生。在这之前 IOS 端一直使用的是 UIWebView。

从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。目前一些一线互联网 app 在 IOS 已经切换到了 WKWebView,所以感觉我们无法拒绝。

整个 WKWebView 的初始化也很简单:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://mybj123.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和 UIWebView 的很像。

2.WKWebView 与 UIWebView 的对比

上面有提到性能的提升,为什么 app 接入 WKWebView 之后,相对比 UIWebView 内存占用小那么多,主要是因为网页的载入和渲染这些耗内存和性能的过程都是由 WKWebView 进程去实现的(WKWebView 是独立于 app 的进程)。如下图:

WKWebView 是独立于 app 的进程

这样,互相进程独立相当于把整个 App 的进程对内存的占用量减少,App 进程会更为稳定。况且,即使页面进程崩溃,体现出来的就是页面白屏或载入失败,不会影响到整个 App 进程的崩溃。

除了上面说的性能以外,WKWebView 会比 UIWebView 多了一个询问过程。在服务器完成响应之后,会询问获取内容是否载入到容器内,在控制上会比 UIWebView 更细粒度一点,也可以在一些通信上更好的和 m 页进行交互。大概流程如下图:

WKWebView 会比 UIWebView 多了一个询问过程

WKWebView 的代理协议为 WKNavigationDelegate,对比 UIWebDelegate 首先跳转询问,就是载入 URL 之前的一次调用,询问开发者是否下载并载入当前 URL,UIWebView 只有一次询问,就是请求之前的询问,而 WKWebView 在 URL 下载完毕之后还会发一次询问,让开发者根据服务器返回的 Web 内容再次做一次确定。

四、任重而道远

前面说到 WKWebView 这么赞,其实开发中也有一些痛点。不同于 UIWebView,WKWebView 很多交互都是异步的,所以在很大程度上,在和 m 页通信的时候,提高了开发成本。

1.cookie

首先就是 cookie 问题,这个目前我认为也是 WKWebView 在业界的一个坑。之前出现过一个问题,就是在 IOS 登陆完成后,马上进入 m 页,会有登录态的 cookie 获取不到的问题。这个问题在 UIWebView 中是不存在的。

经过调研发现,主要问题是 UIWebView 对 cookie 是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,然后在下次请求时,在请求头里会带上相应的 cookie,来做到 m 页和 native 共享 cookie 的值。

但是在 WKWebView 中,则不然。它虽然也会对NSHTTPCookieStorage来写入 cookie,但却不是实时存储的。而且从实际的测试中发现,不同的 IOS 版本,延迟的时间还不一样,无意对 m 页的开发者是一种挑战。同样,发起请求时,也不是实时读取,无法做到和 native 同步,导致页面逻辑出错。

针对这个问题,目前我们转转的解决方法是需要客户端手动干预一下 cookie 的存储。将服务响应的 cookie,持久化到本地,在下次 webview 启动时,读取本地的 cookie 值,手动再去通过 native 往 webview 写入。大致流程如下图:

转转的解决方法是需要客户端手动干预一下 cookie 的存储

当然这也不是很完美的解决方案,因为偶尔还有 spa 的页面路由切换的时候丢失 cookie 的问题。cookie 的问题还需要我们和客户端的同学继续去探索解决。在这里,如果大家有什么好的建议和处理方法欢迎留言,大家一起学习进步。

2.缓存

除了 cookie 以外,WKWebView 的缓存问题,最近我们也在关注。由于 WKWebView 内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是 IOS8 版本,也许是当时刚诞生 WKWebView 的缘故,还很不完善,根本没法操作(当然相信 IOS8 很快会退出历史舞台)。对于一些 m 页的静态资源,偶尔会出现缓存不更新的情况,着实让人头疼。

但在 IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
                                            WKWebsiteDataTypeDiskCache,
                                            WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
                                           modifiedSince:date
                                       completionHandler:^{
}];

至于 IOS8,就只能通过删除文件来解决了,一般 WKWebView 的缓存数据会存储在这个目录里:

~/Library/Caches/BundleID/WebKit/

可通过删除该目录来实现清理缓存。

另外,以上我们说的痛点以外,还有 webview 的通病,就是我们每次首次打开 m 页时,都要有 webview 初始化的过程,那么如何减少初始化 webview 的时间,也是我们可以提高页面打开速度的一个重要环节。

当然,为了提高页面的打开速度,咱们 m 页也可以跟 native 去结合,做一些离线方案,目前转转内部也有一些离线页面的项目有上线,今天就不在此展开。

讲到这里,我们也进入尾声了,也许不久的将来各种新兴的技术会掩盖一些 webview 的光环,像 react-native、小程序、安卓的轻应用开发等等,但是不可否认的是,webview 不会轻易退出历史舞台,我们会把交互做的更好,我们也有情怀。哪有什么岁月静好,只不过有人负重前行

「点点赞赏,手留余香」

18

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » [转]带你深入认识webview的交互

2 评论

回复 码云 取消回复