IOS moved to some pits crossed by wkwebview

Time:2021-9-21

preface

In IOS, there are two web page views that can load web pages, except the controller of the system. One is uiwebview and the other is wkwebview. In fact, wkwebview wants to replace uiwebview, because we all know that uiwebview takes up a lot of memory and other problems, but now many people still use uiwebview. Why? Moreover, the official also announced that uiwebview was abandoned in IOS 12. Let’s use wkwebview as soon as possible. In fact, these things are: * * page size, JS interaction, request interception, and the problem of not bringing cookies** So sometimes you have to solve these problems if you want to migrate, so it’s still annoying, so solve them one by one.

Page size problem

We know that some web pages are well displayed on uiwebview, and there will be size problems when using wkwebview. At this time, I wonder if Android won’t. don’t you say it’s the front-end problem? But in fact, web pages in wkwebview need to be adapted, so add JS yourself. Of course, if you have a good relationship with the front end, you can ask him to add it. Next, add JS by setting the usercontentcontroller in the configuration.


WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
configuration.userContentController = wkUController;

JS interaction

We all know that in uiwebview, we can use our own JavaScript core for interaction, which is very convenient. There are three commonly used in JavaScript core, namely jscontext, jsvalue and jsexport.

Convenient interaction method in uiwebview

//Jscontext provides the running environment H5 context for it
- (void)webViewDidFinishLoad:(UIWebView *)webView{
 JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
 self.jsContext = jsContext;
}
//Execute script to add JS global variable
[self.jsContext evaluateScript:@"var arr = [3, '3', 'abc'];"];
//  ⚠️ When adding JS methods, it should be noted that the added methods will overwrite the original JS methods, because we obtain the context to operate after the web page is loaded successfully.
//Parameterless
self.jsContext[@"alertMessage"] = ^() {
 Nslog (@ "JS side will come here when calling alertmessage!");
};
//Values with parameters must be converted
 self.jsContext[@"showDict"] = ^(JSValue *value) {
 NSArray *args = [JSContext currentArguments];
 JSValue *dictValue = args[0];
 NSDictionary *dict = dictValue.toDictionary;
 NSLog(@"%@",dict);
 };
//Get the ARR data in JS
JSValue *arrValue = self.jsContext[@"arr"];
//Exception capture
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
 weakSelf.jsContext.exception = exception;
 NSLog(@"exception == %@",exception);
};
//Reassign objects in JS
OMJSObject *omObject = [[OMJSObject alloc] init];
self.jsContext[@"omObject"] = omObject;
NSLog(@"omObject == %d",[omObject getSum:20 num2:40]);

//We all know that when the object must comply with the jsexport protocol, JS can directly call the methods in the object and need to alias the function name. On the JS side, you can call gets, and OC can continue to use the getsum method
@protocol OMProtocol <JSExport>
//Protocol protocol method 
JSExportAs(getS, -(int)getSum:(int)num1 num2:(int)num2);
@end

How to do it in wkwebview?

Not like the above, the system provides the following two methods, so it is difficult. In addition, the front end has to use messagehandler to call, that is, Android and IOS are processed separately.

//Call JS directly
NSString *jsStr = @"var arr = [3, '3', 'abc']; ";
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
 NSLog(@"%@----%@",result, error);
}];
//After registering the name, JS uses messagehandlers to call the specified name to enter the proxy

//OC after we add the JS name
- (void)viewDidLoad{
 //...
 [wkUController addScriptMessageHandler:self name:@"showtime"];
 configuration.userContentController = wkUController;
}

//When messagehandlers in JS is called with the same name in OC, it will enter the following proxy to OC
window.webkit.messageHandlers.showtime.postMessage('');

//Agent, judgment logic
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
 if ([message.name isEqualToString:@"showtime"]) {
  Nslog (@ "coming!");
 }
 NSLog(@"message == %@ --- %@",message.name,message.body); 
}

//Finally, the dealloc must be removed
[self.userContentController removeScriptMessageHandlerForName:@"showtime"];
//If it is a pop-up window, you must implement the proxy method yourself
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
 Uialertcontroller * alert = [uialertcontroller alertcontrollerwithtitle: @ "reminder" message: message preferredstyle: uialertcontrollerstylealert];
 [alert Addaction: [uialertaction actionwithtitle: @ "got it" style: uialertactionstylecancel handler: ^ (uialertaction * _nonullaction){
  completionHandler();
 }]];
 
 [self presentViewController:alert animated:YES completion:nil];
}

