wkwebview_wkwebview卡顿 - CSDN
精华内容
参与话题
  • WKWebViewDemo.zip

    2020-07-30 23:31:26
    1. 利用iOS原生WKWebView打开 H5 游戏的同时将整个游戏所需的资源文件下载到本地沙盒目录下,便于第二次秒开(加载时优先通过本地资源文件进行加载) 2.此工程也可适用于打开 H5 商城,也可便于实现秒开 原理:在第...
  • 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吧。

    展开全文
  • iOS WKWebView学习笔记(一)

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

    WebKit是在iOS8.0以后,提出的一个网页视图框架,视图渲染控件也有UIWebView,演变成了WKWebView,首先特别好的一点,就是灵活性比之前好很多,另外一个,渲染速度和性能比之前快。(目前还没亲自验证)

    同时,核心类,也演变成了如下:

    WKPreferences,用户偏好设置设置类,

    1)可以设置最小的字体大小minimumFontSize,默认是0

    2)是否允许JS交互javaScriptEnabled,是否允许在无交互的情况下,默认YES,

    3)系统自动打开一个新的window窗口javaScriptCanOpenWindowsAutomatically,默认NO。这个属性主要是针对,JS方法中,window.open('新的链接')。参考文章https://blog.csdn.net/ioszhanghui/article/details/89368229。javaScriptCanOpenWindowsAutomatically在设置成YES的时候,javaScriptEnabled也必须设置成YES。同时,需要设置UIDelegate对象,在实现的时候,系统会调用,- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures。

    使用javaScriptCanOpenWindowsAutomatically时,可能出现,点击无响应的bug,主要原因是WKWebView允许,创建多个WKWebVIew加载,跟HTML5中,新建一个窗口是一样的,也就是HTML5中,target='_blank'。解决办法,参考文章https://blog.csdn.net/ioszhanghui/article/details/89368400

    WKProcessPool 进程池设置,

    如果项目中创建多个WKWebView,想要这个几个WKWebView共享资源内容,比如Cookie内容,需要这个几个WKWebView公用一个WKProcessPool对象,默认,没创建一个WKWebView对象 都会创建一个WKProcessPool对象,而且WKProcessPool类没有对外提供属性,也没有提供方法。参考文章https://blog.csdn.net/ioszhanghui/article/details/89368864

    WKUserContentController 主要是提供用户交互类,比如常见的js注入,原生和h5的点击交互处理。
    //查看所有的注入的用户交互JS,这个可以重复注入。
    @property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
    //添加JS交互注入
    - (void)addUserScript:(WKUserScript *)userScript;
    //移除所有的JS交互注入
    //移除所有的JS交互注入
    - (void)removeAllUserScripts;
    //注册和h5交互的标识 (1)需要WKScriptMessageHandler遵循代理,(2)需要和h5统一name标识
    - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
    //移除和h5交互的标识
    - (void)removeScriptMessageHandlerForName:(NSString *)name;

    1)在原生端通过addScriptMessageHandler,添加代理和区别标示进行注册。
    2)实现代理方法
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
    3)h5端JS实现中,通过Window的方法
    window.webkit.messageHandlers.testMessage(标示).postMessage({"code":"200","message":"成功"});

    WKScriptMessage 主要是交互的消息体类,属性name,是注册的标示,属性body是接收到的交互数据。

    h5端相关的操作可以参考文章https://mp.csdn.net/postedit/89379171

    UIDelegate的代理方法
    //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
    //输入框替换 等同于prompt();
    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

    WKNavigationDelegate代理
    //在发起请求之前 决定跳转不跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
    //在收到响应之后,决定跳转不跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
    //开始发起请求
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
    //在收到响应之后,是否重定向
    - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
    //开始加载失败
    - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
    //收到响应内容
    - (void)webView:(WKWebView *)webView didCommitNavigation:(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;
    //HTTPs的授权认证
    - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

    WKWebsiteDataStore WKWebView的缓存数据,包含内存缓存,硬盘缓存,cookie缓存,这个存储的是你用WKWebView加载过的所有缓存记录。WKWebsiteDataStore是以域名的格式进行缓存的。
    / * 磁盘缓存。 * /
    WKWebsiteDataTypeDiskCache,
    / * 内存中的缓存 。* /
    WKWebsiteDataTypeMemoryCache,
    / * HTML离线Web应用程序缓存。 * /
    WKWebsiteDataTypeOfflineWebApplicationCache,
    / * Cookies 缓存。 * /
    WKWebsiteDataTypeCookies,
    / * HTML会话存储。 * /
    WKWebsiteDataTypeSessionStorage,
    / * HTML本地存储。 * /
    WKWebsiteDataTypeLocalStorage 
    / * IndexedDB数据库。 * /
    WKWebsiteDataTypeIndexedDBDatabases,
    / * WebSQL数据库。 * /
    WKWebsiteDataTypeWebSQLDatabases
    
    //默认的存储容器,是属于持久存储
    + (WKWebsiteDataStore *)defaultDataStore;
    //临时的存储容器
    + (WKWebsiteDataStore *)nonPersistentDataStore;
    //判断是不是持久存储
    @property (nonatomic, readonly, getter=isPersistent) BOOL persistent;
    //缓存数据的形式
    + (NSSet<NSString *> *)allWebsiteDataTypes;
    //获取WKWebsiteDataStore的缓存数据 根据上面
    - (void)fetchDataRecordsOfTypes:(NSSet<NSString *> *)dataTypes completionHandler:(void (^)(NSArray<WKWebsiteDataRecord *> *))completionHandler;
    //删除WKWebsiteDataStore 根据删除的缓存 形式 数据存储的内容
    - (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes forDataRecords:(NSArray<WKWebsiteDataRecord *> *)dataRecords completionHandler:(void (^)(void))completionHandler;
    //删除缓存数据 从某个修改的日期开始之后的删除
    - (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;
    //获取Http的Cookie缓存
    @property (nonatomic, readonly) WKHTTPCookieStore *httpCookieStore API_AVAILABLE(macosx(10.13), ios(11.0));
    

    WKWebsiteDataStore存储的数据样式是 WKWebsiteDataRecord ,
    //缓存的域名 
    @property (nonatomic, readonly, copy) NSString *displayName;
    //存在的缓存的数据类型
    @property (nonatomic, readonly, copy) NSSet<NSString *> *dataTypes;


    WKBackForwardList  WKWebView加载的网页链接,所有加载过的页面记录。
    //当前正在加载的页面 还没记录到backList
    @property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *currentItem;
    //可以返回的上一级网页页面
    @property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *backItem;
    //网页返回之后 可以前进的页面
    @property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *forwardItem;
    //根据你的索引获取 返回的上一页的页面项
    - (nullable WKBackForwardListItem *)itemAtIndex:(NSInteger)index;
    //所有的可返回页面项 只要加载过 没有后退 都在
    @property (nonatomic, readonly, copy) NSArray<WKBackForwardListItem *> *backList;
    //所有后退出来的页面
    @property (nonatomic, readonly, copy) NSArray<WKBackForwardListItem *> *forwardList;

    WKWebViewConfiguration WKWebView的配置类

    //是不是所有的数据都加载到内存中 才开始渲染

    @property (nonatomic) BOOL suppressesIncrementalRendering;

    //是否允许HTML5内部播放

    @property (nonatomic) BOOL allowsInlineMediaPlayback;

    //是否允许自动播放 设置NO 不需要用户点击自动播放

    @property (nonatomic) BOOL requiresUserActionForMediaPlayback

    //是否可以AirPlay播放

    @property (nonatomic) BOOL mediaPlaybackAllowsAirPlay

    WKWebView加载方法

    //加载本地资源文件或者网络资源文件
    - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
    //加载本地的资源 iOS 9.0以后的方法
    - (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));

    但是这个方法,有一个ReadAccessToURL访问路径需要注意一下,需要把AccessToURL写到最公共的路径,保证资源都被加载,否则第一次加载的时候,会出不来:

    NSString *pathA = "file:///path/to/abc/dirA/A.html";//需要加载的资源路径1
    NSString *pathB = "file:///path/to/abc/dirB/B.html";//需要加载的资源路径2
    NSString *pathC = "file:///path/to/abc/dirC/C.html";//需要加载的资源路径3


    NSURL *url = [NSURL fileURLWithPath:pathA];

    NSURL *readAccessToURL = [[url URLByDeletingLastPathComponent] URLByDeletingLastPathComponent];
     // readAccessToURL == "file:///path/to/abc/"[self.wk_webview loadFileURL:url allowingReadAccessToURL:readAccessToURL];
    // then you want load  pathB
    url = [NSURL fileURLWithPath:pathB];
    // this will work fine
    [self.wk_webview loadFileURL:url allowingReadAccessToURL:readAccessToURL];
    //加载网页内容 网页内容转化成字符串 baseURL 必须要有 否则 网页中的相对路径内容无法加载。
     - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
    //加载网页消息  data网页的二进制数据 数据类型 MIMEType比如网页类型:text/html  characterEncodingName内容编码 utf-8 baseURL 资源的相对路径
    - (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL

    WKWebView执行JS

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

    //允许执行边缘手势返回

    @property (nonatomic) BOOL allowsBackForwardNavigationGestures;

    //UserAgent设置

    @property (nullable, nonatomic, copy) NSString *customUserAgent

    //设置链接预览功能 默认NO 需要长按预览

    @property (nonatomic) BOOL allowsLinkPreview API_AVAILABLE(macosx(10.11), ios(9.0));

    //重新加载 会从服务器重新加载一份 跟本地缓存比较 如果有变化 加载请求的内容

    - (nullable WKNavigation *)reloadFromOrigin;

     

     

    展开全文
  • 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 才是未来。

    展开全文
  • 随说 : 最近有个需求,是将公司的一个内网的页面嵌套在app中作为一个模块.这不是很简单的webView请求一下就行了么?其实内里大有乾坤.自己也将思路整理一遍 UIWebView UIWebView的基本使用方法 : ...

    随说 : 最近有个需求,是将公司的一个内网的页面嵌套在app中作为一个模块.这不是很简单的webView请求一下就行了么?其实内里大有乾坤.自己也将思路整理一遍

    UIWebView

    UIWebView的基本使用方法 :

    就这样就已经整整个baidu的页面展示到app上
    下面我们看一下webView的属性与方法

        UIWebView *webView = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
        self.view = webView;
        NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        [webView loadRequest:request];

    UIWebView的层级结构 :


    UIWebView的类继承关系

    UIWebView的属性 :

    // 代理属性 重点需要知道代理方法的使用
    @property (nullable, nonatomic, assign) id <UIWebViewDelegate> delegate;
    
    // 这个是webView内部的scrollView 只读,但是利用这个属性,设置scrollView的代理,就可以控制整个webView的滚动事件
    @property(nonatomic, readonly, strong) UIScrollView *scrollView;
    
    // webView的请求,这个属性一般在整个加载完成后才能拿到
    @property (nullable, nonatomic, readonly, strong) NSURLRequest *request;
    
    // A Boolean value indicating whether the receiver can move backward. (read-only)
    // If YES, able to move backward; otherwise, NO.
    // 如果这个属性为YES,才能后退
    @property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
    
    // A Boolean value indicating whether the receiver can move forward. (read-only)
    // If YES, able to move forward; otherwise, NO.
    // 如果这个属性为YES,才能前进
    @property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
    
    // A Boolean value indicating whether the receiver is done loading content. (read-only)
    // If YES, the receiver is still loading content; otherwise, NO.
    // 这个属性很好用,如果为YES证明webView还在加载数据,所有数据加载完毕后,webView就会为No
    @property (nonatomic, readonly, getter=isLoading) BOOL loading;
    
    //A Boolean value determining whether the webpage scales to fit the view and the user can change the scale.
    //If YES, the webpage is scaled to fit and the user can zoom in and zoom out. If NO, user zooming is disabled. The default value is NO.
    // YES代表网页可以缩放,NO代表不可以缩放
    @property (nonatomic) BOOL scalesPageToFit;
    
    // 设置某些数据变为链接形式,这个枚举可以设置如电话号,地址,邮箱等转化为链接
    @property (nonatomic) UIDataDetectorTypes dataDetectorTypes NS_AVAILABLE_IOS(3_0);
    
    // iPhone Safari defaults to NO. iPad Safari defaults to YES
    // 设置是否使用内联播放器播放视频
    @property (nonatomic) BOOL allowsInlineMediaPlayback NS_AVAILABLE_IOS(4_0); 
    
    // iPhone and iPad Safari both default to YES
    // 设置视频是否自动播放
    @property (nonatomic) BOOL mediaPlaybackRequiresUserAction NS_AVAILABLE_IOS(4_0); 
    
    // iPhone and iPad Safari both default to YES
    // 设置音频播放是否支持ari play功能
    @property (nonatomic) BOOL mediaPlaybackAllowsAirPlay NS_AVAILABLE_IOS(5_0); 
    
    // iPhone and iPad Safari both default to NO
    // 设置是否将数据加载入内存后渲染界面
    @property (nonatomic) BOOL suppressesIncrementalRendering NS_AVAILABLE_IOS(6_0); 
    
    // default is YES
    // 设置用户是否能打开keyboard交互
    @property (nonatomic) BOOL keyboardDisplayRequiresUserAction NS_AVAILABLE_IOS(6_0); 
    
    /* IOS7 */ 以后的新特性
    // 这个属性用来设置一种模式,当网页的大小超出view时,将网页以翻页的效果展示,枚举如下:
    @property (nonatomic) UIWebPaginationMode paginationMode NS_AVAILABLE_IOS(7_0);
    typedef NS_ENUM(NSInteger, UIWebPaginationMode) { 
    UIWebPaginationModeUnpaginated, //不使用翻页效果 
    UIWebPaginationModeLeftToRight, //将网页超出部分分页,从左向右进行翻页 
    UIWebPaginationModeTopToBottom, //将网页超出部分分页,从上向下进行翻页 
    UIWebPaginationModeBottomToTop, //将网页超出部分分页,从下向上进行翻页 
    UIWebPaginationModeRightToLeft //将网页超出部分分页,从右向左进行翻页 
    };
    
    // This property determines whether certain CSS properties regarding column- and page-breaking are honored or ignored. 
    // 这个属性决定CSS的属性分页是可用还是忽略。默认是UIWebPaginationBreakingModePage 
    @property (nonatomic) UIWebPaginationBreakingMode paginationBreakingMode NS_AVAILABLE_IOS(7_0);
    
    // 设置每一页的长度
    @property (nonatomic) CGFloat pageLength NS_AVAILABLE_IOS(7_0);
    
    // 设置每一页的间距
    @property (nonatomic) CGFloat gapBetweenPages NS_AVAILABLE_IOS(7_0);
    
    // 获取页数
    @property (nonatomic, readonly) NSUInteger pageCount NS_AVAILABLE_IOS(7_0);

    还有一些属性请详细翻苹果文档

    UIWebView的代理方法 :

    UIWebView的代理方法是用的最多的方法,并且一般来说,相对Web页面作处理都在这相应的4个方法中
    分别解释一下方法的调用情况

        // Sent before a web view begins loading a frame.请求发送前都会调用该方法,返回NO则不处理这个请求
        - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
    
        // Sent after a web view starts loading a frame. 请求发送之后开始接收响应之前会调用这个方法
        - (void)webViewDidStartLoad:(UIWebView *)webView;
    
        // Sent after a web view finishes loading a frame. 请求发送之后,并且服务器已经返回响应之后调用该方法
        - (void)webViewDidFinishLoad:(UIWebView *)webView;
    
        // Sent if a web view failed to load a frame. 网页请求失败则会调用该方法
        - (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;

    UIWebView的对象方法

    // 加载Data数据创建一个webView
    - (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)encodingName baseURL:(NSURL *)baseURL
    
    // 加载本地HTML创建一个webView
    - (void)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL
    
    // 加载一个请求创建一个webView
    - (void)loadRequest:(NSURLRequest *)request
    
    // 刷新网页
    - (void)reload;
    
    // 停止网页加载内容
    - (void)stopLoading;
    
    // 后退
    - (void)goBack;
    
    // 前进
    - (void)goForward;
    
    // 执行JS方法
    - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script






    WKWebView

    WKWebView的简介 :

    从文档中可以看到,这个是IOS8之后新增的一个类,也是苹果推崇的一个新的类


    WKWebView的类层级结构

    WKWebView的基本使用方法 :

    其实和UIWebView的用法没什么区别
    但是WKWebView相对于UIWebView强大了很多,内存的消耗相对少了,所提供的接口也丰富了。
    推荐使用
    多了一部操作就是需要包含webkit框架
    @import webkit

        WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
        self.view = webView;
        NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        [webView loadRequest:request];

    WKWebView的属性 :

    // UIWebView 中会自动保存Cookie,如果登录了一次下次再次进入的时候,会记住登录状态
    // 在WKWebView中,新增一个configuration属性,  configuration 让WKWebView知道登录状态,
    // configuration 可以通过已有的Cookie进行设置,也可以通过保存上一次的configuration进行设置
    // WKWebViewConfiguration类中也有一些相应的属性
    @property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
    
    // The methods of the WKNavigationDelegate protocol help you track the progress of the web site's main frame navigations and decide load policy for main frame and subframe navigations.
    // WKWebView中,加入了网站导航的概念,这个对象决定主框架导航加载方法协议。
    @property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
    
    // The WKUIDelegate class provides methods for presenting native user interface 
    elements on behalf of a webpage.
    // WKWebView中,加入了网站窗口的概念,这个对象决了webView窗口的一些方法协议。
    @property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
    
    A WKBackForwardList object is a list of webpages previously visited in a web view that can be reached by going back or forward.
    // WKWebView中,加入了网站列表的概念,这个WEBBackForwardList对象是以前在Web视图访问的网页,可以通过去后退或前进
    @property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

    还有很多方法,同样可以查文档看到

    WKWebView的代理方法 :

    有一些方法和UIWebView是基本一直的,但是因为返回了navigation,所能用到的属性多了很多,另外多了一些方法,将请求与相应的整个过程

    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{
        NSLog(@"webViewWebContentProcessDidTerminate:  当Web视图的网页内容被终止时调用。");
    }
    
    
    - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
    {
        [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
        NSLog(@"webView:didFinishNavigation:  响应渲染完成后调用该方法   webView : %@  -- navigation : %@  \n\n",webView,navigation);
    }
    
    
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
    {
        [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
        NSLog(@"webView:didStartProvisionalNavigation:  开始请求  \n\n");
    }
    
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
        NSLog(@"webView:didCommitNavigation:   响应的内容到达主页面的时候响应,刚准备开始渲染页面应用 \n\n");
    }
    
    
    // error
    - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
        // 类似 UIWebView 的- webView:didFailLoadWithError:
    
        NSLog(@"webView:didFailProvisionalNavigation:withError: 启动时加载数据发生错误就会调用这个方法。  \n\n");
    }
    
    
    
    - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error{
        NSLog(@"webView:didFailNavigation: 当一个正在提交的页面在跳转过程中出现错误时调用这个方法。  \n\n");
    }
    
    
    
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    
        NSLog(@"请求前会先进入这个方法  webView:decidePolicyForNavigationActiondecisionHandler: %@   \n\n  ",navigationAction.request);
    
        decisionHandler(WKNavigationActionPolicyAllow);
    
    }
    
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
    
        NSLog(@"返回响应前先会调用这个方法  并且已经能接收到响应webView:decidePolicyForNavigationResponse:decisionHandler: Response?%@  \n\n",navigationResponse.response);
    
        decisionHandler(WKNavigationResponsePolicyAllow);
    }
    
    
    
    - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
    
        NSLog(@"webView:didReceiveServerRedirectForProvisionalNavigation: 重定向的时候就会调用  \n\n");
    }

    WKWebView的对象方法 :

    这些方法,基本上和UIWebView中的使用用法是一致的,所以

    // 这是加载网页最常用的一种方式,通过一个网页URL来加载一个WKWebView,这个URL可以是远程的也可以是本地的,例如我加载百度的主页
    - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
    
    // 根据一个文件,加载一个WKWebView
    - (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0);
    
    // 这个方法需要将html文件读取为字符串从而加载为WKWebView,其中baseURL是我们自己设置的一个路径,用于寻找html文件中引用的图片等素材。
    - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
    
    // 这个方式使用的比较少,但也更加自由,其中data是文件数据,MIMEType是文件类型,characterEncodingName是编码类型,baseURL是素材资源路径
    - (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);

    基本使用

    下面会总结一些我在开发过程中遇到的坑,和解决问题的一些思路,不过在此之前我发现,如果要webView玩得好,有以下几点的只是也需要掌握好,因为我认为在H5崛起的今天,源生App和H5的交互之间会产生比较大改变,而且源生与H5之间的混编,越来越被重视.所以 :

    1. 源生技术,特别是有关于webView这一块的API要非常熟练,
    2. js语法, js的语法需要熟练,特别是操作document的几个常用js,标签需要用得滚瓜烂熟.
    3. 要非常了解网络请求 - 响应的机制,理解请求头,响应头,等等.HTTP的整套协议

    需求一 : 展示一个网页,但是需要隐藏一部分页面

    首先看看百度的页面,这是用Chrome浏览器打开的开发者模式
    基本界面组成如下,基本使用用法请详情百度,这里不作介绍


    假设现在想将这个Logo由网页开始加载就去掉


    百度的logo就是一个div套着一个image标签


    - (void)webViewDidFinishLoad:(UIWebView *)webView
    {
        // 在HTML标签都加载完成后,开始处理HTML标签,调用JS,操作document
        [webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('plus-card').remove();"];
    }

    就这样, logo标签就被去掉了,思路就是等HTML加载完成后,操作JS从而操作document标签从而改变整个html页面的应用,下图是去掉整个Body主题内容后的结果


    另外还可以将一段函数封装到里面,执行函数,原理是通过stringByEvaluatingJavaScriptFromString将JS函数写进head标签中,然后再调用该函数

    // 自定义editMyLogo函数
    [webView stringByEvaluatingJavaScriptFromString:@"var script = document.createElement('script');"
         "script.type = 'text/javascript';"
         "script.text = \"function editMyLogo() { "
         "var logo = document.getElementById('logo');"
         "logo.innerHTML= logo.innerHTML + '这是我自己定义的名字';"
         "var imglist = logo.getElementsByTagName('IMG');"
         "for (i=0 ; i < imglist.length ; i++ ){"
         "imglist[i].src = 'http://pic.to8to.com/attch/day_160218/20160218_d968438a2434b62ba59dH7q5KEzTS6OH.png';"
         "}"
         "}\";"
         "document.getElementsByTagName('head')[0].appendChild(script);"];
    
        // 执行editMyLogo函数
        [webView stringByEvaluatingJavaScriptFromString:@"editMyLogo();"];

    效果如下 :


    有几点问题,这种操作是在webViewDidFinishLoad方法下进行的,webViewDidFinishLoad方法是webView的document已经渲染好后,再去处理这个这个页面.

    1. 你会发现有时候会出现一些闪屏现象,原因是渲染过后,内部处理JS代码后,页面会再渲染一次
    2. 资源浪费,假设这边的需求只需要显示10%的内容,却要加载100%的内容,不过这一方面还需要网页端作出很好的适配
    3. 某些时候,JS会失效,不知道什么原因,有些时候自定义加载的JS的方法并没有执行到.等于内容并没有屏蔽
    4. 等等..

    需求二 : 怎样处理403,404的情况 ?


    @property (nonatomic, assign) BOOL isPost; // 定义一个变量
    
    // 每一个请求开始发送前都会调用这个方法
    // 1, 定义一个全局变量currentRequest,用作保存当前的请求
    // 2, 将请求转换成data,然后处理data再将data作为请求数据再次请求
    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
    
        if (!_isPost) {
            NSHTTPURLResponse *response = nil;
            NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
            if (response.statusCode == 404) {
                // 这里处理 404 代码
            } else if (response.statusCode == 403) {
                // 这里处理 403 代码
            } else {
    
                _isPost = true;
                [webView loadData:data MIMEType:@"text/html" textEncodingName:@"NSUTF8StringEncoding" baseURL:[request URL]];
    
            }
            return NO;
        }else{
            NSLog(@"\n\n shouldStartLoadWithRequest请求准备 --  %@ \n\n ",request);
            _isPost = NO;
            return YES;
        }
    }

    需求一 : 进一步改进


    在处理HTML这里,将你想隐藏的页面,加上 display:none 属性,
    或者,将整段HTML标签去掉.




    展开全文
  • WKWebView 的使用和踩过的坑

    万次阅读 2017-02-13 17:38:10
    iOS8之后,苹果推出了WebKit这个框架,用来替换原有的UIWebView,新的控件优点多多,不一一叙述。由于一直在适配iOS7,就没有去替换,现在仍掉了iOS7,以为很简单的就替换过来了,然而在替换的过程中,却遇到了很多...
  • WKWebView 常见问题

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

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

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

    千次阅读 2017-09-01 17:36:25
    wkWebView.isOpaque = false wkWebView.backgroundColor = UIColor.black
  • WKWebView 禁止跳转链接

    千次阅读 2018-01-15 13:46:12
    self.wkWebView.navigationDelegate = self; #pragma mark - wkWebViewDelegate //禁止链接跳转 - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction de
  • WKWebView显示空白页bug

    万次阅读 2015-12-27 15:20:58
    问题:用WKWebView加载网页,配置正确情况下,始终无法加载出网页,显示空白view 解决方法:用源码方式打开info.plist ,在里面加入如下代码:(原因是,这是因为 iOS 9 SDK 中默认不再支持访问非 HTTPS 的地址,...
  • 使用wkwebview 加载淘宝页面 比浏览器要慢 大神们谁有什么解决的办法
  • 一、创建WKWebView的时候设置WKWebViewConfiguration的属性_allowUniversalAccessFromFileURLs为YES,代码如下  WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];  [configuration...
  • WKWebView 白屏问题

    万次阅读 2017-01-20 16:43:57
    1、WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,...
  • WKWebView 内容显示不全问题

    千次阅读 2017-08-03 14:35:53
    最近项目在嵌套h5 网页,使用苹果WKWebView 来展示,在5s 上展示是可以的,但是在6 或者 7 上会显示不全。 全是感觉WKWebView 跑到屏幕外边去了。设置:self.navigationController.navigationBar.translucent = NO;...
  • WKWebView 点击链接无反应

    千次阅读 2016-05-13 19:54:38
    wkWebView 点击链接无反应  多半是因为网页中有target="_blank" 在新窗口打开链接 而你有没有实现...-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfigurati
  • WKWebView加载网页加载不出来问题

    万次阅读 2018-08-01 13:11:34
    之前有一个项目一直使用WKWebView,比UIWebView占用性能少很多,而且很流畅。网上有很多小伙伴遇到一个问题,WKWebView加载网页加载不出来,白屏等,于是就说WK还不成熟。 不要轻易下结论收WK还不成熟,反正我用...
  • 情况是这样的,访问的资源是 --》 外网连接 vpn,然后访问内网的资源; 加载不出来, 用UIWebView 可以;刚开始我以为是cookie的问题;... wkwebview 是可以访问到的;通过vpn访问就会有问题;大家有遇过吗?
  • WKWebView禁止放大缩小(捏合手势)

    千次阅读 2019-04-10 14:58:09
    在webView加载完毕之后的delegate中,添加下面的JS代码禁止WKWebView放大缩小 // 加载完毕 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { NSString *...
1 2 3 4 5 ... 20
收藏数 6,528
精华内容 2,611
关键字:

wkwebview