iOS WKWebView的使用

  • 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
- (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)[XPZ_Proxy proxyWithTarget:self] name:@"ScanAction"];
        
        // 2.自定义NSProxy
//        [userContentController addScriptMessageHandler:(id)[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 )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)[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 

- (instancetype)initWithScriptMessageHandlerWith:(id)scriptMessageHandler;

@property (nonatomic, weak, readonly) id scriptMessageHandler;
@end
  
// XPZ_WKWeakScriptMessageHandler.m 中代码
- (instancetype)initWithScriptMessageHandlerWith:(id)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 
  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 )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     *)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的使用)