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.
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)
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 201923: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.
**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.
**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:
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:
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:
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.
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.
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.
- (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"]) {
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.
苹果开发者文档对WKProcessPool的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有WKWebView共享同一个WKProcessPool实例,可以实现多个WKWebView之间共享cookie数据。不过WKProcessPool实例在app杀进程重启后会被重置,导致WKProcessPool中的cookie、session cookie数据丢失,目前也无法实现WKProcessPool实例本地化保存。 **注意:**由于WKWebView在请求过程中用户可能退出界面销毁对象,当请求回调时由于接收处理对象不存在,造成Bad Access crash,所以可将WKProcessPool设为单例 附使用方式: