Introduction:

WKWebView is a new webView component launched by Apple in WWDC 2014 to replace UIKit’s bulky and difficult to use and memory leak UIWebView. WKWebView has a scrolling refresh rate of 60fps and the same JavaScript engine as safari.

Simple adaptation method is no longer described in this article, mainly to say that adaptation WKWebView process filled pit and treat the technical problems to solve.

1, WKWebView white screen problem

WKWebView boasts faster Loading speed and lower memory footprint, but actually WKWebView is a multi-process component, with Network Loading and UI Rendering performed in other processes. When we first adapted WKWebView, we were surprised that the memory consumption of the App Process decreased significantly after WKWebView was opened. However, after careful observation, we found that the memory consumption of Other Process increased. In some complex webGL rendered pages, the total Memory footprint (App Process Memory + Other Process Memory) of WKWebView is not much less than that of UIWebView.

In UIWebView, when the memory usage is too high, the App Process will crash; In WKWebView, when the overall memory consumption is relatively large, the WebContent Process will crash, resulting in a white screen phenomenon. Loading the following test link in WKWebView can stably reproduce the white screen:

People.mozilla.org/~rnewman/fe…

At this time, wkWebView. URL will become nil, and the simple reload refresh operation has failed, which has a great impact on some long-resident H5 pages.

Our final solution is:

A. WKNavigtionDelegate

Since iOS 9, WKNavigtionDelegate has added a callback function:

- (void) webViewWebContentProcessDidTerminate: (WKWebView *) webView API_AVAILABLE (macosx (10.11), the ios (9.0));Copy the code

When WKWebView’s total memory usage is too large and the page is about to go blank, the system will call the above callback function. We perform [webView reload] in this function (webView.URL value is not nil at this time) to solve the blank screen problem. The current page may be frequently refreshed on some pages with high memory consumption. Therefore, you need to perform corresponding adaptation operations on H5.

B. Check whether webview. title is empty

Not all H5 pages will call the above callback function when the H5 page is blank. For example, I recently encountered a system camera presented on a H5 page with high memory consumption, and a blank screen appeared when I returned to the original page after taking pictures (the process of taking pictures consumes a lot of memory, resulting in memory strain. The WebContent Process is suspended by the system), but the above callback function is not called. Another phenomenon is that webView.titile is empty when WKWebView is white, so you can check webView.title is empty when viewWillAppear to reload the page.

Combining the above two methods can solve the vast majority of white screen problems.

2. WKWebView Cookie problem

Cookies are one of WKWebView’s weaknesses

2.1 WKWebView Cookie storage

It is widely believed that WKWebView has its own private storage and does not store cookies in the standard Cookie container NSHTTPCookieStorage.

Practice found that WKWebView instances actually store cookies in NSHTTPCookieStorage, but there is a delay in the storage timing. On iOS 8, when the page jumps, The Cookie on the current page is written to NSHTTPCookieStorage, and on iOS 10, Cookies injected by JS document.cookie or server set-cookie are quickly synchronized to NSHTTPCookieStorage, FireFox engineers recommended that you reset WKProcessPool to trigger the Cookie synchronization to NSHTTPCookieStorage. However, the reset WKProcessPool does not work and may cause the loss of session cookies on the current page.

The problem with WKWebView cookies is that requests from WKWebView do not automatically carry cookies stored in the NSHTTPCookieStorage container.

For example, NSHTTPCookieStorage stores a Cookie:

name=Nicholas; value=test; domain=y.qq.com; Expires =Sat, 02 May 2019 23:38:25 GMT;Copy the code

Make a request to y.qq.com through UIWebView, then the request header will automatically include cookie: Nicholas=test; When sending a request to y.qq.com through WKWebView, the request header will not automatically include cookie: Nicholas=test.

2.2, WKProcessPool

WKProcessPool represents the world of the Web Content process. WKProcessPool represents the world of the Web Content process. By having all WKWebViews share the same WKProcessPool instance, you can achieve the sharing of Cookie (session Cookie and persistent Cookie) data among multiple WKWebViews. However, the WKWebView WKProcessPool instance will be reset after the app killing process restarts. As a result, the WKProcessPool Cookie and session Cookie data will be lost. WKProcessPool instances cannot currently be localized.

2.3, the Workaround

Since many H5 services rely on cookies for login state verification, and requests on WKWebView do not automatically carry cookies, the main solutions are as follows:

A, WKWebView loadRequest, set the Cookie in the request header, to solve the problem that the Cookie does not carry the first request;
WKWebView * webView = [WKWebView new]; 

NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];

[webView loadRequest:request];Copy the code
B. Set cookies through document.cookie to solve the cookie problem of subsequent pages (same-domain)Ajax and IFrame requests;

Note: Document.cookie () cannot set cookies across domains

WKUserContentController* userContentController = [WKUserContentController new]; 

WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];

[userContentController addUserScript:cookieScript];Copy the code