Always use always cool interaction

We have written some interactions between the two. Although they can be used, they do not bring a very simple and easy realm, so there is an open source library:WebViewJavaScriptBridge。 This open source library can be compatible with both at the same time, and the interaction is very simple, but you have to work with the front end, otherwise it will be open-ended.

//Use
self.wjb = [WebViewJavascriptBridge bridgeForWebView:self.webView];
//If you want to implement the proxy method of uiwebview in VC, implement the following code (otherwise omitted)
[self.wjb setWebViewDelegate:self];

//Register JS method name
[self.wjb registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
 NSLog(@"currentThread == %@",[NSThread currentThread]);
 NSLog(@"data == %@ -- %@",data,responseCallback);
}];

//Call JS
dispatch_async(dispatch_get_global_queue(0, 0), ^{
  [self. WJB callhandler: @ "occalljsfunction" data: @ "OC calls JS" responsecallback: ^ (ID responsedata){
   NSLog(@"currentThread == %@",[NSThread currentThread]);
   Nslog (@ "callback after calling JS:% @", responsedata);
  }];
});

The application examples of the front end are as follows. You can view the specific application methodsWebViewJavaScriptBridge


function setupWebViewJavascriptBridge(callback) {
	if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
	if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
	window.WVJBCallbacks = [callback];
	var WVJBIframe = document.createElement('iframe');
	WVJBIframe.style.display = 'none';
	WVJBIframe.src = 'https://__bridge_loaded__';
	document.documentElement.appendChild(WVJBIframe);
	setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
	
	/* Initialize your app here */

	bridge.registerHandler('JS Echo', function(data, responseCallback) {
		console.log("JS Echo called with:", data)
		responseCallback(data)
	})
	bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
		console.log("JS received response:", responseData)
	})
})

Request interception

We used uiwebview in the early days- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationTypeTo intercept and do custom logic processing according to scheme, host and pathcomponents. However, this method is not very flexible, so we use nsurlprotocol to intercept. For example, wechat intercepts Taobao and directly displays a prompt. Or intercept the request, call the local interface, and turn on the camera, recording, album and other functions. It can also directly intercept and change the original request and directly return data or other URLs, which can be used when removing advertisements.

When we use it, we must use the subclass of nsurlprotocol to perform some operations. And you need to register a custom class before using it. Remember to mark after interception to prevent multiple execution of self loop. Unfortunately, in wkwebview, the post-processing operation of interception cannot be carried out. It can only be monitored, but it cannot be changed. It comes from that wkwebview adopts WebKit loading, which is the same mechanism as the browser of the system.

//Subclass
@interface OMURLProtocol : NSURLProtocol<NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@end

//Register
[NSURLProtocol registerClass:[OMURLProtocol class]];
//1. First, intercept here. If yes is returned, it means that it needs to be processed by our user, and if no, it goes through the system
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

//2. The interception process will go to the next step and return a standardized request, which can be redirected here
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

//3. This method will be used whether the interception is successful or not. You can do some custom processing here
- (void)startLoading;

