I. Pain points and background
The earliest UIWebView, interaction is not as convenient as WKWebView, but also ok, mainly because too much memory, poor performance, and now the minimum support for iOS 8, so I decided to change to WKWebView, which opened the road to step on the pit.
In daily development, we often have the need to operate some local resource information in the H5 page loaded by WKWebView or UIWebView. With UIWebView you just pass the local path of the file to the front end and the front end accesses the local path of the file, but with WKWebView you can’t load the resource file.
Due to the mandatory requirements of the company’s UI, the font style of the H5 page inside the APP must be consistent with Native, and a customized font is also required. Currently, the processing method of H5 is to request on the H5 page. It can solve the UI problem, but it brings huge consumption of traffic, hundreds of G of traffic are consumed every day, so the operation and maintenance hope that we can help solve it.
Second, solutions
1. Use NSURLProtocol for interception
At present there are a lot of articles on the Internet, not here. Problems with using NSURLProtocol
- Audit risk
- The body of the POST request is lost when HTTP/HTTPS is intercepted
- If Ajax hook mode is used, the length of post header characters may be limited, and Put type requests may be abnormal
We had run several versions of this solution with no problem, until one day the H5 page internal POST requests didn’t go out and the solution was declared dead.
From this point of view, private apis weren’t perfect until iOS11 WKURLSchemeHandler arrived.
There is a third party classification called NSURLProtocol+WebKitSupport
- NSURLProtocol + WebKitSupport. H
#import <Foundation/Foundation.h>
@interface NSURLProtocol (WebKitSupport)
+ (void)wk_registerScheme:(NSString*)scheme;
+ (void)wk_unregisterScheme:(NSString*)scheme;
@end
Copy the code
- NSURLProtocol + WebKitSupport. M
#import "NSURLProtocol+WebKitSupport.h" #import <WebKit/WebKit.h> /** * The functions below use some undocumented APIs, which may lead to rejection by Apple. */ FOUNDATION_STATIC_INLINE Class ContextControllerClass() { static Class cls; if (! cls) { cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class]; } return cls; } FOUNDATION_STATIC_INLINE SEL RegisterSchemeSelector() { return NSSelectorFromString(@"registerSchemeForCustomProtocol:"); } FOUNDATION_STATIC_INLINE SEL UnregisterSchemeSelector() { return NSSelectorFromString(@"unregisterSchemeForCustomProtocol:"); } @implementation NSURLProtocol (WebKitSupport) + (void)wk_registerScheme:(NSString *)scheme { Class cls = ContextControllerClass(); SEL sel = RegisterSchemeSelector(); if ([(id)cls respondsToSelector:sel]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [(id)cls performSelector:sel withObject:scheme]; #pragma clang diagnostic pop } } + (void)wk_unregisterScheme:(NSString *)scheme { Class cls = ContextControllerClass(); SEL sel = UnregisterSchemeSelector(); if ([(id)cls respondsToSelector:sel]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [(id)cls performSelector:sel withObject:scheme]; #pragma clang diagnostic pop } } @endCopy the code
- Custom URLProtocol
# # import "YCFontReplaceURLProtocol. H" import "NSURLProtocol + WebKitSupport. H" / * * for the modified request to define the key key, To prevent repeated modification request * / static nsstrings * const kFilteredKey = @ "YCFontReplaceURLProtocolFilteredKey"; / / static NSString * const kFontPathExtension = @" TTF "; @implementation YCFontReplaceURLProtocol #pragma mark - public method + (void)registerSelf { [NSURLProtocol registerClass:[YCFontReplaceURLProtocol class]]; for (NSString *scheme in @[@"http", @"https"]) { [NSURLProtocol wk_registerScheme:scheme]; } } + (void)unregisterSelf { [NSURLProtocol unregisterClass:[YCFontReplaceURLProtocol class]]; for (NSString *scheme in @[@"http", @"https"]) { [NSURLProtocol wk_unregisterScheme:scheme]; }} #pragma mark-urlprotocol Must Implemention Method */ + (BOOL)canInitWithRequest:(NSURLRequest *)request { return [self isHandleWithRequest:request]; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } - (void)startLoading { NSMutableURLRequest* request = self.request.mutableCopy; [NSURLProtocol setProperty:@YES forKey:kFilteredKey inRequest:request]; NSString *fontPath = [[NSBundle mainBundle] pathForResource:request.URL.lastPathComponent ofType:nil]; NSData* data = nil; if (fontPath) { data = [NSData dataWithContentsOfFile:fontPath]; } else { NSAssert(! fontPath || ! Data, @" font path not found or font load failed "); } NSURLResponse* response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:@"application/x-font-ttf" expectedContentLength:data.length textEncodingName:nil]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; [self.client URLProtocol:self didLoadData:data]; [self.client URLProtocolDidFinishLoading:self]; } - (void)stopLoading { } #pragma mark - private method + (BOOL)isHandleWithRequest:(NSURLRequest *)request { // Whether the request has been replaced font BOOL noFilterKey = ([NSURLProtocol propertyForKey: kFilteredKey inRequest: request] = = nil); if (! noFilterKey) { return NO; } / / determine whether for font format nsstrings. * lastPathComponent = request URL. LastPathComponent; NSString *pathExtension = lastPathComponent.pathExtension; if (! [pathExtension isEqualToString:kFontPathExtension]) { return NO; } / / whether the font request nsstrings. * fontStringByDeletingPathExtension = lastPathComponent stringByDeletingPathExtension; BOOL isTTF = [@[BOLD_FONT, CUSTOM_FONT] indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { return [fontStringByDeletingPathExtension compare:obj options:NSCaseInsensitiveSearch] == NSOrderedSame; }] ! = NSNotFound; if (! isTTF) { return NO; } return YES; } @endCopy the code
And then in the controller that you want to use, just call
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[YCFontReplaceURLProtocol registerSelf];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[YCFontReplaceURLProtocol unregisterSelf];
}
Copy the code
2. WKWebView infuses font CSS
- The following code
NSMutableString *javascript = [NSMutableString string]; [javascript appendString:@"document.documentElement.style.webkitTouchCallout='none';"] ; / / ban long press [javascript appendString: @ "document. The documentElement. Style. WebkitUserSelect = 'none'"] ; // Disable NSString *boldFont = [self getBase64FromFile:@" onionmath_bold-bold "ofType:@" TTF "]; [javascript appendString:[NSString stringWithFormat:@"\ var boldcss = '@font-face { font-family: \"OnionMath\"; src: url(data:font/ttf;base64,%@) format(\"truetype\");}'; \ var head = document.getElementsByTagName('head')[0], \ style = document.createElement('style'); \ style.type = 'text/css'; \ style.innerHtml = boldcss; \ head.appendChild(style);", boldFont]]; WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:javascript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; self.vipWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, NAV_BAR_HEIGHT, SCREEN_SIZE_WIDTH, SCREEN_SIZE_HEIGHT - NAV_BAR_HEIGHT)]; Self. VipWebView. ScrollView. DecelerationRate = 0.998; [self.vipWebView .configuration.userContentController addUserScript:noneSelectScript]; self.vipWebView.yc_navigationDelegate = self; self.vipWebView.scrollView.showsHorizontalScrollIndicator = NO; [self.vipWebView sizeToFit]; [self.view addSubview:self.vipWebView]; - (NSString*)getBase64FromFile:(NSString*)fileName ofType:(NSString*)type { NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:type]; NSData *nsdata = [NSData dataWithContentsOfFile:filePath]; NSString *base64Encoded = [nsdata base64EncodedStringWithOptions:0]; return filePath; }Copy the code
Can solve font problems, but when font resources are large, there are performance problems, and in terms of scalability, scalability is not very high.
3. Use GCDWebServer
(1) GCDWebServer profile
GCDWebServer is a modern lightweight HTTP 1.1-based GCD Server, which is mainly used for embedding OS X & iOS apps. It begins with the following goals in mind:
- Elegant, easy-to-use architecture with only 4 core classes: Server, Connection, Request and Response (see “Understanding the Architecture of GCDWebServer” below)
- Well-designed apis and complete documentation make it easy to integrate and customize
- For better performance and concurrency through Grand Central Dispatch is built entirely on an event-driven design
- No reliance on third-party libraries
- It is available under the New BSD License
Additional built-in features:
- Allows the execution of an incoming HTTP request completely asynchronous handler
- Minimize memory usage for large HTTP request or response bodies of disk streams
- Web Forms submissions are coded with “Application/X-www-form-urlencoded” or “multipart/form-data” (including file upload)
- JSON parsing and serialization, mainly to Request and Response HTTP bodies
- Chunked transfer encoding for request and response HTTP bodies
- HTTP compression with gzip for request and response HTTP bodies
- HTTP range support for requests of local files
- Basic and Digest Access authentications for password protection
- Automatically handles transitions between foreground, background, and suspend modes in iOS Apps
- IPv4 and IPv6 are supported
- NAT Port Mapping (IPv4 only)
Extensions included:
- GCDWebUploader:
GCDWebServer
That implements an interface for uploading and downloading files using a Web browser - GCDWebDAVServer:
GCDWebServer
Class, which implements a level 1WebDAVServer (with OS X’s Finder Part level 2 support)
What is not supported (but not really needed from an embedded HTTP server) :
- Stay connected
- HTTPS
(2) Access mode
- The iOS code is as follows:
- (void)startLocalServer {
self.webServer = [[GCDWebServer alloc] init];
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"OnionMath_Bold-Bold" ofType:@"ttf"];
[self.webServer addGETHandlerForBasePath:@"/"
directoryPath:bundlePath
indexFilename:nil
cacheAge:0
allowRangeRequests:YES];
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithInteger:8848]
forKey:GCDWebServerOption_Port];
[options setObject:[NSNumber numberWithBool:YES]
forKey:GCDWebServerOption_BindToLocalhost];
[self.webServer startWithOptions:options error:nil];
}
Copy the code
For local services, stop the service when it is not needed by calling [self.webserver stop].
For H5 page, need to approach the address of the font, such as http://xxx.com/OnionMath_Bold-Bold.ttf to http://127.0.0.0:8848/OnionMath_Bold-Bold.ttf.
(3) Problems encountered
Through the above method, I found it was so perfect after the joint adjustment with H5. The font could load normally and the display style was customized font style. However, when the H5 page did not know HTTPS, it suddenly found that the font did not take effect. How can not understand, suddenly test a word, is HTTP to HTTPS problem. Because of the cross-domain problem, you can’t access the HTTP resource path in HTTPS.
- Solution A. Cross-domain (Insecure)
For GCDWebServer, his function is more powerful, if this set can be realized, it can realize H5 fast open, video cache, image cache and many other functions, unfortunately, GCDWebServer does not support HTTPS, so it is not the best solution.
4. Use WKURLSchemeHandler
WKWebView was created after iOS11 and can block H5 internal addresses by defining Scheme, but WKWebView does not allow Scheme to block “HTTPS”, “FTP”, “file” requests. You can use the new interface + [WKWebView handlesURLScheme:] to check whether Scheme is handled by WKWebView by default.
YCCustomURLSchemeHandler. H content
@interface YCCustomURLSchemeHandler : NSObject <WKURLSchemeHandler>
@end
Copy the code
YCCustomURLSchemeHandler. M content
# import "YCCustomURLSchemeHandler. H" / / bold static nsstrings * kBoldFontScheme = @ "getboldfontscheme"; // static NSString *kRegularFontScheme = @" getregularFontScheme "; @implementation YCCustomURLSchemeHandler - (void)webView:(WKWebView *)webView StartURLSchemeTask :(id<WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0)){NSString *urlString = urlSchemeTask.request.URL.relativeString; NSData *data = nil; if([urlString containsString:kBoldFontScheme]) { NSString *fontUrl = [[NSBundle mainBundle] pathForResource:@"OnionMath_Bold-Bold" ofType:@"ttf"]; data = [NSData dataWithContentsOfFile:fontUrl]; } else if([urlString containsString:kRegularFontScheme]) { NSString *fontUrl = [[NSBundle mainBundle] pathForResource:@"OnionMath_Regular-regular" ofType:@"ttf"]; data = [NSData dataWithContentsOfFile:fontUrl]; } NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL MIMEType:@"application/x-font-truetype" expectedContentLength:data.length textEncodingName:nil]; [urlSchemeTask didReceiveResponse:response]; [urlSchemeTask didReceiveData:data]; [urlSchemeTask didFinish]; } - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0)){ urlSchemeTask = nil; } @endCopy the code
For H5 pages, write the following style
<script> var fontStyle = document.createElement('style') var iOSFont = '@font-face { font-family: "OnionMath"; src: url("getRegularFontScheme://OnionMath_Regular-regular.ttf") format("truetype"); }' var cssText = iOSFont + ' @font-face { font-family: "OnionMath"; src: url("getBoldFontScheme://OnionMath_Bold-Bold.ttf") format("truetype"); font-weight: bold; }' fontStyle.type = "text/css" fontStyle.innerHTML = cssText document.head.appendChild(fontStyle) </script>Copy the code
Third, summary
For solution 1, if H5 does not receive POST requests, you can use this method. Solution 2, performance problems may exist. Solution 3, if H5 is Http. As simple as this, you can write a utility class that handles the start and end of local services globally, without the need for a separate page for configuration. Plan four, because it’s only available in iOS11 and later, so if you want to be compatible with earlier versions, you need to do something about it.