For example, if the first request is www.a.com, we solve the problem by putting a Cookie in the request header. Then page 302 goes to www.b.com. At this point the www.b.com request may not be accessible because it does not carry a cookie. Of course, since the callback function is called before every page jump:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;Copy the code

You can intercept 302 requests in this callback function, copy the request, add cookies to the request header, and reload the request. After all, -[WKWebView loadRequest:] is only suitable for loading mainFrame requests.

3, WKWebView NSURLProtocol problem

WKWebView performs network requests in a separate process from the app process. The request data does not pass through the main process, so using NSURLProtocol directly on WKWebView cannot intercept requests. Apple’s open source webKit2 source code exposes the proprietary API:

+ [WKBrowsingContextController registerSchemeForCustomProtocol:]Copy the code

By registering an HTTP (S) scheme WKWebView will be able to intercept HTTP (s) requests using NSURLProtocol:

The Class CLS = NSClassFromString (@ "WKBrowsingContextController"); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); If ([(id) CLS respondsToSelector:sel]) {// Register HTTP (s) scheme, NSURLProtocol [(id) CLS performSelector:sel withObject:@" HTTP "]; [(id)cls performSelector:sel withObject:@"https"]; }Copy the code

But there are two serious drawbacks to this approach:

A, The body data of the POST request is cleared

Because WKWebView performs network requests in a separate process. Once the HTTP (S) Scheme is registered, Network requests are sent from Network Process to App Process so that NSURLProtocol can intercept Network requests. In the design of Webkit2, MessageQueue is used to communicate between processes. The Network Process will encode the request into a Message, and then send it to the App Process through IPC. The HTTPBody and HTTPBodyStream fields are discarded for performance reasons

Refer to apple source code:

Github.com/WebKit/webk… (Copy the link to open it in your browser)

And the bug report:

Bugs.webkit.org/show_bug.cg… (Copy the link to open it in your browser)

As a result, If the registration by registerSchemeForCustomProtocol HTTP (s) scheme, then launched by WKWebView all HTTP request (s) will be passed to main process IPC NSURLProtocol processing, Causes the body of the POST request to be emptied.

B. Insufficient ATS support

Tests have found that once the ATS switch is turned on: Allow Arbitrary Loads option is set to NO, at the same time, through registerSchemeForCustomProtocol HTTP (s) registered scheme, All HTTP network requests from WKWebView will be blocked (even with the Allow Arbitrary In Web Content option set to YES);

WKWebView can register a customScheme, such as Dynamic ://, so requests that want to use offline functionality without using POST can be initiated through the customScheme. dynamic://www.dynamicalbumlocalimage.com/, for example, and then in the process of app NSURLProtocol intercepting the request and load data offline. Insufficient: The scheme is still not applicable to post requests, and the REQUEST scheme and CSP rules need to be modified on H5.

4. WKWebView loadRequest problem

The body of a POST request from loadRequest on WKWebView will be lost:

// Also due to interprocess communication performance issues, the HTTPBody field was discarded

[request setHTTPMethod:@"POST"]; [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]]; [wkwebview loadRequest: request];Copy the code

workaround:

If want to pass – [WKWebView loadRequest:] loaded post request request1: h5.qzone.qq.com/mqzone/inde… , you can perform the following steps:

  1. Replace the request scheme with a new POST request request2: post://h5.qzone.qq.com/mqzone/index and copies the body field of request1 to request2 header (its not discard the header fields);

  2. Load a new POST request2 with -[WKWebView loadRequest:];

  3. By + [WKBrowsingContextController registerSchemeForCustomProtocol:] registered scheme: post: / /;

  4. Registration request post://h5.qzone.qq.com/mqzone/index NSURLProtocol interception, replacement request scheme, generate a new request request3: h5.qzone.qq.com/mqzone/inde… Copy the body field of the request2 header into the body of the request3 and load the request3 using NSURLConnection. Finally, the load result is returned to WKWebView via NSURLProtocolClient.

5, WKWebView page style problems

In the process of WKWebView adaptation, we found that the position of some H5 page elements was downward offset or stretched and deformed. After tracing, we found that it was mainly caused by abnormal height value of H5 page:

A. space H5 navigation page has transparent, transparent navigation drop-down refresh, full screen, such as demand, therefore before webView whole began layout (0, 0), by adjusting the webView. ScrollView. ContentInset to fit special navigation requirements. And on WKWebView contentInset adjustment will feedback to webView. The scrollView. ContentSize. The change of the height, Such as setting up a webView. ScrollView. ContentInset. Top = a, then the contentSize. Height can increase the value of a, H5 page length increases, page element position downward migration;

Solution: adjust the WKWebView layout, avoid to adjust the webView. ScrollView. ContentInset. In fact, even on the UIWebView directly adjust the webView is not recommended. The scrollView. ContentInset value, it will bring some strange questions. If you have to change the contentInset in some special cases, you can return the H5 page to normal by using the following method:

