Adapting to WKWebView

1. Analysis

In iPhone 6s and iOS 10.3.2, make 10 requests to http://www.qq.com and get the following data:

Run UIWebView memory consumption WKWebView memory (app) consumption UIWebView request time-consuming WKWebView request time-consuming
1 67.47 MB 0.81 MB 4.13 s 0.80 s
2 58.23 MB 0.86 MB 1.16 s 0.54 s
3 57.83 MB 0.50 MB 1.14 s 0.56 s
4 59.38 MB 0.88 MB 1.08 s 1.07 s
5 59.70 MB 0.75 MB 1.07 s 0.71 s
6 64.05 MB 0.83 MB 1.47 s 0.65 s
7 59.45 MB 0.81 MB 1.11 s 0.63 s
8 57.55 MB 0.45 MB 1.15 s 0.64 s
9 58.47 MB 0.77 MB 1.17s 0.75 s
10 58.89 MB 0.84 MB 1.11 s 0.70 s

UIWebView average memory consumption: 54.13 MB
WKWebView average (app) memory consumption: 0.75 MB
UIWebView average request time: 1.46 s
WKWebView average request time: 0.7 s

In summary, you can get: The request time of WKWebView is about 50% of that of UIWebView, and it is even better in terms of memory. But in fact WKWebView is a multi-process component, and Network request and UI Rendering are executed in other processes. If you observe carefully, you will find that: when loading, the memory consumption of the App process is very small or even drops significantly, but the memory usage of the Other Process will increase. Therefore: on UIWebView, when the memory usage is too large, App Process will crash; on WKWebView, when the overall memory usage is relatively large, WebContent Process will crash, resulting in a white screen phenomenon.

Tip: In some complex pages rendered with webGL, the overall memory usage (App Process Memory + Other Process Memory) of using WKWebView is not necessarily much less than that of UIWebView.

It is too risky to consider completely replacing WKWebView. The grayscale capability of WKWebView can be realized by the server delivering a URL list when the app is started. By encapsulating and inheriting SFWebView from UIView, the dual-core capable WebView of UIWebView and WKWebView is realized.

Features

Key WKWebView Features:

  • Significant improvements in performance, stability, and functionality;
  • Allow JavaScript Nitro library to be loaded and used (limited in UIWebView);
  • Supports more HTML5 features;
  • Up to 60fps scrolling refresh rate and built-in gestures;
  • Refactor UIWebView and UIWebViewDelegate into 14 categories and 3 protocols;

2. Some problems and solutions

2.1 White screen problem

On UIWebView, when the memory usage is too large, App Process will crash; on WKWebView, when the overall memory usage is relatively large, WebContent Process will crash, resulting in a white screen phenomenon.

Experiment link: http://people.mozilla.org/~rnewman/fennec/mem.html

At this time, WKWebView.URL will become nil, and the simple reload refresh operation has failed, which will have a greater impact on some permanent H5 pages.
Solution:

  • With WKNavigtionDelegate (only applicable to iOS9 and above)
1
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

When the overall memory usage of WKWebView is too large and the page is about to go blank, the system will call the above callback function. We execute [webView reload] in this function (the value of webView.URL is not nil at this time) to solve the white screen problem. In some pages with high memory consumption, the current page may be refreshed frequently, and corresponding adaptation operations must also be performed on the H5 side.

  • Detect whether webView.title is empty (the default webView.title needs to be set during initialization)

Not all H5 pages will call the above callback function when the screen is white. For example, when the system camera is present on an H5 page with high memory consumption, a white screen will appear when returning to the original page after taking the photo (the photo taking process consumes a lot of memory, resulting in memory shortage, and the WebContent Process is suspended by the system), but the above callback function is not called. When WKWebView has a white screen, another phenomenon is that webView.title will be empty. Therefore, you can check whether webView.title is empty during viewWillAppear to reload page.

A combination of the above two methods can solve most white screen problems.

2.2 Cookie problem

2.2.1 Cookie private storage issue

The industry generally believes that WKWebView has its own private storage and will not store cookies in the standard cookie container NSHTTPCookieStorage.
Practical discovery: On iOS 8, when the page jumps, the cookie of the current page will be written into NSHTTPCookieStorage, while on iOS 10, JS executes document.cookie or the server set-cookie The injected cookie will be synchronized to NSHTTPCookieStorage soon.

