WKWebView问题小结

内存泄漏

当需要拦截Web页面的Javascript函数时会使用以下方法

1
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

在Web页面需要调用以下方法

1
window.webkit.messageHandlers.name.postMessage();

这两个方法的name必须一样才能被WKWebView拦截到。
但是如果不及时移除Handler的话WebView不会被释放。所以只要调用了addScriptMessageHandler方法就要调用相对应的方法移除

1
- (void)removeScriptMessageHandlerForName:(NSString *)name;

内嵌在Cell里显示不全

在iOS10及以上系统里在TableViewCell中嵌入WKWebView时,滚动TableView时WebView渲染会出错。往往内容显示不出来。

具体解决办法在TableView的代理方法中调用WKWebView的setNeedsLayout方法:

1
2
3
4
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[webView setNeedsLayout];
}

WKWebView不能打开新窗口<a target=_blank />

如果Web页面的链接是<a target=_blank />时,正常结果是新建窗口打开这个页面。但在WKWebView中,如果没有实现相关的代理方法,那么这个方法失效,点击网页中任何外部链接都没有反应。期望的结果是通过Safari来打开这个外部链接。原来的WebView不进行任何处理。如果这样需要在WebView的代理方法中进行如下处理:

1
2
3
4
5
6
7
8
9
10
- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
if (!navigationAction.targetFrame) { //处理<a target=_blank />这种情况
NSURL *url = navigationAction.request.URL;
UIApplication *app = [UIApplication sharedApplication];
if ([app canOpenURL:url]) {
[app openURL:url];
}
}
}

在WKWebView中动态更改UserAgent

往往在访问我们自己的域名时会修改相关UserAgent上传。而访问其他域名时会将UserAgent修改成默认的。这样就需要选择性的修改UserAgent
首先在程序加载时我们先获取系统默认的UserAgent并将它保存在本地:

1
2
3
4
5
6
7
+(void)load
{
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:oldAgent, @"originAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}

然后在加载新页面时来判断是否为我们自己的域名,调用如下方法针对性的修改UserAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)setUserAgent:(NSString *)userAgent
{

if ([URL.host hasSuffix:baseDomain] {
//如果是我们自己的域名将传进来的userAgent设置到http的请求header中
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:userAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}
else{
//否则设置成系统默认的UserAgent
NSString *string = [[NSUserDefaults standardUserDefaults] objectForKey:@"originAgent"];
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:OutNull(string), @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}
}

本地Request的Cookies与WKWebView的Cookies共享

往往我们想将本地URL请求的Cookies同步到WKWebView中,在使用UIWebView时会自动同步我们请求URL的Cookies,但在WKWebView中会失效,因为WKWebView自己独立管理Cookies,不与NSURLRequest的公用。因此要通过一下方法进行设置WKWebview的Cookies。

首先在初始化是执行一下脚本来设置Cookies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

-(void)initWebView
{
WKWebViewConfiguration *webViewconfiguration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
if(URL.host hasSuffix:baseDomain){
//在此处要判断域名是否是自己网站。
NSString *jScript = [self setCookies];
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[wkUController addUserScript:wkUScript];
}
webViewconfiguration.userContentController = wkUController;
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, width, height) configuration:webViewconfiguration];

}


//执行的脚本,可能略有不同
+(NSString *)setCookies
{
NSString *script = [NSString string];

for (NSHTTPCookie *httpCookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies])
{
NSString *cookie = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",httpCookie.name,httpCookie.value,httpCookie.domain,httpCookie.path?:@"/"];
script = [script stringByAppendingString:[NSString stringWithFormat:@"document.cookie='%@';",cookie]];

}
return script;
}

然后在创建NSURLRequest对象时手动添加cookies到HTTP的Header中:

1
2
3
4
5
6
7
8
9
10
- (void)loadRequest:(NSURLRequest *)request
{
//此处也要判断要加载的url是否是我们自己的域名
if ([request.URL.host hasSuffix:baseDomain]){
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
NSString *cookies = @""; //设置Cookies
[mutableRequest addValue:cookies forHTTPHeaderField:@"Cookie"];
}
// do request
}

WKWebView在iOS10及以上系统设置新的Cookies不能覆盖旧的Cookies

在使用WKWebView时发现在iOS10以下的系统中如果按照上面的方法设置完Cookies后会生效,并且会自动覆盖旧的Cookies。而在iOS10及以上系统时设置新的Cookies并不会生效,WKWebView还会使用之前的设置过的Cookies。所以我们在切换用户时需要调用以下方法手动删除Cookies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)clearCookies
{
WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
[dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
for (WKWebsiteDataRecord *record in records){
if ([record.displayName isEqualToString:baseDomain]){
//判断如果是我们自己的域名那么删除该域名下的Cookies
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{

}];
}
}
}];
}

WKWebView在在iOS10及以上系统清除Cookies失效

使用上面的方法清除Cookies时会发现有时会清除失败。加载新的页面时还会使用旧的Cookies。这是因为清除Cookies的时机不对,我们应该在清除完成的回调内再加载新的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)loadRequest:(NSURLRequest *)request{
WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
[dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
BOOL isExit = NO;
for (WKWebsiteDataRecord *record in records){
if ([record.displayName isEqualToString:baseDomain]){
//判断如果是我们自己的域名那么删除该域名下的Cookies
isExit = YES;
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
[webView loadRequest:request];
}];
break;
}
}
if (!isExit) {
[webView loadRequest:[mutableRequest copy]];
}
}];
}

注意:如果在退出页面时清除Cookies,也会出现清除失败的情况。

Native页面内嵌WKWebView高度计算问题

计算WKWebView的高度是我们往往调用document.body.scrollHeight这个方法来计算。如果在下面的代理方法里调用那么计算的高度是不准确的:

1
2
3
- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

}

因为页面内部有很多图片还没有加载出来。所以此时调用document.body.scrollHeight这个方法计算的高度是未加载完的页面高度。
如果在加载结束的代理方法里面调用document.body.scrollHeight,那么页面只能在全部资源下载后才会显示。如果我们想要边加载资源边动态的改变页面高度需要做如下处理:

我们在初始化WebView时监听WebView的加载进度

1
2
3
4
5
-(void)initWebView
{
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}

然后边加载边计算WebView的高度,这样就不必等页面全部加载完再计算WebView的高度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"estimatedProgress"]) {
self.estimatedProgress = [change[NSKeyValueChangeNewKey] doubleValue];
if ([change[NSKeyValueChangeOldKey] doubleValue]) {
[self evaluateJavaScript:@"document.body.scrollHeight" completionHandler:^(id heitht, NSError *error) {
if (!error) {
self.pageHeight = [heitht floatValue];
}
}];
}
}

}