美文网首页webview
iOS UIWebView与WKWebView 那些事

iOS UIWebView与WKWebView 那些事

作者: 写代码的小李 | 来源:发表于2022-08-26 15:33 被阅读0次

    一、前言介绍

    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;
     }
    

    相关文章

      网友评论

        本文标题:iOS UIWebView与WKWebView 那些事

        本文链接:https://www.haomeiwen.com/subject/lsntnrtx.html