FireFox engineers once suggested using reset WKProcessPool to trigger cookie synchronization to NSHTTPCookieStorage. In practice, it was found that it does not work and may cause problems such as the current page session cookie being lost.

2.2.2 The request does not automatically bring the cookie in the container

WKWebView will not automatically bring cookies stored in the NSHTTPCookieStorage container.
For example, NSHTTPCookieStorage stores a cookie:

1
name=Nicholas;value=test;domain=www.smallfan.net;expires=Sat, 02 May 2019 23:38:25 GMT;

A UIWebView request to http://www.smallfan.net automatically includes the cookie header: Nicholas=test.
A WKWebView request to the same URL does not include it automatically.
Solution:

  • A. WKWebView loadRequest Before, set the cookie in the header of the request to solve the problem that the cookie cannot be carried in the first request.
1
2
3
4
WKWebView *webView = [WKWebView new];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.fxiaoke.com"]];
[request addValue:@"uid=1000" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
  • B. Solve the cookie problem of Ajax and iframe requests on subsequent pages (same domain) through ·document.cookie·setting 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
- (NSString *)shareHttpCookieFromStorage:(NSURL *)url {

NSMutableArray *array = [NSMutableArray array];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:url]) {

NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
value = [NSString stringWithFormat:@"document.cookie = '%@'", value];
[array addObject:value];
}

NSString *header = @"";
header = [array componentsJoinedByString:@";"];

return header;
}

- (void)addCookiesWithUrl:(NSURL *)url {
WKUserContentController *userContentController = [WKUserContentController new];
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:[self shareHttpCookieFromStorage] injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:NO];
[controller addUserScript:cookieScript];
[userContentController addUserScript:cookieScript];
_wkWebView = [[WKWebView alloc] initWithFrame:self.bounds configuration:configuration];
...
}

**Note: **Because NSHTTPCookieStorage is a single instance shared by the entire app and contains cookies of all domains. When WKWebView is initialized, it is necessary to obtain the URL and load the corresponding cookie in advance to prevent security vulnerabilities such as login imitation due to cookie leakage.

Plan B cannot solve the cookie problem of 302 requests (cross-domain). You can intercept the callback function that is called every time the page jumps:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler To copy the request object, bring the cookie in the request header and re-loadRequest.

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

NSMutableURLRequest *newReq = [navigationAction.request mutableCopy];
NSMutableArray *array = [NSMutableArray array];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:navagationAction.request.URL]) {
NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
[array addObject:value];
}

NSString *cookie = [array componentsJoinedByString:@";"];
[newReq setValue:cookie forHTTPHeaderField:@"Cookie"];
[webView loadRequest:newReq];
}

**Defect:** Still cannot solve the cookie problem of cross-domain request for iframe on the page. After all, -[WKWebView loadRequest:] is only suitable for loading mainFrame request.

2.2.3 WKProcessPool cannot be saved locally (offline cache)

Apple developer documentation defines WKProcessPool as: A WKProcessPool object represents a pool of Web Content process. By letting all WKWebViews share the same WKProcessPool instance, cookie data can be shared between multiple WKWebViews. However, the WKProcessPool instance will be reset after the app killing process is restarted, resulting in the loss of cookie and session cookie data in WKProcessPool. Currently, it is not possible to localize the WKProcessPool instance.
**Note:** Since the user may exit the interface and destroy the object during the request process of WKWebView, when the callback is requested, the receiving processing object does not exist, causing a Bad Access crash, so WKProcessPool can be set as a singleton
Attached usage method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static WKProcessPool *_sharedWKProcessPoolInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedWKProcessPoolInstance = [[WKProcessPool alloc] init];
});

self.processPool = _sharedWKProcessPoolInstance;

WKWebViewConfiguration *configuration1 = [[WKWebViewConfiguration alloc] init];
configuration1.processPool = self.processPool;
WKWebView *webView1 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration1];
...
WKWebViewConfiguration *configuration2 = [[WKWebViewConfiguration alloc] init];
configuration2.processPool = self.processPool;
WKWebView *webView2 = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration2];
...

2.3 NSURLProtocol issue

WKWebView performs network requests in a process independent of the app process, and the request data does not pass through the main process. Therefore, using NSURLProtocol directly on WKWebView cannot intercept the request. Apple’s open source Webkit2 source code exposes private API:

1
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

