- 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种方案
- 使用
removeScriptMessageHandlerForName:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.wkWebview.configuration.userContentController removeScriptMessageHandlerForName:@"ScanAction"];
}
- 创建继承系统的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
- 创建自定义的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
- 自定义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'
- 引入头文件
#import <WKWebViewJavascriptBridge.h>
- 初始化 WKWebViewJavascriptBridge
_webViewBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebview];
[_webViewBridge setWebViewDelegate:self];
- 注册并调用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);
}];
- 复制并粘贴到您的 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)
}
- 最后调用
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];
}
}