preface


Currently, the iOS system has been updated to iOS11, and most projects are compatible with iOS8 at most. Therefore, when reconstructing and packaging WebView components in projects, WE intend to directly abandon UIWebView and switch to WKWebView.

If you are currently reading some articles about WKWebView on the Internet, you are already familiar with the advantages of WKWebView, and you have witnessed the problems you have encountered in the process of using WKWebView. This article will provide detailed solutions to the problems you have encountered so far. At the end of the article, it will also tell about the WKWebView performance optimization scheme.

Problem solved


  • gobackThe return page is not refreshed
  • Cookie
  • POSTRequest for invalidation
  • crash
  • navigationBackItem
  • The progress bar
  • NativewithJSThe interaction of the
  • To optimize theH5Page startup speed

Into the pit


The goback Api returns no refresh

When WE used UIWebView before, when we called goBack, the page would refresh. After WKWebView is used and goback is called, H5 will not refresh even if the reload method is called.

The reason is that when YOU call goback, UIWebView will trigger an onload event, WKWebView will not trigger an onload event, and if the front end is still handling the iOS page return event in the onload event, it can’t handle it, The solution is to have the front end listen for the PAGE goback event of WKWebView using the onPagesHow event.

The front-end code is as follows:

window.addEventListener("pageshow", function(event){ if(event.persisted){ location.reload(); }});Copy the code

To see if the page is being loaded directly from the server or read from the cache, the persisted property of the PageTransitionEvent object is used.

Return true if the page reads the property from the browser’s cache, false otherwise. The data is then updated by performing the corresponding page refresh action or by directly requesting the Ajax interface based on true or false.

Here’s how onLoad and onPagesHow events differ in Safari and Chrome:

. The event Chrome Safari
The page is loaded for the first time onload The trigger The trigger
The page is loaded for the first time onpageshow The trigger The trigger
Return from another page onload The trigger Don’t trigger
Return from another page onpageshow The trigger The trigger

About the cookie

WKWebView belongs to the WebKit framework, which extracts the browser kernel rendering process from the main App process and manages it by another process, reducing a considerable part of performance loss, which is also one of the reasons why it is superior to UIWebView in performance.

Since the WKWebView worker Process is separate from the App Process, let’s call it the WK Process(arbitrary).

When making a network request using AFN, if the server uses set-cookie to write the cookie into the header, AFN will save the cookie to NSHTTPCookieStorage after receiving the response. Next time, if the request URL is in the same domain, The AFN will take the cookie out of the NSHTTPCookieStorage and send it to the server as a cookie for the Request header, and this happens in the App Process.

Will WKWebView in WK Process also use NSHTTPCookieStorage for cookie processing after sending network request and receiving response? After testing, the answer is yes, but there are some problems that need to be paid attention to in the Process of accessing.

Say first deposit:

Test: iPhone 6P iOS:10 Test process: The client sends a network request using AFN. 2. After receiving the request, the server writes the cookie using set-cookie. 3.

NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:fields forURL:url];
for (NSHTTPCookie *cookie in cookies) {
    NSLog(@"cookie,name:= %@,valuie = %@",cookie.name,cookie.value);
}
Copy the code

4. Enter the WKWebView pages, using loadRequest sends a network with domain request casually, in decidePolicyForNavigationResponse agent approach, using the following code output log:

NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL]; For (NSHTTPCookie *cookie in cookies) {NSLog(@" cookie in wkwebView :%@", cookie); }Copy the code

You can also print the set-cookie for the server response header of the request with the following code:

NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
Copy the code

So, when is the time for WKWebView to store cookies into NSHTTPCookieStorage? Cookies injected by document.cookie or server set-cookie are quickly synchronized to NSHTTPCookieStorage. 2. Cookies will be synchronized to NSHTTPCookieStorage when the H5 page jumps. 3. When the controller page jumps, the Cookie is synchronized to NSHTTPCookieStorage.