After registering the http(s) scheme, WKWebView will be able to use NSURLProtocol to intercept http(s) requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Available only for iOS8.4 and above
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.4) {
Class cls = NSClassFromString(@"WKBrowsingContextController”);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

if ([(id)cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

// Register http(s) scheme, and hand over the http and https requests to NSURLProtocol for processing
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];

#pragma clang diagnostic pop
}
}
}

This solution currently has the following serious flaws:
post request body data is cleared
Because WKWebView performs network requests in an independent process. Once the http(s) scheme is registered, the network request will be sent from the Network Process to the App Process, so that NSURLProtocol can intercept the network request. In the design of webkit2, MessageQueue is used for communication between processes. Network Process will encode the request into a Message and then send it to App Process through IPC. For performance reasons, the two fields HTTPBody and HTTPBodyStream are discarded during encoding.
Therefore, If passed registerSchemeForCustomProtocol Registered http(s) scheme, then all http(s) requests initiated by WKWebView will be passed to the main process through IPC NSURLProtocol processing, causing the post request body to be cleared.
Solution:

2.3.1 If ATS is not enabled

Can register customScheme, such as smallfan://, so if you want to use the offline function without using the post method, you can initiate a request through customScheme, such as smallfan://webCache/HelloWorld, and then in the App process NSURLProtocol intercept this request and load offline data. Disadvantages: This solution is still not applicable to requests using the post method, and the request scheme and CSP rules need to be modified on the HTML5 side.

2.3.2 If ATS is turned on

Because: once the ATS switch is turned on: Allow Arbitrary Loads option is set to NO, and the http(s) scheme is registered through registerSchemeForCustomProtocol, all non-https network requests initiated by WKWebView will be blocked (even if Allow Arbitrary Loads in Web Content option is set to YES).
Can be solved by hooking all post requests:

  • For Ajax post requests, the idea is to use the XMLHttpRequest send and open methods to assemble the http body content into the http header and request normally. The App process NSURLProtocol intercepts this request, takes out the BODY content in the header and places it in the body, sends the request, and returns the result to WKWebView (you can use WebViewProxy completed).
    JS file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.tempSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

XMLHttpRequest.prototype.open = function(a,b) {
this._method = a;
s_ajaxListener.tempOpen.apply(this, arguments);
}

XMLHttpRequest.prototype.send = function(a,b) {
if (this._method && this._method.toLowerCase() == 'post') {
a = encodeURIComponent(a)
s_ajaxListener.tempSetRequestHeader.apply(this, ['BODY', a])
}
return s_ajaxListener.tempSend.apply(this, arguments);
}

App interception request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[WebViewProxy handleRequestsWithHttpHeader:@"BODY" handlerHash:[self hash] handler:^(NSURLRequest *req, WVPResponse *res) {

NSURLSession *session = [NSURLSession sharedSession];
NSMutableURLRequest *request = [req mutableCopy];
request.HTTPMethod = @"POST";
NSString *postData = request.allHTTPHeaderFields[@"BODY"];
NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
NSString *httpBody = decodePostData;
request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
[res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];

}];
[dataTask resume];
}];
  • For form requests, the solution is similar to Ajax, except that the BODY is transcoded and spliced into the URL, and the app process is processed in the same way.

JS file:

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
31
32
33
34
35
36
var s_formListener = window.onsubmit;
window.onsubmit = function (e) {
var node = e.srcElement;
if (node && node.tagName && node.tagName.toLowerCase() === 'form') {
if (node.method && node.method.toLowerCase() === 'post') {
var elements = [].slice.call(node.elements);
var tempData = [], entryName, entryValue;
for (var i = 0, l = elements.length; i < l; ++i) {
entryName = elements[i].name;
entryValue = elements[i].value;
if (entryValue.toString() === '[object File]') {
entryValue = entryValue.name;
}
tempData.push(encodeURIComponent(entryName) + '=' + encodeURIComponent(entryValue));
}
tempData = tempData.join('&');

var action = node.action || location.href;
var hashIndex = action.indexOf('#');
if (hashIndex >= 0) {
action = action.substring(0, hashIndex);
}
var queryIndex = action.indexOf('?');
if (queryIndex >= 0) {
action = action + '&POST_DATA=' + encodeURIComponent(tempData);
} else {
action = action + '?POST_DATA=' + encodeURIComponent(tempData);
}

node.action = action;
}
}
if (s_formListener && s_formListener.apply) {
s_formListener.apply(this, arguments);
}
}

