IOS进阶之WKWebView

作者: o翻滚的牛宝宝o | 来源:发表于2016-10-08 14:41 被阅读42178次
标题图片

前言


Xcode8发布以后,编译器开始不支持IOS7,所以很多应用在适配IOS10之后都不在适配IOS7了,其中包括了很多大公司,网易新闻,滴滴出行等。因此,我们公司的应用也打算淘汰IOS7。

支持到IOS8,第一个要改的自然是用WKWebView替换原来的UIWebView。WKWebView有很多明显优势:

  • 更多的支持HTML5的特性

  • 官方宣称的高达60fps的滚动刷新率以及内置手势

  • 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。文档

  • Safari相同的JavaScript引擎

  • 占用更少的内存

UIWebView

UIWebView

WKWebView

WKWebView

因此,使用WkWebview替换UIWebView还是很有必要的。

基本使用方法


WKWebView有两个delegate,WKUIDelegateWKNavigationDelegate。WKNavigationDelegate主要处理一些跳转、加载处理操作,WKUIDelegate主要处理JS脚本,确认框,警告框等。因此WKNavigationDelegate更加常用。

比较常用的方法:

#pragma mark - lifeCircle
- (void)viewDidLoad {
    [super viewDidLoad];
    webView = [[WKWebView alloc]init];
    [self.view addSubview:webView];
    [webView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view);
        make.right.equalTo(self.view);
        make.top.equalTo(self.view);
        make.bottom.equalTo(self.view);
    }];
    webView.UIDelegate = self;
    webView.navigationDelegate = self;
    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];

}

#pragma mark - WKNavigationDelegate
// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{

}
// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{

}
// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{

}
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{

}
// 接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{

}
// 在收到响应后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{

    NSLog(@"%@",navigationResponse.response.URL.absoluteString);
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}
// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

     NSLog(@"%@",navigationAction.request.URL.absoluteString);
    //允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationActionPolicyCancel);
}
#pragma mark - WKUIDelegate
// 创建一个新的WebView
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
    return [[WKWebView alloc]init];
}
// 输入框
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{
    completionHandler(@"http");
}
// 确认框
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
    completionHandler(YES);
}
// 警告框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
    NSLog(@"%@",message);
    completionHandler();
}

OC与JS交互


WKWebview提供了API实现js交互 不需要借助JavaScriptCore或者webJavaScriptBridge。使用WKUserContentController实现js native交互。简单的说就是先注册约定好的方法,然后再调用。

JS调用OC方法

oc代码(有误,内存不释放):

@interface ViewController ()<WKUIDelegate,WKNavigationDelegate,WKScriptMessageHandler>{
    WKWebView * webView;
    WKUserContentController* userContentController;
}
@end
@implementation ViewController
#pragma mark - lifeCircle
- (void)viewDidLoad {
    [super viewDidLoad];
    //配置环境
    WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc]init];
    userContentController =[[WKUserContentController alloc]init];
    configuration.userContentController = userContentController;
    webView = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration];
    //注册方法
    [userContentController addScriptMessageHandler:self  name:@"sayhello"];//注册一个name为sayhello的js方法

    [self.view addSubview:webView];
    [webView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view);
        make.right.equalTo(self.view);
        make.top.equalTo(self.view);
        make.bottom.equalTo(self.view);
    }];
    webView.UIDelegate = self;
    webView.navigationDelegate = self;
    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.test.com"]]];
}
- (void)dealloc{
    //这里需要注意,前面增加过的方法一定要remove掉。
    [userContentController removeScriptMessageHandlerForName:@"sayhello"];
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo);
}
@end

上面的OC代码如果认证测试一下就会发现dealloc并不会执行,这样肯定是不行的,会造成内存泄漏。原因是[userContentController addScriptMessageHandler:self name:@"sayhello"];这句代码造成无法释放内存。(ps:试了下用weak指针还是不能释放,不知道是什么原因。)因此还需要进一步改进,正确的写法是用一个新的controller来处理,新的controller再绕用delegate绕回来。

oc代码(正确写法):

@interface ViewController ()<WKUIDelegate,WKNavigationDelegate,WKScriptMessageHandler>{
    WKWebView * webView;
    WKUserContentController* userContentController;
}
@end
@implementation ViewController
#pragma mark - lifeCircle
- (void)viewDidLoad {
    [super viewDidLoad];
    //配置环境
    WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc]init];
    userContentController =[[WKUserContentController alloc]init];
    configuration.userContentController = userContentController;
    webView = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration];
    //注册方法
    WKDelegateController * delegateController = [[WKDelegateController alloc]init];
    delegateController.delegate = self;

    [userContentController addScriptMessageHandler:delegateController  name:@"sayhello"];

    [self.view addSubview:webView];
    [webView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view);
        make.right.equalTo(self.view);
        make.top.equalTo(self.view);
        make.bottom.equalTo(self.view);
    }];
    webView.UIDelegate = self;
    webView.navigationDelegate = self;
    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.test.com"]]];
}
- (void)dealloc{
    //这里需要注意,前面增加过的方法一定要remove掉。
    [userContentController removeScriptMessageHandlerForName:@"sayhello"];
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo);
}
@end

WKDelegateController代码:

#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
@protocol WKDelegate <NSObject>

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

@end

@interface WKDelegateController : UIViewController <WKScriptMessageHandler>

@property (weak , nonatomic) id<WKDelegate> delegate;

@end

.m代码:

#import "WKDelegateController.h"

@interface WKDelegateController ()

@end

@implementation WKDelegateController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([self.delegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) {
        [self.delegate userContentController:userContentController didReceiveScriptMessage:message];
    }
}


@end

h5代码:

<html>
<head>
    <script>
function say()
{
//前端需要用 window.webkit.messageHandlers.注册的方法名.postMessage({body:传输的数据} 来给native发送消息
    window.webkit.messageHandlers.sayhello.postMessage({body: 'hello world!'});
}
</script>
</head>
    <body>
        <h1>hello world</h1>
        <button onclick="say()">say hello</button>
    </body>

</html>

打印出的log:

 name:sayhello
 body:{
    body = "hello world!";
}
 frameInfo:<WKFrameInfo: 0x7f872060ce20; isMainFrame = YES; request =   <NSMutableURLRequest: 0x618000010a30> { URL: http://www.test.com/ }>

注意点

  • addScriptMessageHandler要和removeScriptMessageHandlerForName配套出现,否则会造成内存泄漏。
  • h5只能传一个参数,如果需要多个参数就需要用字典或者json组装。

oc调用JS方法

代码如下:

- (void)webView:(WKWebView *)tmpWebView didFinishNavigation:(WKNavigation *)navigation{

    //say()是JS方法名,completionHandler是异步回调block
    [webView evaluateJavaScript:@"say()" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"%@",result);
    }];
    
}

h5代码同上。

WebViewJavascriptBridge


一般来说,一个好的UI总有一个大神会开发出一个好的第三方封装框架。WebViewJavascriptBridge的作者也做了一套支持WKWebView与JS交互的第三方框架:WKWebViewJavascriptBridge。

主要方法如下:

//初始化方法
+ (instancetype)bridgeForWebView:(WKWebView*)webView;
+ (void)enableLogging;

//注册函数名
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;

//调用函数名
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;

//重置
- (void)reset;

//设置WKNavigationDelegate
- (void)setWebViewDelegate:(id<WKNavigationDelegate>)webViewDelegate;

基本的实现方法和上面写的差不多,就是封装了一下,有兴趣的童鞋可以自己pod下来使用。


我是翻滚的牛宝宝,欢迎大家评论交流~

补充


前不久把项目中的UIWebView更新到WkWebView,解决了一大堆问题,但是也遗留了一大堆问题,比方说cookie。

以前UIWebView会自动去NSHTTPCookieStorage中读取cookie,但是WKWebView并不会去读取,因此导致cookie丢失以及一系列问题,解决方式就是在request中手动帮其添加上。

mainWebView.UIDelegate = self;
mainWebView.navigationDelegate = self;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.test.com"]];
[request addValue:[self readCurrentCookieWithDomain:@"http://www.test.com/"] forHTTPHeaderField:@"Cookie"];
[mainWebView loadRequest:request];

