美文网首页
iOS WKWebView的使用

iOS WKWebView的使用

作者: 小瓶子Zgp | 来源:发表于2021-02-04 14:48 被阅读0次
    • WKWebView需要iOS9或更高版本

    优点

    1.多进程,在app的主进程之外执行
    2.使用更快的Nitro JavaScript引擎
    3.异步执行处理JavaScript
    4.消除某些触摸延迟
    5.支持服务端的身份校验
    6.支持对错误的自签名安全证书和证书进行身份验证

    问题

    1.需要iOS9或更高版本(WKWebView在iOS8引入,但是很多功能,支持比较全面在iOS9以后的版本)
    2.不支持通过AJAX请求本地存储的文件
    3.不支持"Accept Cookies"的设置
    4.不支持"Advanced Cache Settings"(高级缓存设置)
    5.App退出会清除HTML5的本地存储的数据
    6.不支持记录WebKit的请求
    7.不能进行截屏操作

    具体翻译文参考:WKWebView相比于UIWebView浏览器之间内核引擎的区别
    原文 WKWebView: Differences from UIWebView browsing engine

    一、WKWebView的基本初始化

    • 需要引入 #import <WebKit/WebKit.h>
    - (WKWebView *)wkWebview
    {
        if (!_wkWebview) {
            // 0.网页配置对象
            WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
            // 1.原生与JS交互管理
            WKUserContentController *userContentController = [[WKUserContentController alloc] init];
            
            /// 解决循环引用
            // 0.在viewdisAppear方法中
    //        [self.wkWebview.configuration.userContentController removeScriptMessageHandlerForName:@"ScanAction"];
    //        [userContentController addScriptMessageHandler:self name:@"ScanAction"];
            
            // 1.继承系统的NSProxy
    //        [userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[XPZ_Proxy proxyWithTarget:self] name:@"ScanAction"];
            
            // 2.自定义NSProxy
    //        [userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[XPZ_CustomProxy proxyWithTarget:self] name:@"ScanAction"];
            
            // 3.自定义WKScriptMessageHandler
            XPZ_WKWeakScriptMessageHandler *scriptMessageHandle = [[XPZ_WKWeakScriptMessageHandler alloc] initWithScriptMessageHandlerWith:self];
            [userContentController addScriptMessageHandler:scriptMessageHandle name:@"ScanAction"];
            // 添加
            config.userContentController = userContentController;
            
            // 3.WKWebview设置
            WKPreferences *prefer = [[WKPreferences alloc] init];
            //设置是否支持javaScript 默认是支持的
            prefer.javaScriptEnabled = true;
            // /最小字体大小
            prefer.minimumFontSize = 40.0;
            // // 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
            prefer.javaScriptCanOpenWindowsAutomatically = true;
            // 添加
            config.preferences = prefer;
            
            // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
            config.allowsInlineMediaPlayback = YES;
            //设置视频是否需要用户手动播放  设置为NO则会允许自动播放
            config.mediaTypesRequiringUserActionForPlayback = YES;
            //设置是否允许画中画技术 在特定设备上有效
            config.allowsPictureInPictureMediaPlayback = YES;
            //设置请求的User-Agent信息中应用程序名称 iOS9后可用
            config.applicationNameForUserAgent = @"ChinaDailyForiPad";
            
            _wkWebview = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config];
            _wkWebview.UIDelegate = self;
            _wkWebview.navigationDelegate = self;
            // 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
            _wkWebview.allowsBackForwardNavigationGestures = YES;
            //可返回的页面列表, 存储已打开过的网页
            WKBackForwardList * backForwardList = [_wkWebview backForwardList];
            //页面后退
            [_wkWebview goBack];
            //页面前进
            [_wkWebview goForward];
            //刷新当前页面
            [_wkWebview reload];
            
            [self.view addSubview:_wkWebview];
        }
        return _wkWebview;
    }
    
    • 主要说下WKUserContentController:这个类主要用来做native与JavaScript的交互管理,依靠 WKScriptMessageHandler 协议
    • 主要用到以下方法
    // 添加脚本信息
    - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
    // 移除脚本信息
    - (void)removeScriptMessageHandlerForName:(NSString *)name;
    // 例:
    [userContentController addScriptMessageHandler:scriptMessageHandle name:@"ScanAction"];
    
    • JS中代码
    function scanClick() {
                    window.webkit.messageHandlers.ScanAction.postMessage(null);
                }
    
    • 对应的协议方法,专门用来处理监听JavaScript方法从而调用原生OC方法
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
    {
    //    message.body  --  Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.
    //    NSDictionary *bodyParam = (NSDictionary*)message.body;
    //    NSString *func = [bodyParam objectForKey:@"function"];
    //    
    //    NSLog(@"MessageHandler Name:%@", message.name);
    //    NSLog(@"MessageHandler Body:%@", message.body);
    //    NSLog(@"MessageHandler Function:%@",func);
        if ([message.name isEqualToString:@"ScanAction"]) {
            NSLog(@"扫一扫");
        }
        
        // 将结果返回给js
        NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"广东省深圳市南山区学府路XXXX号"];
        [self.wkWebview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
            NSLog(@"%@----%@",result, error);
        }];
    }
    
    • ⚠️在使用 addScriptMessageHandler: 方法时会造成内存泄漏

    [configuration.userContentController addScriptMessageHandler:self name:name]

    这里 userContentController 持有了self ,然后 userContentController 又被configuration持有,最终被wkwebview持有,然后wkwebview是self的一个成员变量,所以self也持有 self,所以就造成了循环引用,导致界面不会被释放

    • 解决办法, 以下提供了4种方案
    1. 使用 removeScriptMessageHandlerForName:
    - (void)viewWillDisappear:(BOOL)animated
    {
       [super viewWillDisappear:animated];
       [self.wkWebview.configuration.userContentController removeScriptMessageHandlerForName:@"ScanAction"];
    }
    
    1. 创建继承系统的NSProxy的类
       [userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[XPZ_Proxy proxyWithTarget:self] name:@"ScanAction"];
    
       // NSProxy类中的代码
    
      // XPZ_Proxy.h 中代码
    @interface XPZ_Proxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target;
    @end
       
      // XPZ_Proxy.m 中代码
    @implementation XPZ_Proxy
    + (instancetype)proxyWithTarget:(id)target
    {
       // NSProxy对象不需要调用init,因为它本来就没有init方法
       XPZ_Proxy *proxy = [XPZ_Proxy alloc];
       proxy.target = target;
       return proxy;
    }
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
       return [self.target methodSignatureForSelector:sel];
    }
    - (void)forwardInvocation:(NSInvocation *)invocation
    {
       [invocation invokeWithTarget:self.target];
    }
    @end
    
    
    1. 创建自定义的NSProxy类
     // XPZ_CustomProxy.h 中代码
    @interface XPZ_CustomProxy : NSObject
    @property (nonatomic, weak) id target;
    + (instancetype)proxyWithTarget:(id)target;
    @end
    
    // XPZ_CustomProxy.m 中代码
    @implementation XPZ_CustomProxy
    + (instancetype)proxyWithTarget:(id)target
    {
        XPZ_CustomProxy *proxy = [[XPZ_CustomProxy alloc] init];
        proxy.target = target;
        return proxy;
    }
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return self.target;
    }
    @end
    
    
    1. 自定义WKScriptMessageHandler
    //XPZ_WKWeakScriptMessageHandler.h 中代码
    @interface XPZ_WKWeakScriptMessageHandler : NSObject <WKScriptMessageHandler>
    
    - (instancetype)initWithScriptMessageHandlerWith:(id<WKScriptMessageHandler>)scriptMessageHandler;
    
    @property (nonatomic, weak, readonly) id<WKScriptMessageHandler> scriptMessageHandler;
    @end
      
    // XPZ_WKWeakScriptMessageHandler.m 中代码
    - (instancetype)initWithScriptMessageHandlerWith:(id<WKScriptMessageHandler>)scriptMessageHandler
    {
        self = [super init];
        if (self) {
            _scriptMessageHandler = scriptMessageHandler;
        }
        return self;
    }
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
    {
        [self userContentController:userContentController didReceiveScriptMessage:message];
    }
    @end
    

    二、WKWebView的代理方法

    1.WKUIDelegate

    #pragma mark WKUIDelegate
    // 警告
    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
    {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction * action = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
            completionHandler();
        }];
        [alert addAction:action];
        
        [self presentViewController:alert animated:YES completion:nil];
    }
    // 确认框
    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
        //    DLOG(@"msg = %@ frmae = %@",message,frame);
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            completionHandler(NO);
        }])];
        [alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            completionHandler(YES);
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
    }
    // 输入框
    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
            textField.text = defaultText;
        }];
        [alertController addAction:([UIAlertAction actionWithTitle:@"完成" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            completionHandler(alertController.textFields[0].text?:@"");
        }])];
    
    
        [self presentViewController:alertController animated:YES completion:nil];
    }
    // 创建一个新的WebView
    - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
        return [[WKWebView alloc]init];
    }
    

    2.WKNavigationDelegate

    #pragma mark - WKNavigationDelegate
    /*
     WKNavigationDelegate主要处理一些跳转、加载处理操作,WKUIDelegate主要处理JS脚本,确认框,警告框等
     */
    
    // 页面开始加载时调用
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    }
    
    // 页面加载失败时调用
    - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
        [self.progressView setProgress:0.0f animated:NO];
    }
    
    // 当内容开始返回时调用
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
        
    }
    
    // 页面加载完成之后调用
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    
        // 设置字体
    //    NSString *fontFamilyStr = @"document.getElementsByTagName('body')[0].style.fontFamily='Arial';";
    //    [webView evaluateJavaScript:fontFamilyStr completionHandler:nil];
    //    //设置颜色
    //    [ webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextFillColor= '#9098b8'" completionHandler:nil];
    //    //修改字体大小
    //    [ webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '200%'"completionHandler:nil];
    }
    
    //提交发生错误时调用
    - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
        [self.progressView setProgress:0.0f animated:NO];
    }
    
    // 接收到服务器跳转请求即服务重定向时之后调用
    - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
        
    }
    // 在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    
        NSString *href = navigationAction.request.URL.absoluteString;
        if ([href hasPrefix:@"http"]||[href hasPrefix:@"https"]) {
            
        }
        if ([href hasPrefix:@"config:"]) {
            // parse and get json, then ...
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        decisionHandler(WKNavigationActionPolicyAllow);
    }
    

    3.WKScriptMessageHandler

    #pragma mark WKScriptMessageHandler
    // 用来处理监听JavaScript方法从而调用原生OC方法
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
    {
    //    message.body  --  Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.
    //    NSDictionary *bodyParam = (NSDictionary*)message.body;
    //    NSString *func = [bodyParam objectForKey:@"function"];
    //    
    //    NSLog(@"MessageHandler Name:%@", message.name);
    //    NSLog(@"MessageHandler Body:%@", message.body);
    //    NSLog(@"MessageHandler Function:%@",func);
        if ([message.name isEqualToString:@"ScanAction"]) {
            NSLog(@"扫一扫");
        }
        
        // 将结果返回给js
        NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"广东省深圳市南山区学府路XXXX号"];
        [self.wkWebview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
            NSLog(@"%@----%@",result, error);
        }];
    }
    

    三、JS与OC的交互

    1.JavaScriptCore

    • UIWebView
    // 创建JSContext
        JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        self.context = context;
        // 调用系统相机
        context[@"iOSCamera"] = ^(){
            dispatch_async(dispatch_get_main_queue(), ^{
                
         });
            return @"调用相机";
        };
        
        // callWithArguments:
        JSValue *labelAction = self.context[@"picCallback"];
        [labelAction callWithArguments:@[@"参数"]];
    

    在从UIWebView过度到WkWebView,我们还向之前使用UIWebView那样,在页面加载完成后,获取JSContext上下文
    会发现在 self.jsContext = [_wkWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; 这里崩了,原因就是 WKWebView 不支持 JavaScriptCore 的方式, 但提供 messagehandler 的方式为JavaScript与OC通信;
    到这里我们会想如何拿到WKWebView JsContext上下文,可是很遗憾我们无法获取上下文,因为布局和JavaScript是在另一个进程上处理的。

    2.MessageHandler

    这个方法上面提到过,主要是依靠WKScriptMessageHandler协议类和WKUserContentController两个类:WKUserContentController对象负责注册JS方法,设置处理接收JS方法的代理,代理遵守WKScriptMessageHandler,实现捕捉到JS消息的回调方法。

    • 上面是JS调用OC,补充一下OC调用JS方法
        // 将结果返回给js
        NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"广东省深圳市南山区学府路XXXX号"];
        [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
            NSLog(@"%@----%@",result, error);
        }];
    
    • JS中代码
     function setLocation(location) {
                    asyncAlert(location);
                    document.getElementById("returnValue").value = location;
                }
    

    3.WebViewJavascriptBridge

    通过CocoaPods集成
    WebViewJavascriptBridge
    在工程的Podfile里面添加以下代码:

       pod 'WebViewJavascriptBridge'
    
    1. 引入头文件
        #import <WKWebViewJavascriptBridge.h>
    
    1. 初始化 WKWebViewJavascriptBridge
    _webViewBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebview];
    [_webViewBridge setWebViewDelegate:self];
    
    1. 注册并调用js方法
     // js调用原生
        [_webViewBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
            NSLog(@"data : %@",data);
            responseCallback(@"12345678");
        }];
        
        // 原生调用js方法
         //    // 如果不需要参数,不需要回调,使用这个
        //    [_webViewBridge callHandler:@"testJSFunction"];
        //    // 如果需要参数,不需要回调,使用这个
        //    [_webViewBridge callHandler:@"testJSFunction" data:@"一个字符串"];
        // 如果既需要参数,又需要回调,使用这个
        [_webViewBridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
            NSLog(@"调用完JS后的回调:%@",responseData);
        }];
    
    1. 复制并粘贴到您的 JS 中:setupWebViewJavascriptBridge
       function setupWebViewJavascriptBridge(callback) {
       if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
       if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
       window.WVJBCallbacks = [callback];
       var WVJBIframe = document.createElement('iframe');
       WVJBIframe.style.display = 'none';
       WVJBIframe.src = 'https://__bridge_loaded__';
       document.documentElement.appendChild(WVJBIframe);
       setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }
    
    1. 最后调用 setupWebViewJavascriptBridge
        WebViewJavascriptBridge.callHandler('scanClick', {'foo': 'bar'}, function(response) {
                        alert('扫描结果:' + response);
                        document.getElementById("returnValue").value = response;
                    })
                    
        setupWebViewJavascriptBridge(function(bridge) {
                     bridge.registerHandler('testJSFunction', function(data, responseCallback) {
                        alert('JS方法被调用:'+data);
                        responseCallback('js执行过了');
                     })
                })
    

    4.拦截URL

    #pragma mark - WKNavigationDelegate
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
    {
        NSURL *URL = navigationAction.request.URL;
        NSString *scheme = [URL scheme];
        if ([scheme isEqualToString:@""]) {
            
            // 在这里解析URL
            // 需要调用js方法 还可以通过以下这种方法插入js例:
            // 将结果返回给js
        // NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"广东省深圳市南山区学府路XXXX号"];
        // [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
    //        NSLog(@"%@----%@",result, error);
    //    }];
                    
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        decisionHandler(WKNavigationActionPolicyAllow);
    }
    

    四.加载进度条和title的监听

    注意:
    iOS9之前,被观察这对观察者之间是unsafe_unretain引用,观察者释放之后会造成野指针
    而iOS9 之后是weak引用关系,对象释放之后,指针也释放,不会崩溃

    通知NSNotification在注册者被回收时需要手动移除,是一直以来的使用准则。原因是在MRC时代,通知中心持有的是注册者的unsafe_unretained指针,在注册者被回收时若不对通知进行手动移除,则指针指向被回收的内存区域,成为野指针。这时再发送通知,便会造成crash。而在iOS 9以后,通知中心持有的是注册者的weak指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。我们知道,向空指针发送消息是不会有问题的。

    ⚠️ 但是有一个例外。如果用- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));这个API来注册通知,可以直接传入block类型参数。使用这个API会导致注册者被系统retain,因此仍然需要像以前一样手动移除通知,同时这个block类型参数也需注意避免循环引用。

    • 所以不再需要写移除观察者方法

      [self.wkWebview removeObserver:self
                      forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
      [self.wkWebview removeObserver:self
                    forKeyPath:NSStringFromSelector(@selector(title))];
      
    • 添加监测网页加载进度的观察者

        [self.wkWebview addObserver:self
                       forKeyPath:@"estimatedProgress"
                          options:0
                          context:nil];
        //添加监测网页标题title的观察者
        [self.wkWebview addObserver:self
                       forKeyPath:@"title"
                          options:NSKeyValueObservingOptionNew
                          context:nil];
                          
        #pragma mark kvo 监听进度 必须实现此方法
        -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary<NSKeyValueChangeKey,id>     *)change
                          context:(void *)context{
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]
            && object == _wkWebview) {
            NSLog(@"网页加载进度 = %f",_wkWebview.estimatedProgress);
            self.progressView.progress = _wkWebview.estimatedProgress;
            if (_wkWebview.estimatedProgress >= 1.0f) {
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    self.progressView.progress = 0;
                });
            }
        }else if([keyPath isEqualToString:@"title"]
                 && object == _wkWebview){
            self.navigationItem.title = _wkWebview.title;
        }else{
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
    }
    

    相关文章

      网友评论

          本文标题:iOS WKWebView的使用

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