App interception request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[WebViewProxy handleRequestsWithHttpHeader:@"POST_DATA" handler:^(NSURLRequest *req, WVPResponse *res) {

NSURLSession *session = [NSURLSession sharedSession];
NSMutableURLRequest *request = [req mutableCopy];
request.HTTPMethod = @"POST";
NSString *postData = request.allHTTPHeaderFields[@"POST_DATA"];
NSString *decodePostData = (__bridge_transfer NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)postData, CFSTR(""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
NSString *httpBody = decodePostData;
request.HTTPBody = [httpBody dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
[res respondWithNSData:data mimeType:response.MIMEType header:headers statusCode:((NSHTTPURLResponse *)response).statusCode];

}];
[dataTask resume];
}];

Defect: Http header value and url characters have length restrictions. On WWDC 2017, it was mentioned that iOS 11 will open a WKURLSchemeHandler registration to provide custom response capabilities, wait and see.

2.4 JavaScript interaction

2.4.1 WKWebView calls JavaScript

1
2
3
4
[_wkWebView evaluateJavaScript:@"Hello" completionHandler:^(NSString result, NSError * _Nullable error) {
if([result isEqualToString:@"Hi"]) {
}
}];

2.4.2 JavaScript calls WKWebView

1
2
3
4
5
6
7
8
 WKWebViewConfiguration * Configuration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *userContentController = [[WKUserContentController alloc] init];

//Register a js method with name as HelloNative
[userContentController addScriptMessageHandler:self name:@"HelloNative"];

Configuration.userContentController = userContentController;
_wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 300,500) configuration:Configuration];
1
2
3
4
5
6
7
8
#pragma mark WKScriptMessageHandler
//Set WKWebView’s WKScriptMessageHandler proxy method
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
if ([message.name isEqualToString:@"HelloNative"]) {
// Prints the passed parameters, only supports NSNumber, NSString, NSDate, NSArray, NSDictionary, NSNull types
NSLog(@"%@", message.body);
}
}

JS call:

1
window.webkit.messageHandlers.HelloNative.postMessage(message);

Attention: When closing the web page, you need to call removeScriptMessageHandlerForName to prevent memory leaks.

1
2
3
4
5
- (void)dealloc {
_wkWebView.UIDelegate = nil;
_wkWebView.navigationDelegate = nil;
[[_wkWebView configuration].userContentController removeScriptMessageHandlerForName:@"HelloNative"];
}

2.5 Crash problem

2.5.1 Crash caused by JS calling the window.alert() function

When JS calls the alert function, WKWebView uses the following method to call back:

1
+ (void)presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

The main reason is that the above completionHandler is not called. When adapting WKWebView, we need to implement the callback function ourselves, and window.alert() can call up the alert box.
Solution:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
if (/*UIViewController of WKWebView has finish push or present animation*/) {
completionHandler();
return;
}
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Confirm" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]];
if (/*UIViewController of WKWebView is visible*/)
[self presentViewController:alertController animated:YES completion:^{}];
else
completionHandler();
}

2.5.2 Crash caused by calling evaluateJavaScript:completionHandler: before WKWebView exits

The main reason is that after WKWebView exits and is released, completionHandler becomes a wild pointer. At this time, JavaScriptCore is still executing JS code. After JavaScriptCore completes execution, it will call completionHandler(), causing a crash.
This crash only occurs on iOS 8 systems. iOS 9 and above mainly copy the completionHandler block.

Solution:
Retain WKWebView inside completionHandler to prevent the handler from being released too early.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (void) load {
[self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil];
}
/*
* fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation
*/
- (void)altEvaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler {
id strongSelf = self;
[self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e) {
[strongSelf title];
if (completionHandler) {
completionHandler(r, e);
}
}];
}

2.6 Progress bar problem