- (NSString *)readCurrentCookieWithDomain:(NSString *)domainStr{
    NSHTTPCookieStorage*cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    NSMutableString * cookieString = [[NSMutableString alloc]init];
    for (NSHTTPCookie*cookie in [cookieJar cookies]) {
        [cookieString appendFormat:@"%@=%@;",cookie.name,cookie.value];
    }

//删除最后一个“;”
    [cookieString deleteCharactersInRange:NSMakeRange(cookieString.length - 1, 1)];
    return cookieString;
}

但是这只能解决第一次进入的cookie问题,如果页面内跳转(a标签等)还是取不到cookie,因此还要再加代码。

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

   //取出cookie
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    //js函数
    NSString *JSFuncString =
@"function setCookie(name,value,expires)\
    {\
    var oDate=new Date();\
    oDate.setDate(oDate.getDate()+expires);\
document.cookie=name+'='+value+';expires='+oDate+';path=/'\
    }\
    function getCookie(name)\
    {\
    var arr = document.cookie.match(new RegExp('(^| )'+name+'=([^;]*)(;|$)'));\
    if(arr != null) return unescape(arr[2]); return null;\
    }\
    function delCookie(name)\
    {\
    var exp = new Date();\
    exp.setTime(exp.getTime() - 1);\
    var cval=getCookie(name);\
    if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString();\
    }";

    //拼凑js字符串
    NSMutableString *JSCookieString = JSFuncString.mutableCopy;
    for (NSHTTPCookie *cookie in cookieStorage.cookies) {
    NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", cookie.name, cookie.value];
        [JSCookieString appendString:excuteJSString];
    }
    //执行js
    [webView evaluateJavaScript:JSCookieString completionHandler:nil];

}

相关文章