//4. Any network request will go through the above interception processing. Even if we reschedule, we will go through one or more processes again, which need to be marked for processing
//Determine whether to intercept according to the tag value obtained by request, and process it in caninitwithrequest
+ (nullable id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
//Mark
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
//Remove tag
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;

Request header or data confusion

It should also be noted that if the line interception processing is implemented, when we use AFN and urlsession for access, we will find that the data or request header may not meet the expectations with the data or request after your interception processing. This is because we only request a before requesting B during interception, which does not meet the expectations, Although urlconnection will not be used, it has been abandoned and is not worth advocating. When we print the protocol configured in the session through lldb during interception, we find that it does not include our customized protocol. We exchange the protocolclasses method through the runtime exchange method, and we implement our own protocolclasses method. However, in order to ensure the original properties of the system, we should add our protocol class to the original protocol table of the system. At present, we can use [nsurlsession sharedsession]. Configuration. Protocolclasses; Get the default protocol class of the system, but if we write it in the current custom class, it will cause an endless loop because we exchange the getter method of this attribute. We save the class name and store it in nsuserdefaults. When we get the value, we restore the class.


po session.configuration.protocolClasses
<__NSArrayI 0x600001442d00>(
_NSURLHTTPProtocol,
_NSURLDataProtocol,
_NSURLFTPProtocol,
_NSURLFileProtocol,
NSAboutURLProtocol
)
//Custom return our protocol class
- (NSArray *)protocolClasses {
 NSArray *originalProtocols = [OMURLProtocol readOriginalProtocols];
 NSMutableArray *newProtocols = [NSMutableArray arrayWithArray:originalProtocols];
 [newProtocols addObject:[OMURLProtocol class]];
 return newProtocols;
}

//When we print again, we find that our custom protocol class has been added
po session.configuration.protocolClasses
<__NSArrayM 0x60000041a4f0>(
_NSURLHTTPProtocol,
_NSURLDataProtocol,
_NSURLFTPProtocol,
_NSURLFileProtocol,
NSAboutURLProtocol,
OMURLProtocol
)
//Original protocol class of storage system
+ (void)saveOriginalProtocols: (NSArray<Class> *)protocols{
 NSMutableArray *protocolNameArray = [NSMutableArray array];
 for (Class protocol in protocols){
  [protocolNameArray addObject:NSStringFromClass(protocol)];
 }
 Nslog (@ "protocol array is:% @", protocolnamearray);
 [[NSUserDefaults standardUserDefaults] setObject:protocolNameArray forKey:originalProtocolsKey];
 [[NSUserDefaults standardUserDefaults] synchronize];
}

//Get the original protocol class of the system
+ (NSArray<Class> *)readOriginalProtocols{
 NSArray *classNames = [[NSUserDefaults standardUserDefaults] valueForKey:originalProtocolsKey];
 NSMutableArray *origianlProtocols = [NSMutableArray array];
 for (NSString *name in classNames){
  Class class = NSClassFromString(name);
  [origianlProtocols addObject: class];
 }
 return origianlProtocols;
}
+ (void)hookNSURLSessionConfiguration{
 NSArray *originalProtocols = [NSURLSession sharedSession].configuration.protocolClasses;
 [self saveOriginalProtocols:originalProtocols];
 Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
 Method originalMethod = class_getInstanceMethod(cls, @selector(protocolClasses));
 Method stubMethod = class_getInstanceMethod([self class], @selector(protocolClasses));
 if (!originalMethod || !stubMethod) {
  [nsexception raise: nsinternalinconsistencyexception format: @ "cannot exchange without this method];
 }
 method_exchangeImplementations(originalMethod, stubMethod);
}

Cookie carrying problem

Many application scenarios need to use session for processing. It is easy to carry these cookies in uiwebview. However, due to the different mechanism of wkwebview, the loss of cookies across domains is very bad. There are currently two uses: scripting and adding cookies manually. The script is not reliable. It is recommended to add it manually.

//Use scripts to add cookies

//Get cookie data
- (NSString *)cookieString
{
 NSMutableString *script = [NSMutableString string];
 [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
 for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {

  if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
   continue;
  }
  [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.kc_formatCookieString];
 }
 return script;
}

//Add cookie
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[[[WKUserContentController alloc] init] addUserScript: cookieScript];
//Add a category to fix cookie loss
@interface NSURLRequest (Cookie)

- (NSURLRequest *)fixCookie;

@end

@implementation NSURLRequest (Cookie)

- (NSURLRequest *)fixCookie{
 NSMutableURLRequest *fixedRequest;
 if ([self isKindOfClass:[NSMutableURLRequest class]]) {
  fixedRequest = (NSMutableURLRequest *)self;
 } else {
  fixedRequest = self.mutableCopy;
 }
 //Prevent cookie loss
 NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
 if (dict.count) {
  NSMutableDictionary *mDict = self.allHTTPHeaderFields.mutableCopy;
  [mDict setValuesForKeysWithDictionary:dict];
  fixedRequest.allHTTPHeaderFields = mDict;
 }
 return fixedRequest;
}

@end
 
 
 //Usage scenario
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
 [navigationAction.request fixCookie];
 decisionHandler(WKNavigationActionPolicyAllow);
}

summary

The above is the whole content of this article. I hope the content of this article has certain reference and learning value for your study or work. Thank you for your support for developpaer.