In UIWebView, the progress bar has always been a defective problem. Although there is a type of open source component NJKWebViewProgress, there are still some deficiencies in accurately processing the loading completion (some pages can see webViewDidStartLoad: and webViewDidFinishLoad: unpaired callbacks, resulting in incorrect calculation of the progress bar loading result). A estimatedProgress attribute has been added to WKWebView, and precise progress bar control can be achieved through KVO. iOS WKWebView adds a progress bar similar to WeChat
Through observation of WeChat, the precise result of the progress bar is not the primary priority, but good user mentality expectations are the focus. Therefore, you can consider implementing a "virtual" progress bar in the following way.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
- (void)startProgress {
if (_hideProgress) {
return;
}

if (_progress == 0) {
_progress = 0.9;

[_progressLayer removeAllAnimations];//Clear all animations

CGRect frame = _progressView.frame;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//Set keyframe array
animation.values=@[[NSValue valueWithCGPoint:CGPointMake(0, 0)],
[NSValue valueWithCGPoint:CGPointMake(0.7 * frame.size.width, .0)],
[NSValue valueWithCGPoint:CGPointMake(0.9 * frame.size.width, .0)],
[NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
// Set the time point corresponding to each key frame, the value is 0~1
animation.keyTimes = @[[NSNumber numberWithFloat:.0],
[NSNumber numberWithFloat:.3],
[NSNumber numberWithFloat:.7],
[NSNumber numberWithFloat:1.]];
animation.removedOnCompletion = YES;
animation.fillMode = kCAFillModeForwards;
animation.duration = 20;
animation.delegate = self;
[_progressLayer addAnimation:animation forKey:@"startProgress"];
}
}

- (void)completeProgress {
if (_hideProgress) {
return;
}

CGPoint point = _progressLayer.presentationLayer.position;//The position of the current animation
if (round(point.x) == 0 && !_progress) {
return;
}
[_progressLayer removeAnimationForKey:@"startProgress"];

_progress = 1.0;

CGRect frame = _progressView.frame;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//Set keyframe array
animation.values=@[[NSValue valueWithCGPoint:point],
[NSValue valueWithCGPoint:CGPointMake(1.0 * frame.size.width, .0)]];
//Set the time point corresponding to each key frame
animation.keyTimes = @[[NSNumber numberWithFloat:.0],
[NSNumber numberWithFloat:1.]];
animation.duration = .27;
animation.removedOnCompletion = YES;
animation.fillMode = kCAFillModeForwards;
[_progressLayer addAnimation:animation forKey:@"completeProgress"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
// When WKWebViewcalls back webView:didFinishNavigation:, the page rendering is not actually completed.
// Monitors loadingattribute changes and can accurately determine whether the request is completed+rendering is completed
if ([keyPath isEqualToString:@"loading"]) {

BOOL oldLoading = [[change objectForKey:NSKeyValueChangeOldKey] boolValue];
BOOL newLoading = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];

if (newLoading) {
dispatch_async(dispatch_get_main_queue(), ^{
[self startProgress];
});
} else if (!newLoading) {
dispatch_async(dispatch_get_main_queue(), ^{
[self completeProgress];
_progress = 0;
});
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

- (void)dealloc {
_wkWebView.UIDelegate = nil;
_wkWebView.navigationDelegate = nil;

//Remember to release the listener when destroyed
[_wkWebView removeObserver:self forKeyPath:@"loading"];
}

2.7 Screenshot problem

1
2
3
4
5
6
7
- (UIImage*)imageSnapshot {
UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor);
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}

Using the above method to take screenshots of webGL pages results in either blank or pure black images.

Solution: Agree on a JS interface and let H5 implement this interface. Specifically, the canvas getImageData() method obtains the image data and returns the data in base64 format. When the client needs to take a screenshot, it calls this JS interface to obtain base64 String and converted to UIImage.

2.8 Other questions

2.8.1 Audio cannot be stopped under iOS8.2

Various solutions:

1
2
3
4
5
6
7
//Type 1: Request an empty page when returning
[NSURL URLWithString:@"about:blank"]

//Second: Load an empty HTML String when returning
[_wkWebView loadHTMLString:@"<html/>" baseURL:nil]

//The third method: Play the silent audio and then pause it in the viewDidDisappear method

2.8.2 Video does not play automatically

Solution:
WKWebView needs to set whether to allow automatic play through WKWebViewConfiguration.mediaPlaybackRequiresUserAction, but it must be set before WKWebView is initialized. The setting is invalid after WKWebView is initialized.

2.8.3 Page rollback problem

  • Business requirements, when there is only one history in the end, directly pop goes back, it needs to be rewritten as follows.
1
2
3
4
5
6
- (BOOL)canGoBack {
if (self.backForwardList.backList.count <= 1) {
return NO;
}
return YES;
}
  • Called on WKWebView-[WKWebView goBack], it will not be triggered after returning to the previous page. window.onload() Functions will not execute JS.

2.8.4 When the page is returned, the font size becomes larger

Solution:
Execute the following JavaScript in page webView:didFinishNavigation: to restore the font in webkit to 100%

1
[self evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '100%'" completionHandler:nil];

Reference:
1. Tencent Bugly Team [WKWebView Those Pitfalls]