网友评论

  • 大冰子gg:内存问题不需要用新的controller绕用delegate,只需要在退出界面的时候写[webView.configuration.userContentController removeScriptMessageHandlerForName:@"name"];就行
  • SDBridge:分享一个Demo. WKWebView 监听JS端的所有的console.log日志
    https://www.jianshu.com/p/09e4799c5328
    https://github.com/housenkui/WKWebView-Console/
  • 阿莫司林:https://m.toutiao.com/i6586442406311232004/
    大神怎么监测上面这个链接,点击打开后不自动跳到“头条”,想在跳之前加个Alert
    阿莫司林:或者这样说,点击那个链接的所有事件,监测到有要离开App的事件的时候,做个Alert让用户确认
  • 你是我此世不渝的执着:h5只能传一个参数,如果需要多个参数就需要用字典或者json组装。 大佬这个需要怎么改
    月夜芳华:包装一个data字典数据或者json数据
  • edison0428:你好,请问下,怎么在webview里的跳转加上header,cookier已经加上l
  • LazyJ:UIwebview出现的刷新界面JS对象丢失,在WK上是不是就是这个手动读取cookies解决的
  • YimG:贼6666666666
  • Ko_Neko:博主你好,请问下 下面这个方法在点击网页的时候不执行是为什么呢?

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo);
    }
    selice:同问
  • 当优秀成为习惯:楼主,内存泄漏的原因找到了吗
  • 来呀快活呀哈哈:我现在遇到个问题,当加载HTML的过程中,还没加载完就调用原生注册的方法不行,只有加载完成再调用原生方法才可以。就相当于HTML页面先拿到放在我们原生的数据在去渲染他们的页面。
  • 乱尘:我是第一百个喜欢的呢
  • Hunter琼:代码呢?/
  • 怎样m:博主 我的app web cookie 真机可以同步加载数据没问题 但是模拟器加载网页 cookie就失效了 有遇到嘛
  • 美的不像话:用window.open打开新页面,让webview同时存在两个页面可以么
  • 美的不像话:我现在就遇到 wkwebview 的坑了, 在 web 中,html 页面,open 打开一个 window ,wkwebview 经过 createWebViewWithConfiguration 方法 指挥重新加载或者重新建立一个 web 吗? 我现在的需求是 open 打开的 html 页面还可以给上个 html 页面 postmessage 数据
    o翻滚的牛宝宝o:@美的不像话 oc与js交互就能实现啊,第二个界面的js给controller值,然后传给上一个controller,然后上一个controller与js交互就行了
    美的不像话:@o翻滚的牛宝宝o 有一个网站用safari浏览的话,点一个链接会打开一个新窗口,
    在新窗口里面选择某一项之后,新窗口自动关闭并且把选中的值传到旧窗口的输入框里面。
    o翻滚的牛宝宝o:@美的不像话 不知道你在说什么:cry:
  • dc3a8dbf4c69:楼主,你好,感谢你的分享。已经打赏
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
    如果是页面内跳转,这个方法不会被调用。页面内跳转(不重定向),有什么方式可以让这个方法调用呢?
    我需要知道什么什么时候我的页面加载完。
    13149a3d9e65:@dc3a8dbf4c69 js 中document有一个readyState 属性 , 你可以判断这个属性 的值 来获取 页面渲染完成的时机.
    1.uninitialized : 还没开始加载

    2.loading : 加载中

    3.loaded : 加载完成

    4.interactive : 结束渲染,用户已经可以与网页进行交互。但内嵌资源还在加载中

    5.complete : 完全加载完成
    dc3a8dbf4c69:@o翻滚的牛宝宝o 尝试过这个方式,estimatedProgress和isLoading 我都监听过。看到的现象也是一样的,并不能满足要求:cold_sweat:
    o翻滚的牛宝宝o:你用KVO监听webView的estimatedProgress属性试试看。我用百度测试,大部分跳转方式都能监听到。
  • BurNIng110:感谢
  • 小朴同学:当在页面中,点击一个网络链接且以下方法这么写的话
    - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    return [[WKWebView alloc] init];
    }
    APP 直接crash。解决方法是
    一,return nill
    二,如果你想在本界面加载这个链接的话
    if (!navigationAction.targetFrame.isMainFrame) {
    [webView loadRequest:navigationAction.request];
    }
    return nil;
    三,直接不实现这个方法。
  • 9efe3ac98eed:感谢分享解决一个困扰我一天的bug :smile:
  • 8978c16619df:APP第一次使用的时候,wkwebview在获取第三方cookie的时候为什么cookie会丢失?把app关闭了再打开就不会出现这样的问题,有什么办法能够处理第一次丢失的问题吗
  • initial_J:你好,请问 怎么监听WKWebView的滚动? scrollView的代理方法不执行?
    生无可恋的程序员:webView.scrollView.delegate = self;
  • 没有黑眼圈de熊猫:为什么W KebView loadHTMLString会比UIWebView loadHTMLString里面的字体要小呢???这是什么原因呢
    Ilovecoding822:需要注入js配置
    Vine_Finer:@普罗旺斯_minmin 默认自适应大小?
    o翻滚的牛宝宝o:@普罗旺斯_minmin 这个没试过。。可以找下资料。
  • 谈Xx:主要是不能走urlcache资源缓存的替换,混合开发的hybridAPP就很尴尬了
  • Yancy_90:已成功替换 多谢分享 :blush:
  • 戎耀:如果要保存网页里的cookie的话该怎么做呢?
    天下无贼:我也想问这个问题,大佬找到保存cookie的方法吗:heart: :heart:
    无星灬:我也想问这个问题,大佬找到保存cookie的方法吗
    o翻滚的牛宝宝o:@戎耀gej 这个没有测试过,估计cookie不会清除的吧。你可以读取cookie看看,不行再找找资料。
  • 鐵甲陳小寶:不用再重新添加一个delegate 可以再viewwilldisappear里面remove
    aae7e00037b6:@o翻滚的牛宝宝o 类似的问题,我在viewDidLoad记录了navigation.controllers个数,在viewWillDisappear里判断下,不是push就处理了
    鐵甲陳小寶:@鐵甲陳小寶 这个方法很蠢 还是加个delegate最好
    o翻滚的牛宝宝o:如果是push一个新界面呢?如果界面有跳转呢?你能保证add和remove配对么?
  • 我系哆啦:666确实有必要替换,我们项目也是从这个版本开始放弃支持iOS7了

本文标题:IOS进阶之WKWebView

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