Besides take:

WKWebView does not actively store cookies in NSHTTPCookieStorage when sending a loadRequest to the network, even if the request is in the same domain.

So, if you have a request that requires a cookie, instead of loading the URL directly, you need to create a URLMutableRequest object based on the URL, Will need additional cookies use addValue: forHTTPHeaderField: method to manually add a cookie to the request header, but it can only solve the problem of request for the first time without cookies, if the page to send an ajax request, cookies are not same, The solution is to set cookies via document.cookie, which means you should inject the relevant script when you instantiate WKWebView.

We said above are all in the same domain, in the event of 302 requests (domain name change is understandable, that is to say, different domain), the above solution with an item, then you need to intercept the URL in your WKWebView decidePolicyForNavigationAction agent method, Determine whether the current URL and the initial request URL are in the same domain. If they are different, obtain the request object of the current request in the proxy method and copy a new object. Manually add the cookie to the header through the addValue:forHeaderField: method. Then let WKWebView reload the copy of the new Request object using loadRequest.

Is that the end of the problem? NO, the above solution is also limited in that it can only solve the problem of subsequent same-domain Ajax requests without cookies. If an IFrame cross-domain request occurs, we can’t intercept the request, so we can’t manually add a cookie to the header of the request. WKWebView is only suitable for loading mainFrame requests.

So, be sure to warn the front end, avoid using IFrame, use Ajax where you can, on the other hand, iframe is not recommended anymore, except for specific problems.

A POST request

POST requests cannot be sent using WKWebView.

Therefore, at this time we need to use the custom NSURLProtocol interception WKWebView network request, and the advantages of using NSURLProtocol interception WKWebView network request are: 1. If the product requirements require the client to collect logs, including all network requests, you can obtain them in this way. 2. If the company has high requirements for user experience, WKWebView initialization and concurrent execution of relevant network requests can be realized here, so as to shorten the speed of opening H5 in client, or even in seconds, so as to achieve the same experience as native.

The problem is that normally NSURLProtocol cannot intercept network requests from WKWebView.

NSURLProtocol is also used to send a network request through WKWebView, but Apple filters out HTTP and HTTPS schemes. We cannot intercept the network request sent by WKWebView.

Therefore, in our custom NSURLProtocol, through the use of private API to register the some scheme, registration scheme and the name of the class of WKBrowsingContextController, There is a property called browsingContextController WKWebView, are the objects of a class. Registration method called registerSchemeForCustomProtocol:, know this private API, we can through the way of target – the action, registered WKWebView launched a network request need to intercept the URL scheme, The registered scheme must contain at least three types: HTTP, HTTPS, and POST.

The problem is not play, solve a problem at the same time often accompanied by another problem.

The problem with using this scheme to intercept WKWebView’s web request is that the body data of the POST request is cleared.

