iOS UIWebView与WKWebView 那些事

一、前言介绍

UIWebViewiOS 2 中推出的网页容器,UIWebView是最占内存的控件;直到 iOS 8 以后,苹果推出了 WebKit 框架,其中 WKWebView 正式被推出来接替 UIWebView 的位置;iOS 12 中,苹果正式弃用 UIWebView,要求开发者用 WKWebView 全面替换 UIWebView,apple 官方文档

二、特点对比

UIWebView 特点:

  • 1、加载速度慢
  • 2、内存占用大,App停留在后台很容易被系统杀死
  • 3、原生容器完全不带进度条,需要自定义开发

WKWebView 特点:

  • 1、在性能、稳定性、功能方面有很大提升(最直观的提升就是加载网页是占用的内存很少,测试加载不同网页其内存性能提升3倍到4倍,而且没有缓存)
  • 2、允许JavaScript的Nitro库加载并使用(UIWebView中限制)
  • 3、支持更多的 HTML5 特性
  • 4、与 Safari 具有相同的 JavaScript 引擎

三、能力提供

a、加载能力

  • UIWebView不仅可以加载 HTML 页面,还支持 pdf、word、txt 以及各种图片的显示;
  • 相比 UIWebView 而言,WKWebView 也支持各种文件格式,并新增了加载本地文件,即新增了 LoadFileURL 函数。

1.1 UIWebView加载网页请求

- (void)loadRequest:(NSURLRequest *)request;

1.2 WKWebView加载网页请求

- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;

2.1 UIWebView加载HTML

- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

2.2 WKWebView加载HTML

- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

3.1 UIWebView加载文件,并指定 MIME 类型和编码类型

- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

3.2 WKWebView加载文件,并指定 MIME 类型和编码类型

- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL;

4 WKWebView加载本地文件,UIWebView做不到

- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL

b、导航刷新相关

UIWebView 网页导航相关 WKWebView 网页导航相关 说明
canGoBack canGoBack 是否可以后退
canGoForward canGoForward 是否可以前进
isLoading isLoading 是否正在加载

1.1 UIWebView 相关方法

//刷新
- (void)reload;
//停止加载
- (void)stopLoading;
//后退
- (void)goBack;
//前进
- (void)goForward;

1.2 WKWebView 相关方法

//刷新
- (nullable WKNavigation *)reload;
//停止加载
- (void)stopLoading;
//后退
- (nullable WKNavigation *)goBack;
//前进
- (nullable WKNavigation *)goForward;

注:区别于UIWebVie, WKWebView 的方法是有返回值的( stopLoading 除外),返回值类型为 WKNavigation ,主要用于跟踪网页加载进度。

1.3 WKWebView 独有方法

会比较网络数据变化,如果没有变化,则使用缓存,否则重新请求

- (nullable WKNavigation *)reloadFromOrigin;

跳转到某个指定的历史界面

- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

c、代理协议

  • UIWebView 的代理协议主要是UIWebViewDel- 第一行
  • WKWebView 的代理协议主要有 3 个,分别是 WKNavigationDelegate、WKUIDelegate 和 WKScriptMessageHandler 。

UIWebViewDelegate & WKNavigationDelegate

其中 UIWebViewDelegate 和 WKNavigationDelegate 的等效项如下所示

UIWebViewDelegate

//开始加载网页
- (void)webViewDidStartLoad:(UIWebView *)webView ;
//网页加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView;
//网页加载错误
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
//是否允许加载网页,或者获取JS即将打开的URL,通过截取此URL可与JS交互
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

WKNavigationDelegate

//开始加载网页
-  (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
//网页加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
//网页加载错误
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
//是否允许加载网页,或者获取JS即将打开的URL,通过截取此URL可与JS交互
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;

WKNavigationDelegate 拦截加载的代理方法 并不像 UIWebViewDelegate中等效的函数返回 BOOL,而是通过block中 decisionHandler 决定是否可以跳转,返回 allow 或者 cancel 。

WKWebView 独有代理
WKScriptMessageHandler 用于App 与 JS 的交互,提供从网页中收消息的回调方法

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

WKScriptMessageHandler 是必须实现的函数,是用于App 与 JS 的交互,提供从网页中收消息的回调方法,响应从网页的 JavaScript 代码发送的消息。使用 message 参数获取消息内容并确定原始 Web 视图。

WKUIDelegate 是 UI 界面相关的代理协议,主要用于处理三种提示框:输入、确认、警告。因为在 UIWebView 中,Alert、Confirm、Prompt 等视图是可以直接执行的,但在 WKWebView 上,需要通过这个协议接收通知,然后通过 iOS 原生执行,即需要将 Web 提示框拦截然后再通过原生做处理。

//创建一个新的WebView
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;

//经常用于在项目中处理 H5 界面中含有 target = __blank 标签(表示新建一个页面打开网页)或者网页中点击无响应的情况。

//调用 JS 的 alert 方法
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
//调用JS的confirm方法
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
//调用 JS 的 prompt 方法
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
//通知 App,DOM 窗口已成功关闭
- (void)webViewDidClose:(WKWebView *)webView;

四、WKWebView 历史栈缓存策略

a、 WebKit 基础概念

WKWebView 运行时有三种进程协同工作:UIProcess 进程、WebContent 进程、Networking 进程。

1、WebContent 进程
网页 DOM 及 JS 所处进程。进程数量可能有多个,取决于一些细节策略。
在该进程初始化时会创建唯一的 WebProcess 实例,并且作为 IPC::Connection 的 client,与其它进程通信的代理。

2、UIProcess 进程
应用程序对应的进程。
初始化后,WebPageProxy 做为了 IPC::Connection 的 client,与其它进程通信的代理。
WebPageProxy / WebProcessProxy 分别对应了 WebContent 进程的 WebPage / WebProcess。
WebProcessPool(关联 WKWebViewConfiguration 的 WKProcessPool 对象)抽象了 WebContent 进程池,也就是说一个 WKWebView 是可以对应多个 WebContent 进程。

3、Networking 进程
负责网络相关处理,创建多个 WKWebView 也仅只有一个进程。

b、 历史栈缓存策略

简述:
WKWebView 可以通过goBack/goForward接口进行历史栈的切换,切换时有一套缓存策略,命中时能省去请求网络的时间。
WebContent 进程的 BackForwardCache 是一个单例,管理着历史栈缓存。
UIProcess 进程的 WebProcessPool 抽象了 WebContent 进程池,每一个 WebProcessPool 都有唯一的 WebBackForwardCache 表示历史栈缓存,对应着 WebContent 进程池子里的各个 BackForwardCache 单例。
BackForwardCache 用了一个有序 hash 表存储缓存元素,并设定了最大缓存数量:

ListHashSet<RefPtr<HistoryItem>> m_items;
unsigned m_maxSize {0};

1、缓存淘汰策略

BackForwardCache 和 WebBackForwardCache 的策略基本一致,现以 BackForwardCache 为例说明。
WebContent 进程 在切换页面时,会将当前页面通过:

BackForwardCache::singleton().addIfCacheable(...);

添加缓存:

bool BackForwardCache::addIfCacheable(HistoryItem& item, Page* page) {
 ...
item.setCachedPage(makeUnique<CachedPage>(*page));
item.m_pruningReason = PruningReason::None;
 m_items.add(&item);
 ...
}

最大缓存数量源码:

namespace WebKit {
voidcalculateMemoryCacheSizes(...){
uint64_t memorySize = ramSize() / MB; 
 ...
// back/forward cache capacity (in pages)
if (memorySize >= 512)
 backForwardCacheCapacity = 2;
elseif (memorySize >= 256)
 backForwardCacheCapacity = 1;
else
 backForwardCacheCapacity = 0;
 ...
 }
...

可以看出是实现了一个简单的 LRU 淘汰策略。

2、最大缓存数量

前面说到 WebContent 进程最多两个历史栈缓存,实际上这个缓存数量是 UIProcess 进程决定的。在 UIProcess 进程中,WebProcessPool 初始化 WebBackForwardCache 时会设置最大缓存数量,并且在创建 WebProcessProxy 时通过 IPC 通知到对应的 WebContent 进程去设置 BackForwardCache 的m_maxSize。

WebProcessPool 的 WebBackForwardCache 对应了 WebContent 进程池里每一个的 BackForwardCache 单例,是一个一对多的模式,WebBackForwardCache 在修剪缓存元素析构时会自动触发 IPC 通知到 WebContent 进程去清理对应缓存:

WebBackForwardCacheEntry::~WebBackForwardCacheEntry() {
if (m_backForwardItemID && !m_suspendedPage) {
auto& process = this->process();
 process.sendWithAsyncReply(Messages::WebProcess::ClearCachedPage(m_backForwardItemID), [] { });
 }
}

所以缓存最大数量取决于 WebProcessPool 的数量,一个 WebProcessPool 就最多两个历史栈缓存,不管它的进程池有多少个 WebContent。

3、状态同步

在历史栈缓存状态发生变化时,WebContent 进程会调用notifyChanged()通过 IPC 通知到 UIProcess 进程的对应 WebBackForwardCache 去同步状态:

notifyChanged() 最终调用到:
static void WK2NotifyHistoryItemChanged(HistoryItem& item) {
 WebProcess::singleton().parentProcessConnection()->send(Messages::WebProcessProxy::UpdateBackForwardItem(toBackForwardListItemState(item)), 0);
}

五、WKWebView 中常见问题及解决方案

a、POST 类型请求 Request Body 丢失

原因分析:
当使用网络拦截后,WebKit的IPC进程的请求会转到主进程执行,由于进程切换回导致性能下降,所以WebKit会主动丢弃request的body。

解决方案:
1、在 NSURLProtocol 子类中的 - startLoading 通过获取 request.HTTPBodyStream 来填充 request.HTTPBody 实现。但是此方法在有些时候(如Ajax)会失败;

2、通过 hook js XMLHTTPRequest 相关方法实现。具体的来说,注入 XHR requestType 为 post 的 send 方法,把 requstBody 和对应的 URL 通过桥的方式提前传给客户端,客户端接收端数据后,保存起来,在 NSURLProtocol 子类中的 -initWithRequest:cachedResponse:client: 方法拦截中填充保存在客户端的 requstBody 到 request 中,即可保证 requestBody 不会丢失;

b、白屏问题

原因分析:
1、内存占用过大导致 WebContent process 进程崩溃;
2、内存占用过大导致 WebContent process 被挂起。

解决方案:
1、当 WKWebView 占用内存过大的时候,会导致 WebContent process crash,会回调 -webViewWebContentProcessDidTerminate:,可在此方法中添加 [webView reload],重新载入页面解决;

2、当 WKWebView 占用内存过大的时候(多见于选择相册) ,会导致 WebContent process 被挂起,此情况不会调用 -webViewWebContentProcessDidTerminate:,可通过在 WebViewController 的 -viewWillAppear: 方法中检测 webView.title 是否存在,如果不存在可认为进程被挂起,可在此方法中添加 [webView reload],重新载入页面解决;

c、显示HTML页面不是最新的内容

原因分析:
WKWebView有缓存;

解决方案:
为了保证每次加载的都是最新的页面, 可以在加载的链接后面加上一个时间戳;

d、当前页面无导航时不能填充状态栏(iOS11+ 会下移状态栏的高度)

原因分析:
在页面无导航的情况下,系统会自动调节滚动视图的contentInset,使其视图永远处于状态栏之下;

解决方案:

if (@available(iOS 11.0, *)) {
      self.webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
      // Fallback on earlier versions
      self.automaticallyAdjustsScrollViewInsets = NO;
 }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容