/ * * after setting contentInset value by adjusting the webView frame to make the page back to normal display * reference: http://km.oa.com/articles/show/277372 * /

webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);Copy the code

B. When accessing the Now live broadcast, we found that the WKWebView page would be stretched and deformed on iOS 9. Finally, we found that the incorrect value of window.innerHeight (which returned a very large value on WKWebView) was the cause. H5 sets the page height by getting window.innerHeight, which results in the page being stretched. A review of the data revealed that the bug was only present on a few versions of iOS 9, and Apple has since fixed the bug. Our final solution is to delay calling window.innerheight

setTimeout(function(){height = window.innerHeight},0);Copy the code

or

Use shrink-to-fit meta-tag 
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">Copy the code

6. WKWebView screenshot problem

-[CALayer renderInContext:] -[CALayer renderInContext:] -[CALayer renderInContext:]

@implementation UIView (ImageSnapshot) 
- (UIImage*)imageSnapshot { 
    UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor); 
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; 
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext(); 
    return newImage; 
} 
@endCopy the code

However, this approach still does not solve the problem of webGL page screenshots. I have searched the Apple documentation and studied the private API of webKit2 source, but still have not found a suitable solution. Safari and Chrome, both of which were fully switched to WKWebView, had the same problem: screenshots of webGL pages were either blank or plain black. In desperation, we can only agree on a JS interface, and let game developers implement this interface. Specifically, the canvas getImageData() method is used to obtain image data and return base64 data. When the client needs screenshots, Call this JS interface to get the Base64 String and convert it to UIImage.

WKWebView crash problem

After WKWebView was increased in volume, some crashes were added to the extranet. Among them, the main stack of one kind of crash is as follows:

. 28 UIKit 0x0000000190513360 UIApplicationMain + 208

29 Qzone 0x0000000101380570 main (main.m:181) 30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36

Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not calledCopy the code

Alert () is called by JS. Crash is the WKWebView callback.

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

The completionHandler is not called. When adapting to WKWebView, we need to implement the callback function, window.alert(), to raise the alert box. Our initial implementation looks like this:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction: [UIAlertAction actionWithTitle: @ "confirmed" style: UIAlertActionStyleCancel handler: ^ (UIAlertAction *action) { completionHandler(); }]]; [self presentViewController:alertController animated:YES completion:^{}]; }Copy the code

If WKWebView exits and JS executes window.alert(), the alert box may not pop out, and the completionHandler is not executed, causing crash. The other case is that as soon as WKWebView is opened, JS executes window.alert(). At this time, because the animation of the UIViewController (push or present) in which WKWebView is located is not finished, the alert box may not pop out. The completionHandler is not executed at the end, causing the crash. Our final implementation looks something like this:

- (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: @ "confirmed" style: UIAlertActionStyleCancel handler: ^ (UIAlertAction *action) { completionHandler(); }]]; if (/*UIViewController of WKWebView is visible*/) [self presentViewController:alertController animated:YES completion:^{}]; else completionHandler(); }Copy the code

Ensure that completionHandler can be executed in both cases above, eliminating the crash of the alert box under WKWebView. The cause and solution of the crash of the Confirm box under WKWebView are similar to that of the alert box.

Another crash occurs before WKWebView exits:

 -[WKWebView evaluateJavaScript: completionHandler:]Copy the code

Execute JS code. The WKWebView exits and is released, causing the completionHandler to become a wild pointer while the javaScript Core is still executing the javaScript code. CompletionHandler () is called when the javaScript Core is finished executing, Lead to crash. This crash only happened on iOS 8 system. Refer to Apple Open Source. Apple has fixed this bug in iOS9 and later systems, mainly referring to the copy of completionHandler block (refer: Trac.webkit.org/changeset/1…). ; For iOS 8, you can prevent the completionHandler from being released prematurely by retaining WKWebView in the completionHandler. We finally hooked the system method with methodSwizzle:

+ (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); } }]; }Copy the code

8. Other questions

8.1. Automatic video playback

WKWebView need WKWebViewConfiguration. MediaPlaybackRequiresUserAction set whether to allow automatic playback, but must be set before the WKWebView is initialized, Setting invalid after WKWebView initialization.

8.2 goBack API problems

WKWebView -[WKWebView goBack] will not trigger window.onload() and JS will not be executed.

8.3. Page scrolling rate

WKWebView needs to adjust the scrolling rate via scrollView Delegate:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}Copy the code

9, endnotes

This article summarizes some of the potholes we’ve stepped on in WKWebView. Although WKWebView has more pits, it still has great advantages over UIWebView in memory consumption and stability. WKWebView is the future, although apple has been slow to develop it.


If you think our content is good, please scan the QR code to appreciate the author and share with your friends










This article is the exclusive content of Tencent Bugly. Please mark the author and source prominently at the beginning of the article “Tencent Bugly(http://bugly.qq.com)”.