void ArgumentCoder<ResourceRequest>::encodePlatformData(Encoder& encoder, const ResourceRequest& resourceRequest) { RetainPtr<CFURLRequestRef> requestToSerialize = resourceRequest.cfURLRequest(DoNotUpdateHTTPBody); bool requestIsPresent = requestToSerialize; encoder << requestIsPresent; if (! requestIsPresent) return; // We don't send HTTP body over IPC for better performance. // Also, it's not always possible to do, as streams can only be created in process that does networking. RetainPtr<CFDataRef> requestHTTPBody = adoptCF(CFURLRequestCopyHTTPRequestBody(requestToSerialize.get())); RetainPtr<CFReadStreamRef> requestHTTPBodyStream = adoptCF(CFURLRequestCopyHTTPRequestBodyStream(requestToSerialize.get())); if (requestHTTPBody || requestHTTPBodyStream) { CFMutableURLRequestRef mutableRequest = CFURLRequestCreateMutableCopy(0,  requestToSerialize.get()); requestToSerialize = adoptCF(mutableRequest); CFURLRequestSetHTTPRequestBody(mutableRequest, nil); CFURLRequestSetHTTPRequestBodyStream(mutableRequest, nil); } RetainPtr<CFDictionaryRef> dictionary = adoptCF(WKCFURLRequestCreateSerializableRepresentation(requestToSerialize.get(), IPC::tokenNullTypeRef())); IPC::encode(encoder, dictionary.get()); // The fallback array is part of CFURLRequest, but it is not encoded by WKCFURLRequestCreateSerializableRepresentation. encoder << resourceRequest.responseContentDispositionEncodingFallbackArray(); encoder.encodeEnum(resourceRequest.requester()); }Copy the code

Apple does not send the HTTP body in interprocess communication.

Because WKWebView belongs to webKit framework, WKWebView’s network request and content loading/rendering are all carried out in WK Process, but NSURLProtocol interception request is still in App Process, once the HTTP (S) scheme is registered, The network request will be sent from the independent Process to the App Process, so that the custom NSURLProtocol can intercept the network request. In order to improve the interprocess communication efficiency, For performance reasons, Apple will discard the body data of the request. Because there is no limit on the size of body data (binary type), if the size is too large, the data transmission efficiency will be seriously affected, thus affecting the operation of interception request and delaying subsequent network request. Therefore, Apple will discard the body of POST request in inter-process communication.

How to solve it? The ultimate idea is that while HTTP body is discarded during interprocess communication, headers are not.

Therefore, the steps to solve the problem are as follows:

  • WKWebViewinloadRequestPrior torequestObject does some processing, thisrequestLet’s call the object thetaold request.

    1. Write downold requesttheschemeandNSDataThe type ofhttp body.

    2. Obtain the currentold requesttheURLTo replaceURLtheschemeforpost(Which is why we used it earlierNSURLProtocolregisteredpost schemeAnd replace it with a good oneURLCreate a new oneNSMutableURLRequestObject, this object is callednew request.

    3. Tonew requesttheheaderAssign the value obtained in Step 1schemeandhttp bodyAdd to this manuallynew requesttheheaderStudent: If thispostRequest needs to be attachedcookieIf so, you should do the samecookiefromold requestTake it out and put it innew requesttheheaderIn the.

    4. LetWKWebViewLoad thenew request.
  • WKWebViewSend a newrequest(when therequest urltheschemeispost), we can customize inNSURLProtocolTo intercept the request, perform the following steps:

    1. ReplaceschemeFor the time of,schemeispostYou need topost schemereplaceold requesttheschemeThis field has been saved before.

    2. ReplaceschemeAnd then you get a new oneURLAccording to this new oneURLTo generate aNSURLMutableRequestObject to save previouslyhttp body,cookiePut it in this new onerequestThe object’sheaderIn the.

    Use 3.NSURLSessionAccording to the newrequestObject sends a network request and then passesNSURLProtocol ClientReturns the load result toWKWebView.

Note that a total of three Request objects are generated in these steps.

crash

The crash was caused by js calling alert(), that is, when WKWebView was destroyed, JS just executed alert(), the original alert popover might not pop out, and the completionHandler callback was not executed at the end. Lead to crash; The other case is that when WKWebView is opened, JS executes alert(). At this time, the alert box may not pop up because the push or present animation of UIViewController in which WKWebView is located is not finished yet. The completionHandler is not executed at the end, causing the crash.

Solution: Get the final UIViewController on the current window and determine whether THE UIViewController has not been destroyed, whether the UIViewController has been loaded, and whether the animation has finished.

2. Another crash occurs when WKWebView is called before exiting: JS code is executed. 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: https://trac.webkit.org/changeset/179160); For iOS 8, you can prevent the completionHandler from being released prematurely by retaining WKWebView in the completionHandler.

The solution is to hook the system method with Method Swizzling, which strongly references self in the callback to ensure that self is still around when the completionHandler executes.

navigationBackItem

There are two ways to implement the navigation bar’s back item.

  • Custom navigation bar

NavigationBarButtonItems count and function depending on whether the WebView can goback.

  • Use the default navigation back button of the system, similar to wechat

The challenge is to get the event when the system navigation back button is clicked, and then do something with it.

Click the back button, actually call the UINavigationController navigationBar: shouldPopItem method, we can use the method swizzling hook to live this way, This method tells the UIViewController that WKWebView is in to handle it by calling a proxy method.

UIProgressView

This is easy, also don’t say more.

Native interaction with JS

  • Intercept the URL

In WKWebView decidePolicyForNavigationAction agent approach can be used to intercept the URL and general way of using interceptor URL URL format is as follows:

scheme://host? paramKey=paramValueCopy the code

Generally, scheme corresponds to services and host is the corresponding service (method). Then there are the parameters.

When the interaction mode of URL interception is used, there is no problem for JS to call Native when the business logic is not complex, but when the business logic is complex, JS needs to get the callback data processed by Native, it will be very troublesome to process.

In addition, the interaction mode of URL interception is not conducive to the future business expansion of JS and Native.

  • useBridge

WKWebView provides very good support for THE interaction between JS and Native through Bridge. We can achieve the purpose of various interactions through ScriptMessageHandler. The specific code to add scripts using ScriptMessageHandler is not described here, you can study by yourself. Focus on the scripting code for Bridge.

There are many open source solutions for Bridge, but most of them follow a pattern. In the injected Bridge script code, define the name of the method to be called by JS. This method usually contains the following parameters: 1. The name of the native business module to be invoked (some have, some don’t, plus if modularity is recommended in the project). 2. Name of the native service to call (usually method name). 3. Parameters passed to native (that is, parameters required by the method). 4. Callback, the callback that the script needs to call after invoking native methods.

Describe the entire interaction process using Bridge in detail, from creating the Bridge script to executing the Bridge script callback: Bridge script is called script. 1. The script provides JavaScript language methods for JS, which are used to call native methods. The four parameters of the method are described above. 2. In this method, a unique identifier (identifier) is generated based on some of the preceding parameters. 3. In the script, bind a dictionary attribute to the global object (window). Key is identifier and value is callback. 4. Call the postMessage function of MessageHandler and send all the above parameters and identifiers to Native (no callback, which is mainly used in Step 3). 5. Front-end call the code in your script to call native methods. For the specific code, please refer to the official Apple documentation. 5. Native in custom userContentController MessageHandler objects: didReceiveScriptMessage: agent method receives the JS to get the parameters of the (param). Obtain module name, service name, parameter, identifier, etc. In addition, several blocks need to be created corresponding to JS callback. For example, JS callback has a success block, so there should be a success block in native. These blocks are assigned to the param, so now the param has the following values:

TargetName actionName Identifier (by which attribute we can find the JS callback) Success block Failure block Progress block The above parameters are basically enough; if you need to extend them, add them yourselfCopy the code

So what are the main operations in these blocks? The evaluateJavaScript operation of WKWebView is wrapped in a block that takes the result and identifier of the native processing task and converts the result into JSON data. Find the JS callback using the identifier, and pass the resulting JSON data back to the JS callback as a parameter. The code is as follows:

NSString *resultDataString = [self jsonStringWithData:resultDictionary];
    
NSString *callbackString = [NSString stringWithFormat:@"window.Callback('%@', '%@', '%@')", identifier, result, resultDataString];

[message.webView evaluateJavaScript:callbackString completionHandler:nil];
Copy the code

6. Use target-action mechanism, instantiate object according to targetName, call method according to actionName, and pass parameter (param), target object will complete the task processing, Call param’s success block, Failure block, and Progress block to return the result of task processing to JS.

  • Interactive summary

Whether URL interception or Bridge is used, the final mechanism for invoking native methods is target-action. One of the reasons for using target-action mechanism is to reduce the coupling degree between classes, reduce hard coding and facilitate future business expansion.

Of course, if you don’t like the target-Action solution, you can extend it yourself.

Intercept network requests from WKWebView

By looking at the source of WebKit, we can know that WKWebView supports the interception of network requests, but WebKit does not register the scheme that needs to be intercepted, so we can only register manually.

Manually register need to call WKWebView private API, private API is registerSchemeForCustomProtocol: registration scheme, the cancellation of private API is unregisterSchemeForCustomProtocol: Some of you might think that if you’re using a private API in your project and it gets rejected by Apple’s dad when it’s reviewed, but I didn’t, but if you’re getting rejected, you can split the private API into multiple strings and spliced them together.

Therefore, the steps of intercepting WKWebView network requests are: (1) Customize NSURLProtocol to handle intercepted network requests. (2) Use the NSURLProtocol provided by the system to register the customized NSURLProtocol in (1). (3) Register the scheme of the network request to be intercepted through the private API. (4) Unregister the scheme registered in (3) at an appropriate time.

H5 startup performance optimization

One of the most criticized aspects of H5 is that its user experience is not as good as Native’s. In fact, H5’s interactive effect (excluding complex dynamic effects) is very close to Native’s, so the remaining shortcomings are generally related to the rendering of WebView. When we write native interface, The UI elements created by us can be seen when the page is opened, but the remote H5 page elements cannot be seen, because the remote H5 page elements need to be fetched from the server, and then can be displayed after rendering. The process is roughly as follows:


H5 Startup process

Therefore, it takes much longer for an H5 page to be fully displayed to the user than a Native page.

Therefore, for mobile terminals, there are two main points to optimize the startup performance of H5: (1) optimize the startup speed of WebView (2) make HTML/CSS/JavaScript files download faster, that is, the offline package scheme.

(1) OptimizationWebViewStart speed of

The browser kernel will not be initialized when App is opened. When we create a WKWebView, the system will initialize the browser kernel. That is to say, when we first open H5 with WebView, the display time of H5 needs to be added to the browser kernel startup time. So the optimization point is to optimize the browser kernel startup time.

Many solutions are to initialize a singleton WebView and make that one WebView globally available, so that each H5 is opened with the same WebView object, which works somewhat like a PC browser. The downside of this is that if the WebView terminates abnormally for some reason, Reopening H5 with this WebView may cause some unexpected problems, so a different solution is recommended.

Another solution is to maintain a global WebView reuse pool, using the same principles as UITableViewCell, which I won’t go into here. If a WebView is always working, it is put into the reuse pool. If a WebView terminates abnormally for some reason, it is removed from the reuse pool.

Either way, a new problem arises. When we open a new H5 using a reusable WebView, the browser’s browsing history still contains traces of the last H5 opened, so we need to clear this trace and leave the page open with a blank page during reuse.

(2) Use offline package packagingH5Static resources of.

If we open H5 through a remote URL, we can understand that it is open online.

An H5 HTML/CSS/JavaScript file is packed into static resource files and stored in the server. These static resource files stored in the server can be regarded as offline packages. The mobile terminal can choose an appropriate time to download the offline package and decompress it locally. When we open an H5, we actually open the HTML file that has been downloaded locally, so we don’t need to pull resources online, thus saving time.

When the H5 page needs to be updated, you can do incremental updates to the offline package directly.

See bang’s article for more details.

JXBWebKit based on WKWebView encapsulation


Support natigationBackItem & navigationLeftItems 3. Support custom rightBarButtonItem 4. Support progress bar 5. Cookie solution is provided, the first time to add, the subsequent Ajax requests automatically add, 302 requests automatically add 6. Support interception WKWebView interception network request 7. Support POST request 8. Support subclass inheritance 9. Support URL interception and custom URL interception operations. 10. Provide interactive solutions between Native and H5, and support customized MessageHandler operations. 11. Provide H5 second on solution, server using Go implementation.

Github address: JXBWKWebView