wkwebview_wkwebview 禁止滚动 - CSDN
精华内容
参与话题
  • WKWebView 的使用

    2017-08-31 18:13:03
    WKWebView是  在iOS 8后推出要替代UIWebView。相对于成熟的UIWebView来讲,这个后生仔在使用上还是有点点小坑的~ 使用 在初始化上,WKWebView 和 UIWebView 没有多大的差异。 // WKWebView let wkWeb...

    WKWebView是  在iOS 8后推出要替代UIWebView。相对于成熟的UIWebView来讲,这个后生仔在使用上还是有点点小坑的~

    使用

    在初始化上,WKWebView 和 UIWebView 没有多大的差异。

    // WKWebView
    let wkWeb = WKWebView(frame: view.bounds)
    // 一些代理
    wkWeb.navigationDelegate = self
    wkWeb.uiDelegate = self
    
    // UIWebView
    let web = UIWebView(frame: view.bounds)
    // 一些代理
    web.delegate = self
    

    二者在初始化上还是蛮像的。一个图样的我。(逃

    仔细翻开了WKWebView,发现其还提供一个初始化方法。

    public init(frame: CGRect, configuration: WKWebViewConfiguration)

    也就是可以用WKWebViewConfiguration 去init一个WKWebView

    后面再继续港 WKWebViewConfiguration

    那几个协议

    WKNavigationDelegate

    WKNavigationDelegate的协议方法还是挺多的。

    // 1)接受网页信息,决定是否加载还是取消。必须执行肥调 decisionHandler 。逃逸闭包的属性
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        print("\(#function)")
    }
    
    // 2) 开始加载
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        print("\(#function)")
    }
    
    // 3) 接受到网页 response 后, 可以根据 statusCode 决定是否 继续加载。allow or cancel, 必须执行肥调 decisionHandler 。逃逸闭包的属性
    func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        print("\(#function)")
       guard let httpResponse = navigationResponse.response as? HTTPURLResponse else {
            decisionHandler(.allow)
            return
        }
        
        let policy : WKNavigationResponsePolicy = httpResponse.statusCode == 200 ? .allow : .cancel
        decisionHandler(policy)        
    }
    
    // 4) 网页加载成功 
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("\(#function)")
    }
    
    // 4) 加载失败
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        print("\(#function)")
       print(error.localizedDescription)
    }
    

    WKUIDelegate

    主要讲讲网页js的一些函数——

    • alert()

    • comfirm()

    • prompt()

    UIWebView中,js使用上述三个函数,是可以成功弹出的。但是在WKWebView中,使用着三个函数,是不会用任何反应的。原因是,把这三个函数都分别封装到WKUIDelegate的方法中。但js使用这些函数时,那么客户端会在以下几个协议方法中,监测到发送过来的信息,然后需要用原生代码去实现一个alert。累cry~~~

    // MARK: alert
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        let alert = UIAlertController(title: "这是本地代码弹窗", message: message, preferredStyle: .alert)
        lert.addAction(UIAlertAction(title: "ok", style: .cancel, handler: { _ in
                // 必须加入这个 肥调,不然会闪 (逃
            completionHandler()
      }))
        present(alert, animated: true, completion: nil)
    }
        
    // MARK: comfirm
    func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        let alert = UIAlertController(title: "这是本地代码弹窗", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "❤️", style: .default, handler: { _ in
            completionHandler(true)
        }))
        alert.addAction(UIAlertAction(title: "不❤️", style: .default, handler: { _ in
            completionHandler(false)
        }))
        present(alert, animated: true, completion: nil)
    }
        
    // MARK: prompt
    func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        
        let alert = UIAlertController(title: "这是本地代码弹窗", message: prompt, preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = defaultText
        }
        alert.addAction(UIAlertAction(title: "ok", style: .default, handler: { _ in
            completionHandler(alert.textFields?.last?.text)
        }))
        present(alert, animated: true, completion: nil)
        
    }
    

    踩坑

    网页适配

    有些网页在客户端上显示,会出现一些不适配的情况。使用UIWebView的话,我们可以用使用其的scaleToFit属性。即webView.scaleToFit = true。但是在WKWebView里,并没有这个属性,我们只能使用到JS注入进行修改。

    // 这句相当于给网页注入一个 <meta> 标签,<meta name="viewport" content="width=device-width">
    let jsToScaleFit = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);"
    
    let scaleToFitScript = WKUserScript(source: jsToScaleFit, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
            
    let userController = WKUserContentController()
    userController.addUserScript(scaleToFitScript)
            
    let config = WKWebViewConfiguration()
    config.userContentController = userController
            
    let wkWeb = WKWebView(frame: view.bounds, configuration: config!)
    

    不过,还是不太推荐客户端去注入适配代码。最好还是告知前端,让他们去搞定这问题。个人觉得,两端少点干涉还是比较好滴~~~ (逃

    客户端 -> 网页

    有时间,我们需要客户端去调用前端的一些代码。e.g.

    // 比如获取网页内容高度
    let jsToGetWebHeight = "document.body.offsetHeight"
    
    wkWeb?.evaluateJavaScript(jsToGetWebHeight, completionHandler: { (data, error) in
        print(error?.localizedDescription ?? "执行正确")
        // data 是一个 any 类型,因此需要做好类型判断
        if let webHeight : CGFloat = data as? CGFloat {
            print(webHeight)
        }
    })
    

    网页 -> 客户端

    不像UIWebViewWKWebView无法使用JaveSciptCore。我们需要使用到WKScriptMessageHandler这个协议去进行一系列交互。

    首先,在初始化阶段,需要使用到WKWebViewConfiguration。放个文档注释先。

    /*! @abstract Adds a script message handler.
    @param scriptMessageHandler The message handler to add.
    @param name The name of the message handler.
    @discussion Adding a scriptMessageHandler adds a function window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all frames. */

    open func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)

    name是客户端自定义好的一个字符串类型的命名空间。可以有多个name。网页的js代码,需要在进行交互的地方,使用上

    window.webkit.messageHandlers.<name>.postMessage(<messageBody>)

    来发送消息。

    上个栗子�。

    let conifg = WKWebViewConfiguration()
    config.userContentController.add(self, name: "Test")
    let webView = WKWebView(frame: view.bounds, configuration: config)

    这是客户端使用WKWebViewConfiguration去初始化一个WKWebView。并且使用到一个nameTestmessageHandler。而在网页需要进行交互的位置,则是加在一句代码。�

    <script type="text/javascript">
        function test() {
            
            var message = {
                action: "test",
                params: null,  
                callback: "callback()"
            };
            window.webkit.messageHandlers.Test.postMessage(message);
        }
    </script>

    对应messageBody载体的格式,貌似没有多大的规定。可以为NSNumberNSStringNSDateNSArrayNSDictionary,甚至是NSNull。也就是对应js来说,应该是NumberStringDatejsonnull

    那么问题来了。客户端怎么去做处理呢?

    客户端需要去实现WKScriptMessageHandler的协议方法。

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        // 建议是做好判断,毕竟有可能 碰到多种 name 的情况
        if message.name == "Test" {
            // 此处可以去撸 需要 的一些交互了
            print(message.body)
        }
    }

    跳转app store

    WKWebview是无法直接跳转app store。不明白爸爸为什么要这样。反正他高兴就好。。。。
    那么如果PM硬要跳转app store的话,有两种方式——

    1) 砍死PM。。(ps: 个人极度推荐方式一)

    2)那么你只能苦逼码码去解决了。

    WKNavigationDelegate中,当接受到网页信息的时候,也就是——

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let request = navigationAction.request
           
        if let url = request.url {
            if url.host == "itunes.apple.com" {
               UIApplication.shared.openURL(url)
               decisionHandler(.cancel)
           }
        }
            
        decisionHandler(.allow)
    }

    有一些古怪的需求

    某天,PM跑过来跟你港,想要点击一个网页超链接,然后客户端去push controller,而不是在原页面上刷新。。。

    使用UIWebView的时候,其实挺方便的,只需要在UIWebViewDelegate的一个方法中去监听做判断就好。呐。看�。

    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        if navigationType == .linkClicked {
            guard let url = request.url?.absoluteString else {
                return true
            }
            // 此处 push 一个 新的 controller 吧
            
            return false
        }
        
        return true
    }

    但是,WKWebView呢?

    需要在WKNavigationDelegate协议方法中,当接收到网页信息的时候——

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let request = navigationAction.request
        
        if let url = request.url {
        // 2、检测打开 <a href> 标签 。如果要打开一个 新web,那么需要 <a href="xx" target="_blank" >,若无 target="_blank",则只会在原web基础上 reload
             if navigationAction.targetFrame == nil {
                // 这里做 push 新的 webview 操作
                           
            }  
        }
        decisionHandler(.allow)
    }

    结束语

    暂时撸到这里吧。估计还有一些坑。后续继续踩,继续更新吧。

    最后,上个demo吧。

    展开全文
  • WKWebView 的使用简介

    万次阅读 2015-06-10 09:31:35
    WKWebView相对于UIWebView强大了很多,内存的消耗相对少了,所提供的接口也丰富了。 现在谈一谈WKWebView的基本使用 1. navigationDelegate - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:...

    WKWebView相对于UIWebView强大了很多,内存的消耗相对少了,所提供的接口也丰富了。

    现在谈一谈WKWebView的基本使用

    1. navigationDelegate

    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { // 类似UIWebView的 -webViewDidStartLoad:
        NSLog(@"didStartProvisionalNavigation");
        [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    }
    
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
        NSLog(@"didCommitNavigation");
    }
    
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { // 类似 UIWebView 的 -webViewDidFinishLoad:
        NSLog(@"didFinishNavigation");
        [self resetControl];
        if (webView.title.length > 0) {
            self.title = webView.title;
        }
    
    }
    - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
        // 类似 UIWebView 的- webView:didFailLoadWithError:
    
        NSLog(@"didFailProvisionalNavigation");
        
    }
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
        
        decisionHandler(WKNavigationResponsePolicyAllow);
    }
    
    
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
        // 类似 UIWebView 的 -webView: shouldStartLoadWithRequest: navigationType:
    
        NSLog(@"4.%@",navigationAction.request);
    
        
        NSString *url = [navigationAction.request.URL.absoluteString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    
        
        
        decisionHandler(WKNavigationActionPolicyAllow);
    
    }
    - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
        
    }
    

    2 UIDelegate

    - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
        // 接口的作用是打开新窗口委托
        [self createNewWebViewWithURL:webView.URL.absoluteString config:configuration];
        
        return currentSubView.webView;
    }
    
    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)())completionHandler
    {    // js 里面的alert实现,如果不实现,网页的alert函数无效
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message
                                                                                 message:nil
                                                                          preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:[UIAlertAction actionWithTitle:@"确定"
                                                            style:UIAlertActionStyleCancel
                                                          handler:^(UIAlertAction *action) {
                                                              completionHandler();
                                                          }]];
        
        [self presentViewController:alertController animated:YES completion:^{}];
        
    }
    
    
    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
        //  js 里面的alert实现,如果不实现,网页的alert函数无效  , 
    
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message
                                                                                 message:nil
                                                                          preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:[UIAlertAction actionWithTitle:@"确定"
                                                            style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction *action) {
                                                              completionHandler(YES);
                                                          }]];
        [alertController addAction:[UIAlertAction actionWithTitle:@"取消"
                                                            style:UIAlertActionStyleCancel
                                                          handler:^(UIAlertAction *action){
                                                              completionHandler(NO);
                                                          }]];
        
        [self presentViewController:alertController animated:YES completion:^{}];
        
    }
    
    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {
        
        completionHandler(@"Client Not handler");
        
    }
    


    3. WKWebView 执行脚本方法

    - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler;
    

    completionHandler 拥有两个参数,一个是返回错误,一个可以返回执行脚本后的返回值

    4. WKWebView 的Cookie问题

    UIWebView 中会自动保存Cookie,如果登录了一次,下次再次进入的时候,会记住登录状态

    而在WKWebView中,并不会这样,WKWebView在初始化的时候有一个方法

    - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration

    通过这个方法,设置 configuration 让WKWebView知道登录状态,configuration 可以通过已有的Cookie进行设置,也可以通过保存上一次的configuration进行设置

    参考 stackoverflow上回答:http://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303

    WKWebView * webView = /*set up your webView*/
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com/index.html"]];
    [request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"];
    // use stringWithFormat: in the above line to inject your values programmatically
    [webView loadRequest:request];

    WKUserContentController* userContentController = WKUserContentController.new;
    WKUserScript * cookieScript = [[WKUserScript alloc] 
        initWithSource: @"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
        injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    // again, use stringWithFormat: in the above line to inject your values programmatically
    [userContentController addUserScript:cookieScript];
    WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
    webViewConfig.userContentController = userContentController;
    WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];



    展开全文
  • WKWebView 最全适配指南

    千次阅读 2018-03-07 14:15:53
    本文系Smallfan(程序猿小风扇)原创内容,转载请在文章开头显眼处注明作者和出处。 分析 在iPhone 6s、iOS 10.3.2中,对 ... ... WKWebView 内存(APP)消耗 UIWebView 请求耗时 WKWebView 请求耗时 ...

    本文系Smallfan(程序猿小风扇)原创内容,转载请在文章开头显眼处注明作者和出处。

    分析

    在iPhone 6s、iOS 10.3.2中,对 http://www.qq.com 进行10次请求,得到如下数据:

    次数 UIWebView 内存消耗 WKWebView 内存(APP)消耗 UIWebView 请求耗时 WKWebView 请求耗时
    1 67.47 MB 0.81 MB 4.13 s 0.80 s
    2 58.23 MB 0.86 MB 1.16 s 0.54 s
    3 57.83 MB 0.50 MB 1.14 s 0.56 s
    4 59.38 MB 0.88 MB 1.08 s 1.07 s
    5 59.70 MB 0.75 MB 1.07 s 0.71 s
    6 64.05 MB 0.83 MB 1.47 s 0.65 s
    7 59.45 MB 0.81 MB 1.11 s 0.63 s
    8 57.55 MB 0.45 MB 1.15 s 0.64 s
    9 58.47 MB 0.77 MB 1.17s 0.75 s
    10 58.89 MB 0.84 MB 1.11 s 0.70 s

    UIWebView平均内存消耗:54.13 MB
    WKWebView平均(APP)内存消耗:0.75 MB
    UIWebView平均请求耗时:1.46 s
    WKWebView平均请求耗时:0.7 s

    综上可得:WKWebView在请求耗时上为UIWebView的50%左右,内存上更是完胜。但实际上 WKWebView是一个多进程组件,网络请求以及UI渲染在其它进程中执行。仔细观察会发现:加载时,App进程内存消耗虽非常小甚至反而大幅下降,但Other Process的内存占用会增加。所以:在UIWebView上当内存占用太大的时候,App Process会crash;而在WKWebView上当总体的内存占用比较大的时候,WebContent Process会crash,从而出现白屏现象。

    Tip: 在一些用webGL渲染的复杂页面,使用WKWebView总体的内存占用(App Process Memory + Other Process Memory)不见得比UIWebView少很多。

    考虑全部替换WKWebView风险过高,可通过Server端在APP启动时下发URL列表的方式实现WKWebView的灰度能力。通过封装继承 UIViewSFWebView ,实现UIWebView与WKWebView双核能力WebView。

    特性

    关于WKWebView特性:
    + 在性能、稳定性、功能方面有很大提升;
    + 允许JavaScript的Nitro库加载并使用(UIWebView中限制);
    + 支持了更多的HTML5特性;
    + 高达60fps的滚动刷新率以及内置手势;
    + 将UIWebView 和 UIWebViewDelegate 重构成了14类与3个协议;

    一些问题及解决方案

    1.白屏问题

    在UIWebView上当内存占用太大的时候,App Process会crash;而在WKWebView上当总体的内存占用比较大的时候,WebContent Process会crash,从而出现白屏现象。

    实验链接: http://people.mozilla.org/~rnewman/fennec/mem.html

    这个时候WKWebView.URL会变为nil, 简单的 reload 刷新操作已经失效,对于一些长驻的H5页面影响比较大。
    解决方案:

    • 借助 WKNavigtionDelegate(仅适用iOS9以上)
    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

    当WKWebView总体内存占用过大,页面即将白屏的时候,系统会调用上面的回调函数,我们在该函数里执行 [webView reload] (这个时候webView.URL取值尚不为nil)解决白屏问题。在一些高内存消耗的页面可能会频繁刷新当前页面,H5侧也要做相应的适配操作。

    • 检测 webView.title 是否为空(需在初始化时设置默认webView.title)

    并不是所有H5页面白屏的时候都会调用上面的回调函数,比如在一个高内存消耗的H5页面上present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是webView.title会被置空, 因此,可以在viewWillAppear的时候检测webView.title是否为空来 reload 页面。

    综合以上两种方法可以解决绝大多数的白屏问题。

    2.Cookie问题

    2.1 Cookie的私有存储问题

    业界普遍认为WKWebView拥有自己的私有存储,不会将cookie存入到标准的cookie容器NSHTTPCookieStorage中。
    实践发现:在iOS 8上,当页面跳转的时候,当前页面的cookie会写入 NSHTTPCookieStorage 中,而在iOS 10上,JS执行 document.cookie 或服务器 set-cookie 注入的cookie会很快同步到 NSHTTPCookieStorage 中.

    FireFox工程师曾建议通过 reset WKProcessPool来触发cookie同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。

    2.2 请求不会自动带上容器中Cookie问题

    WKWebView发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的cookie。
    比如,NSHTTPCookieStorage 中存储了一个cookie:

    name=Nicholas;value=test;domain=www.smallfan.net;expires=Sat, 02 May 2019 23:38:25 GMT;

    通过UIWebView发起请求http://www.smallfan.net,则请求头会自动带上cookie: Nicholas=test;
    而通过WKWebView发起请求http://www.smallfan.net,请求头不会自动带上cookie: Nicholas=test。
    解决方案:

    • A. WKWebView loadRequest 前,在request的header中设置cookie, 解决首个请求cookie带不上的问题。
    WKWebView *webView = [WKWebView new];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.fxiaoke.com"]];
    [request addValue:@"uid=1000" forHTTPHeaderField:@"Cookie"];
    [webView loadRequest:request];
    • B.通过·document.cookie·设置cookie解决后续页面(同域)Ajax、iframe请求的cookie问题。
    - (NSString *)shareHttpCookieFromStorage:(NSURL *)url {
    
        NSMutableArray *array = [NSMutableArray array];
        for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:url]) {
    
            NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
            value = [NSString stringWithFormat:@"document.cookie = '%@'", value];
            [array addObject:value];
        }
    
        NSString *header = @"";
        header = [array componentsJoinedByString:@";"];
    
        return header;
    }
    
    - (void)addCookiesWithUrl:(NSURL *)url {
        WKUserContentController *userContentController = [WKUserContentController new]; 
        WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:[self shareHttpCookieFromStorage]  injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                             forMainFrameOnly:NO];
        [controller addUserScript:cookieScript];
        [userContentController addUserScript:cookieScript];
        _wkWebView = [[WKWebView alloc] initWithFrame:self.bounds configuration:configuration];
        ...
    }

    注意:因为NSHTTPCookieStorage是整个APP共享的单例,包含了所有domain的cookie,在WKWebView初始化时,务必提前获取URL载入对应的cookie,防止因cookies泄漏造成可模仿登录等安全漏洞。

    B方案无法解决302请求(跨域)的cookie问题,可以拦截页面每次跳转都会调用的回调函数:
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler来实现复制request对象,在request header中带上cookie并重新 loadRequest

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    
        NSMutableURLRequest *newReq = [navigationAction.request mutableCopy];
        NSMutableArray *array = [NSMutableArray array];
        for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:navagationAction.request.URL]) {
            NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
            [array addObject:value];
        }
    
        NSString *cookie = [array componentsJoinedByString:@";"];
        [newReq setValue:cookie forHTTPHeaderField:@"Cookie"];
        [webView loadRequest:newReq];
    }

    缺陷:依然解决不了页面iframe跨域请求的cookie问题,毕竟-[WKWebView loadRequest:]只适合加载mainFrame请求。

    2.3 WKProcessPool无法本地化保存(离线缓存)

    苹果开发者文档对WKProcessPool的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有WKWebView共享同一个WKProcessPool实例,可以实现多个WKWebView之间共享cookie数据。不过WKProcessPool实例在app杀进程重启后会被重置,导致WKProcessPool中的cookie、session cookie数据丢失,目前也无法实现WKProcessPool实例本地化保存。
    注意:由于WKWebView在请求过程中用户可能退出界面销毁对象,当请求回调时由于接收处理对象不存在,造成Bad Access crash,所以可将WKProcessPool设为单例
    附使用方式:

    static WKProcessPool *_sharedWKProcessPoolInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedWKProcessPoolInstance = [[WKProcessPool alloc] init];
    });
    
    self.processPool = _sharedWKProcessPoolInstance;
    
    WKWebViewConfiguration *configuration1 = [[WKWebViewConfiguration alloc] init];
    configuration1.processPool = self.processPool;
    WKWebView *webView1 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration1];
    ...
    WKWebViewConfiguration *configuration2 = [[WKWebViewConfiguration alloc] init];
    configuration2.processPool = self.processPool;
    WKWebView *webView2 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration2];
    ...

    3.NSURLProtocol问题

    WKWebView在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在WKWebView上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 Webkit2 源码暴露了私有API:

    + [WKBrowsingContextController registerSchemeForCustomProtocol:]

    通过注册 http(s) scheme 后WKWebView将可以使用 NSURLProtocol 拦截http(s)请求:

    //仅iOS8.4以上可用
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.4)  {
        Class cls = NSClassFromString(@"WKBrowsingContextController”); 
        SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
    
        if ([(id)cls respondsToSelector:sel]) {
            #pragma clang diagnostic push
            #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    
                // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
                [(id)cls performSelector:sel withObject:@"http"];
                [(id)cls performSelector:sel withObject:@"https"];
    
            #pragma clang diagnostic pop
            }
        }
    }

    这种方案目前存在以下严重缺陷:
    post请求body数据被清空
    由于WKWebView在独立进程里执行网络请求。一旦注册http(s) scheme后,网络请求将从Network Process发送到App Process,这样 NSURLProtocol 才能拦截网络请求。在webkit2的设计里使用MessageQueue进行进程之间的通信,Network Process会将请求encode成一个Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode的时候HTTPBody和HTTPBodyStream这两个字段被丢弃掉了。
    因此,如果通过 registerSchemeForCustomProtocol 注册了http(s) scheme, 那么由WKWebView发起的所有http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致post请求body被清空。
    解决方案:

    3.1 如果没有开启ATS

    可以注册customScheme, 比如smallfan://, 因此希望使用离线功能又不使用post方式的请求可以通过 customScheme 发起请求,比如 smallfan://webCache/HelloWorld ,然后在App进程 NSURLProtocol 拦截这个请求并加载离线数据。不足:使用post方式的请求该方案依然不适用,同时需要HTML5侧修改请求scheme以及CSP规则。

    3.2 如果开启ATS

    因为:一旦打开ATS开关:Allow Arbitrary Loads选项设置为NO,同时通过 registerSchemeForCustomProtocol 注册了http(s) scheme,WKWebView发起的所有非https网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content选项设置为YES)。
    可通过hook所有的post请求的方式解决:

    • 对于Ajax post请求,思路是通过XMLHttpRequest send及open方法,将http body内容拼装在http header中并正常请求,App进程 NSURLProtocol 拦截这个请求,将header中的BODY内容取出置于body,发送请求,并将结果返回WKWebView(可借助 WebViewProxy 完成)。
      JS文件:
    var s_ajaxListener = new Object();
    s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
    s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
    s_ajaxListener.tempSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
    
    XMLHttpRequest.prototype.open = function(a,b) {
      this._method = a;
      s_ajaxListener.tempOpen.apply(this, arguments);
    }
    
    XMLHttpRequest.prototype.send = function(a,b) { 
      if (this._method && this._method.toLowerCase() == 'post') {
        a = encodeURIComponent(a)
        s_ajaxListener.tempSetRequestHeader.apply(this, ['BODY', a])
      }
      return s_ajaxListener.tempSend.apply(this, arguments);
    }

    APP拦截请求:

    [WebViewProxy handleRequestsWithHttpHeader:@"BODY" handlerHash:[self hash] handler:^(NSURLRequest *req, WVPResponse *res) {
    
        NSURLSession *session = [NSURLSession sharedSession];
        NSMutableURLRequest *request = [req mutableCopy];
        request.HTTPMethod = @"POST";
        NSString *postData = request.allHTTPHeaderFields[@"BODY"];
        NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
        NSString *httpBody = decodePostData;
        request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
            [res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];
    
        }];
        [dataTask resume];
    }];
    • 对于form请求,解决方式类似Ajax,只是将BODY转编码后拼接到URL中,APP进程处理方式一致。

    JS文件:

    var s_formListener = window.onsubmit;
    window.onsubmit = function (e) {
    var node = e.srcElement;
    if (node && node.tagName && node.tagName.toLowerCase() === 'form') {
        if (node.method && node.method.toLowerCase() === 'post') {
            var elements = [].slice.call(node.elements);
            var tempData = [], entryName, entryValue;
            for (var i = 0, l = elements.length; i < l; ++i) {
                entryName = elements[i].name;
                entryValue = elements[i].value;
                if (entryValue.toString() === '[object File]') {
                    entryValue = entryValue.name;
                }
                tempData.push(encodeURIComponent(entryName) + '=' + encodeURIComponent(entryValue));
            }
            tempData = tempData.join('&');
    
            var action = node.action || location.href;
            var hashIndex = action.indexOf('#');
            if (hashIndex >= 0) {
                action = action.substring(0, hashIndex);
            }
            var queryIndex = action.indexOf('?');
            if (queryIndex >= 0) {
                action = action + '&POST_DATA=' + encodeURIComponent(tempData);
            } else {
                action = action + '?POST_DATA=' + encodeURIComponent(tempData);
            }
    
            node.action = action;
        }
    }
    if (s_formListener && s_formListener.apply) {
        s_formListener.apply(this, arguments);
    }
    }

    APP拦截请求:

    [WebViewProxy handleRequestsWithHttpHeader:@"POST_DATA" handler:^(NSURLRequest *req, WVPResponse *res) {
    
        NSURLSession *session = [NSURLSession sharedSession];
        NSMutableURLRequest *request = [req mutableCopy];
        request.HTTPMethod = @"POST";
        NSString *postData = request.allHTTPHeaderFields[@"POST_DATA"];
        NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
        NSString *httpBody = decodePostData;
        request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
            NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
            [res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];
    
        }];
        [dataTask resume];
    }];

    缺陷:Http header value及url字符有长度限制。在WWDC 2017上,提到iOS 11将开放一个 WKURLSchemeHandler 注册,提供custom response的能力,拭目以待。

    4.JavaScript交互

    4.1 WKWebView调用JavaScript

    [_wkWebView evaluateJavaScript:@"Hello" completionHandler:^(NSString result, NSError * _Nullable error) {
        if([result isEqualToString:@"Hi"]) {
        }
    }];

    4.2 JavaScript调用WKWebView

     WKWebViewConfiguration * Configuration = [[WKWebViewConfiguration alloc] init];
     WKUserContentController *userContentController = [[WKUserContentController alloc] init];
    
    //注册一个name为HelloNative的js方法
    [userContentController addScriptMessageHandler:self  name:@"HelloNative"];
    
    Configuration.userContentController = userContentController;
    _wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 300,500) configuration:Configuration];
    #pragma mark WKScriptMessageHandler
    //设置WKWebView的WKScriptMessageHandler代理方法
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
        if ([message.name isEqualToString:@"HelloNative"]) {
            // 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray, NSDictionary, NSNull类型
            NSLog(@"%@", message.body);
        }
    }

    JS调用:

    window.webkit.messageHandlers.HelloNative.postMessage(message);

    注意:关闭web页时,需要调用removeScriptMessageHandlerForName以防止内存泄漏。

    - (void)dealloc {
        _wkWebView.UIDelegate = nil;
        _wkWebView.navigationDelegate = nil;
        [[_wkWebView configuration].userContentController removeScriptMessageHandlerForName:@"HelloNative"];
    }

    5.Crash问题

    5.1 JS调用window.alert()函数引起的crash

    当JS调用alert函数时,WKWebView使用如下方法回调:

    + (void)presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

    主要原因是上述 completionHandler 没有被调用导致的。在适配WKWebView的时候,我们需要自己实现该回调函数,window.alert()才能调起alert框。
    解决方案:

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
        if (/*UIViewController of WKWebView has finish push or present animation*/) { 
            completionHandler();
            return;
        } 
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];
        if (/*UIViewController of WKWebView is visible*/)
            [self presentViewController:alertController animated:YES completion:^{}];
        else
            completionHandler();
    }

    5.2 WKWebView 退出前调用evaluateJavaScript:completionHandler:引起的crash

    主要原因WKWebView退出并被释放后导致 completionHandler 变成野指针,而此时 JavaScriptCore还在执行JS代码,待JavaScriptCore执行完毕后会调用 completionHandler() ,导致crash。
    这个crash只发生在iOS 8系统上,iOS9以上主要是对completionHandler block做了copy。

    解决方案:
    通过在completionHandlerretain``WKWebView防止completionHandler被过早释放。

    + (void) load {
        [self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil];
    }
    /*
     * fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation 
     */
    - (void)altEvaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler {
        id strongSelf = self;
        [self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e)  {
            [strongSelf title];
            if (completionHandler) {
                completionHandler(r, e);
            }
        }];
    }

    6.进度条问题

    在UIWebView中,进度条一直是个缺陷问题,尽管有 NJKWebViewProgress 一类开源组件,但在精确处理加载完成上还有些许不足(部分页面可见webViewDidStartLoad:与webViewDidFinishLoad:不成对回调,导致进度条加载结果计算有误)。而WKWebView上增加了一个estimatedProgress属性,通过KVO可实现精准进度条控制。iOS WKWebView添加类似微信的进度条
    通过对微信的观察,进度条的精确结果并不是首要的,良好的用户心态预期才是其重点。所以,可以考虑以如下方式实现“虚拟”的进度条。

    - (void)startProgress {
        if (_hideProgress) {
            return;
        }
    
        if (_progress == 0) {
            _progress = 0.9;
    
            [_progressLayer removeAllAnimations];//清除所有的动画
    
            CGRect frame = _progressView.frame;
            CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
            //设置关键帧数组
            animation.values=@[[NSValue valueWithCGPoint:CGPointMake(0, 0)],
                               [NSValue valueWithCGPoint:CGPointMake(0.7 * frame.size.width, .0)],
                               [NSValue valueWithCGPoint:CGPointMake(0.9 * frame.size.width, .0)],
                               [NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
            //设置每个关键帧对应的时间点,取值为0~1
            animation.keyTimes = @[[NSNumber numberWithFloat:.0],
                                   [NSNumber numberWithFloat:.3],
                                   [NSNumber numberWithFloat:.7],
                                   [NSNumber numberWithFloat:1.]];
            animation.removedOnCompletion = YES;
            animation.fillMode = kCAFillModeForwards;
            animation.duration = 20;
            animation.delegate = self;
            [_progressLayer addAnimation:animation forKey:@"startProgress"];
        }
    }
    
    - (void)completeProgress {
        if (_hideProgress) {
            return;
        }
    
        CGPoint point = _progressLayer.presentationLayer.position;//当前动画的position
        if (round(point.x) == 0 && !_progress) {
            return;
        }
        [_progressLayer removeAnimationForKey:@"startProgress"];
    
        _progress = 1.0;
    
        CGRect frame = _progressView.frame;
        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        //设置关键帧数组
        animation.values=@[[NSValue valueWithCGPoint:point],
                           [NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
        //设置每个关键帧对应的时间点
        animation.keyTimes = @[[NSNumber numberWithFloat:.0],
                               [NSNumber numberWithFloat:1.]];
        animation.duration = .27;
        animation.removedOnCompletion = YES;
        animation.fillMode = kCAFillModeForwards;
        [_progressLayer addAnimation:animation forKey:@"completeProgress"];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        // 当WKWebView回调webView:didFinishNavigation:时,页面实际上渲染并未完成
        // 监听loading属性变化,可精确判断请求完成+渲染完成
        if ([keyPath isEqualToString:@"loading"]) {
    
            BOOL oldLoading = [[change objectForKey:NSKeyValueChangeOldKey] boolValue];
            BOOL newLoading = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
    
            if (newLoading) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self startProgress];
                });
            } else if (!newLoading) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self completeProgress];
                    _progress = 0;
                });
            }
        } else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    
    - (void)dealloc {
        _wkWebView.UIDelegate = nil;
        _wkWebView.navigationDelegate = nil;
    
        //记得在销毁时释放监听
        [_wkWebView removeObserver:self forKeyPath:@"loading"];
    }
    

    7.截屏问题

    - (UIImage*)imageSnapshot {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor);
        [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
        UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return newImage;
    }

    使用以上方法对webGL页面的截屏结果不是空白就是纯黑图片。

    解决方案:约定一个JS接口,让H5实现该接口,具体是通过 canvas getImageData() 方法取得图片数据后返回 base64 格式的数据,客户端在需要截图的时候,调用这个JS接口获取 base64 String 并转换成 UIImage

    8.其他问题

    8.1 iOS8.2以下音频无法停止

    解决方案多种:

    //第一种:返回时请求一个空页面
     [NSURL URLWithString:@"about:blank"]
    
    //第二种:返回时加载一个空的HTML String
    [_wkWebView loadHTMLString:@"<html/>" baseURL:nil]
    
    //第三种:在viewDidDisappear方法里播放无声的音频再暂停

    8.2 视频没有自动播放

    解决方案:
    WKWebView需要通过 WKWebViewConfiguration.mediaPlaybackRequiresUserAction 设置是否允许自动播放,但一定要在WKWebView初始化之前设置,在WKWebView初始化之后设置无效。

    8.3 页面回退问题

    • 业务上的需求,当最后只有一条历史,直接 pop 回去,需要如下改写。
    - (BOOL)canGoBack {
        if (self.backForwardList.backList.count <= 1) {
            return NO;
        }
        return YES;
    }
    • WKWebView上调用 -[WKWebView goBack] , 回退到上一个页面后不会触发 window.onload() 函数、不会执行JS。

    8.4 页面回退时,字体变大

    解决方案:
    在页面 webView:didFinishNavigation: 中执行以下JavaScript将webkit中字体还原到100%

    [self evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '100%'" completionHandler:nil];

    参考文献:
    1. 腾讯Bugly团队【WKWebView 那些坑】

    我是程序猿小风扇,请多多指教
    Github:Smallfan

    展开全文
  • WKWebView 那些坑

    千次阅读 2019-04-27 13:52:23
    WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView。WKWebView拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。 简单的适配方法本文...

    导语

    WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView。WKWebView 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。

    简单的适配方法本文不再赘述,主要来说说适配 WKWebView 过程中填过的坑以及善待解决的技术难题。

    1、WKWebView 白屏问题

    WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面,使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory)不见得比 UIWebView 少很多。

    在 UIWebView 上当内存占用太大的时候,App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:

    people.mozilla.org/~rnewman/fe…

    这个时候 WKWebView.URL 会变为 nil, 简单的 reload 刷新操作已经失效,对于一些长驻的H5页面影响比较大。

    我们最后的解决方案是:

    A、借助 WKNavigtionDelegate

    iOS 9以后 WKNavigtionDelegate 新增了一个回调函数:

    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

    当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用上面的回调函数,我们在该函数里执行[webView reload](这个时候 webView.URL 取值尚不为 nil)解决白屏问题。在一些高内存消耗的页面可能会频繁刷新当前页面,H5侧也要做相应的适配操作。

    B、检测 webView.title 是否为空

    并不是所有H5页面白屏的时候都会调用上面的回调函数,比如,最近遇到在一个高内存消耗的H5页面上 present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是 webView.titile 会被置空, 因此,可以在 viewWillAppear 的时候检测 webView.title 是否为空来 reload 页面。

    综合以上两种方法可以解决绝大多数的白屏问题。

    2、WKWebView Cookie 问题

    Cookie 问题是目前 WKWebView 的一大短板

    2.1、WKWebView Cookie存储

    业界普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

    实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。

    WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie

    比如,NSHTTPCookieStorage 中存储了一个 Cookie:

    name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;

    通过 UIWebView 发起请求y.qq.com, 则请求头会自动带上 cookie: Nicholas=test;
    而通过 WKWebView发起请求y.qq.com, 请求头不会自动带上 cookie: Nicholas=test。

    2.2、WKProcessPool

    苹果开发者文档对 WKProcessPool 的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

    2.3、Workaround

    由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:

    a、WKWebView loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题;

    WKWebView * webView = [WKWebView new]; 
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]]; 
    
    [request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
    [webView loadRequest:request];

    b、通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;

    注意:document.cookie()无法跨域设置 cookie

    WKUserContentController* userContentController = [WKUserContentController new]; 
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 
    
    [userContentController addUserScript:cookieScript];

    这种方案无法解决302请求的 Cookie 问题,比如,第一个请求是 www.a.com,我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 www.b.com,这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

    可以在该回调函数里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。

    3、WKWebView NSURLProtocol问题

    WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了私有API

    + [WKBrowsingContextController registerSchemeForCustomProtocol:]

    通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求:

    Class cls = NSClassFromString(@"WKBrowsingContextController”); 
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); 
    if ([(id)cls respondsToSelector:sel]) { 
               // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
               [(id)cls performSelector:sel withObject:@"http"]; 
               [(id)cls performSelector:sel withObject:@"https"]; 
    }

    但是这种方案目前存在两个严重缺陷:

    a、post 请求 body 数据被清空

    由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了

    参考苹果源码:

    github.com/WebKit/webk… (复制链接到浏览器中打开)

    及bug report: 

    bugs.webkit.org/show_bug.cg… (复制链接到浏览器中打开)

    因此,如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body 被清空

    b、对ATS支持不足

    测试发现一旦打开ATS开关:Allow Arbitrary Loads 选项设置为NO,同时通过 registerSchemeForCustomProtocol 注册了 http(s) scheme,WKWebView 发起的所有 http 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES);

    WKWebView 可以注册 customScheme, 比如 dynamic://, 因此希望使用离线功能又不使用 post 方式的请求可以通过 customScheme 发起请求,比如 dynamic://www.dynamicalbumlocalimage.com/,然后在 app 进程 NSURLProtocol 拦截这个请求并加载离线数据。不足:使用 post 方式的请求该方案依然不适用,同时需要 H5 侧修改请求 scheme 以及 CSP 规则;

    4、WKWebView loadRequest 问题

    在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失:

    //同样是由于进程间通信性能问题,HTTPBody字段被丢弃
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
    [wkwebview loadRequest: request];

    workaround:

    假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: h5.qzone.qq.com/mqzone/inde…,可以通过以下步骤实现:

    1. 替换请求 scheme,生成新的 post 请求 request2: post://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段);

    2. 通过-[WKWebView loadRequest:]加载新的 post 请求 request2;

    3. 通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;

    4. 注册 NSURLProtocol 拦截请求post://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: h5.qzone.qq.com/mqzone/inde…,将 request2 header的body 字段复制到 request3 的 body 中,并使用 NSURLConnection 加载 request3,最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

    5、WKWebView 页面样式问题

    在 WKWebView 适配过程中,我们发现部分H5页面元素位置向下偏移被拉伸变形,追踪后发现主要是H5页面高度值异常导致:

    a. 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求,因此之前 webView 整个是从(0, 0)开始布局,通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height的变化上,比如设置 webView.scrollView.contentInset.top = a,那么 contentSize.height的值会增加a,导致H5页面长度增加,页面元素位置向下偏移;

    解决方案是:调整WKWebView布局方式,避免调整webView.scrollView.contentInset。实际上,即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值,这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话,可以通过下面方式让H5页面恢复正常显示:

    /**设置contentInset值后通过调整webView.frame让页面恢复正常显示 
     *参考:http://km.oa.com/articles/show/277372
     */ 
    webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); 
    webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

    b. 在接入 now 直播的时候,我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况,最后发现是window.innerHeight值不准确导致(在WKWebView上返回了一个非常大的值),而H5同学通过获取 window.innerHeight来设置页面高度,导致页面整体被拉伸。通过查阅相关资料发现,这个bug只在 iOS 9 的几个系统版本上出现,苹果后来fix了这个bug。我们最后的解决方案是:延迟调用window.innerHeight

    setTimeout(function(){height = window.innerHeight},0);

    or

    Use shrink-to-fit meta-tag 
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">

    6、WKWebView 截屏问题

    空间玩吧H5小游戏有截屏分享的功能,WKWebView 下通过 -[CALayer renderInContext:]实现截屏的方式失效,需要通过以下方式实现截屏功能:

    @implementation UIView (ImageSnapshot) 
    - (UIImage*)imageSnapshot { 
        UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor); 
        [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; 
        UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); 
        UIGraphicsEndImageContext(); 
        return newImage; 
    } 
    @end

    然而这种方式依然解决不了 webGL 页面的截屏问题,笔者已经翻遍苹果文档,研究过 webKit2 源码里的截屏私有API,依然没有找到合适的解决方案,同时发现 Safari 以及 Chrome 这两个全量切换到 WKWebView 的浏览器也存在同样的问题:对webGL 页面的截屏结果不是空白就是纯黑图片。无奈之下,我们只能约定一个JS接口,让游戏开发商实现该接口,具体是通过 canvas getImageData()方法取得图片数据后返回 base64 格式的数据,客户端在需要截图的时候,调用这个JS接口获取 base64 String 并转换成 UIImage。

    7、WKWebView crash问题

    WKWebView 放量后,外网新增了一些 crash, 其中一类 crash 的主要堆栈如下:

    ... 
    28 UIKit 0x0000000190513360 UIApplicationMain + 208 
    29 Qzone 0x0000000101380570 main (main.m:181) 
    30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36 
    Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called

    主要是JS调用window.alert()函数引起的,从 crash 堆栈可以看出是 WKWebView 回调函数:

    + (void) presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

    completionHandler 没有被调用导致的。在适配 WKWebView 的时候,我们需要自己实现该回调函数,window.alert()才能调起 alert 框,我们最初的实现是这样的:

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
    { 
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
        [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
        [self presentViewController:alertController animated:YES completion:^{}]; 
    }

    如果 WKWebView 退出的时候,JS刚好执行了window.alert(), alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash;另一种情况是在 WKWebView 一打开,JS就执行window.alert(),这个时候由于 WKWebView 所在的 UIViewController 出现(push或present)的动画尚未结束,alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash。我们最终的实现大致是这样的:

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
    { 
        if (/*UIViewController of WKWebView has finish push or present animation*/) { 
            completionHandler(); 
            return; 
        } 
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
        [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
        if (/*UIViewController of WKWebView is visible*/) 
            [self presentViewController:alertController animated:YES completion:^{}]; 
        else 
            completionHandler(); 
    }

    确保上面两种情况下 completionHandler 都能被执行,消除了 WKWebView 下弹 alert 框的 crash,WKWebView 下弹 confirm 框的 crash 的原因与解决方式与 alert 类似。

    另一个 crash 发生在 WKWebView 退出前调用:

     -[WKWebView evaluateJavaScript: completionHandler:]

    执行JS代码的情况下。WKWebView 退出并被释放后导致completionHandler变成野指针,而此时 javaScript Core 还在执行JS代码,待 javaScript Core 执行完毕后会调用completionHandler(),导致 crash。这个 crash 只发生在 iOS 8 系统上,参考Apple Open Source,在iOS9及以后系统苹果已经修复了这个bug,主要是对completionHandler block做了copy(refer: trac.webkit.org/changeset/1…);对于iOS 8系统,可以通过在 completionHandler 里 retain WKWebView 防止 completionHandler 被过早释放。我们最后用 methodSwizzle hook 了这个系统方法:

    + (void) load 
    { 
         [self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil]; 
    } 
    /* 
     * fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation 
     */ 
    - (void)altEvaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler 
    { 
        id strongSelf = self; 
        [self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e) { 
            [strongSelf title]; 
            if (completionHandler) { 
                completionHandler(r, e); 
            } 
        }]; 
    }

    8、其它问题

    8.1、视频自动播放

    WKWebView 需要通过WKWebViewConfiguration.mediaPlaybackRequiresUserAction设置是否允许自动播放,但一定要在 WKWebView 初始化之前设置,在 WKWebView 初始化之后设置无效。

    8.2、goBack API问题

    WKWebView 上调用 -[WKWebView goBack], 回退到上一个页面后不会触发window.onload()函数、不会执行JS。

    8.3、页面滚动速率

    WKWebView 需要通过scrollView delegate调整滚动速率:

    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
         scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
    }

    9、结语

    本文总结了在 WKWebView 上踩过的一些坑。虽然 WKWebView 坑比较多,但是相对 UIWebView 在内存消耗、稳定性方面还是有很大的优势。尽管苹果对 WKWebView 的开发进度过于缓慢,但相信 WKWebView 才是未来。

    展开全文
  • 网页缓存:https://www.jianshu.com/p/f3019d511f36、https://blog.csdn.net/leikezhu1981/article/details/68491249 网路请求缓存:... // // ViewController.m // WebVie...
  • WKWebView-插入本地图片到html

    千次阅读 2017-09-17 10:06:40
    在开发的过程中使用WKWebView是很常见的一件事,现在我们可能遇到一个需求:需要加载本地的html文件,然后插入本地的图片到html显示出来。 真正的应用场景:像问答产品类app,在回答页面是一个编辑器,我们可以输入...
  • wkwebView基本使用方法

    千次阅读 2019-06-26 11:07:26
    WKWebView有两个delegate,WKUIDelegate和WKNavigationDelegate。WKNavigationDelegate主要处理一些跳转、加载处理操作,WKUIDelegate主要处理JS脚本,确认框,警告框等。因此WKNavigationDelegate更加常用。 比较...
  • 随说 : 最近有个需求,是将公司的一个内网的页面嵌套在app中作为一个模块.这不是很简单的webView请求一下就行了么?其实内里大有乾坤.自己也将思路整理一遍 UIWebView UIWebView的基本使用方法 : ...
  • WKWebview详解

    千次阅读 2018-05-15 17:07:28
    UIWebView 之痛开发App的过程中,常常会遇到在App内部加载网页,通常用UIWebView加载。而这个自iOS2.0开始使用的Web容器一直是开发的心病:加载速度慢,占用内存多,优化困难。如果加载网页多,还可能因为过量占用...
  • iOS WKWebView学习笔记(一)

    千次阅读 2019-04-20 14:10:06
    WebKit是在iOS8.0以后,提出的一个网页视图框架,视图渲染控件也有UIWebView,演变成了WKWebView,首先特别好的一点,就是灵活性比之前好很多,另外一个,渲染速度和性能比之前快。(目前还没亲自验证) 同时,核心类,...
  • 教你使用 WKWebView 的正确姿势

    千次阅读 2017-12-25 18:47:46
    WKWebView 是 iOS 8 之后提供的一款浏览器组件,其载入速度和内存占用对比老的 UIWebView 来说简直是一次飞跃。下面对比 UIWebView 介绍该组件如何去使用,以及使用过程中会存在的问题。 目录
  • WKWebView

    千次阅读 2014-12-08 16:04:18
    apple和google为webkit该浏览器引擎的发扬光大做出了重要贡献,在WWDC 2014发布会上发布iOS 8中,apple公布了...webkit使用WKWebView来代替IOS的UIWebView和OSX的WebView,并且使用Nitro JavaScript引擎,这意味着所
  • WKWebView与js交互之完美解决方案

    万次阅读 热门讨论 2017-04-04 19:09:43
      最近对团队中的混合开发框架进行了重构,下面就和大家来说说自己的思路以及解决方案。   随着H5功能愈发的强大,没进行过混合开发的小伙们都不好意思说自己能够独立进行iOS的app开发,在iOS7操作系统下,常用...
  • ios WKWebview

    2018-07-16 17:26:16
    iOS8之后,苹果推出了WebKit这个框架,用来替换原有的UIWebView,新的控件优点多多,不一一叙述。由于一直在适配iOS7,就没有去替换,现在仍掉了iOS7,以为很简单的就替换过来了,然而在替换的过程中,却遇到了很多...
  • UIWebView和WKWebView

    2019-05-03 00:25:29
    UIWebView 什么是UIWebView UIWebView是iOS内置的浏览器控件 系统自带的Safari浏览器就是通过UIWebView实现的 UIWebView不但能加载远程的网页资源,还能加载绝大部分的常见文件 ... pdf、doc、ppt、txt ...
  • WKWebView 常见问题

    2019-03-12 18:04:11
    WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView。WKWebView拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。 简单的适配方法本文...
  • WKWebViewwkwebview加载HTML字符串

    千次阅读 2018-06-11 12:06:00
    2019独角兽企业重金招聘Python工程师标准>>> ...
  • WKWebView代理方法解析

    千次阅读 2018-05-13 18:17:15
    WKWebView代理方法解析   一.前言 上一篇文章已经对WKWebView做了一个简单的介绍,主要对它的一些方法和属性做了一个简单的介绍,今天看一下WKWebView的两个协议:WKNavigationDelegate 和 WKUIDelegate。 二....
  • WKWebView代替UIWebView使用

    千次阅读 2017-02-28 16:05:41
    webView是我们日常开发中不可缺少的一个组件,通常我们都是使用...iOS8之后苹果推荐使用WKWebView替代UIWebView,其主要的有点有: 在性能、稳定性WKWebView更多的支持HTML5的特性WKWebView更快,占用内存可能只有U
1 2 3 4 5 ... 20
收藏数 7,617
精华内容 3,046
关键字:

wkwebview