iOS app seconds on H5 combat summary

In the "iOS app seconds on H5 optimization explore" a paper introduces the optimal solution as well as some knowledge, we continue to introduce the use WKURLSchemeHandler intercept some of the details of the load off the package to optimize the speed of opening and precautions, before reading this article, please probably look content articles as well as the basic usage WKURLSchemeHandler.

Offline package optimization download

In the previous "iOS app seconds on H5 optimize exploration ", the offline package download process there are many unreasonable, such as downloading scattered resources, not only increases the complexity of the subsequent update logic, and will result in wasted system resources. You can put all resource files (js / css / html etc.) for this purpose integrated into a zip package, a one-time download to the local, use SSZipArchive extract to the specified location, you can update version. In addition, the opportunity to download the app and start switching front and back have to do a check for updates, the better.

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *downLoadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
  if (!location) {
      return ;
  }
  
  //下载成功,移除旧资源
  [fileManager removeFileAtPath:dirPath fileExtesion:nil];
  
  //脚本临时存放路径
  NSString *downloadTmpPath = [NSString stringWithFormat:@"%@pkgfile_%@.zip", NSTemporaryDirectory(), version];
  // 文件移动到指定目录中
  NSError *saveError;
  [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:downloadTmpPath] error:&saveError];
  //解压zip
  BOOL success = [SSZipArchive unzipFileAtPath:downloadTmpPath toDestination:dirPath];
  if (!success) {
      LogError(@"pkgfile: unzip file error");
      [fileManager removeItemAtPath:downloadTmpPath error:nil];
      [fileManager removeFileAtPath:dirPath fileExtesion:nil];
      return;
  }
  //更新版本号
  [[NSUserDefaults standardUserDefaults] setValue:version forKey:pkgfileVisionKey];
  [[NSUserDefaults standardUserDefaults] synchronize];
  //清除临时文件和目录
  [fileManager removeItemAtPath:downloadTmpPath error:nil];
}];
[downLoadTask resume];
[session finishTasksAndInvalidate];
复制代码

WKWebView multiplexing pool

In the debugging process, found for the first time the page loads longer than the subsequent opening times are much slower because webView is expected to start first initialized when you need more resources and services, so try to pre-initialized webView multiplexing scheme, the speed will be much faster.

WKWebView cell multiplexing principle: two pre-prepared NSMutableSet <WKWebView *>, being used a visiableWebViewSet, an idle standby reusableWebViewSet, the + (void) load initialize a WKWebView, reusableWebViewSet added and, when the page required H5, reusableWebViewSet removed from and placed in visiableWebViewSet, used up (the dealloc) reusableWebViewSet back in. If the exception is abandoned WKWebView recreate WKWebView, in order to avoid some of the baffling problems.

1, initialization

+ (void)load {
    __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        dispatch_async(dispatch_get_main_queue(), ^{
            WKWebView *webview = [[WKWebView alloc] init];
            [self->_reusableWebViewSet addObject:webview];
        });

        [[NSNotificationCenter defaultCenter] removeObserver:observer];
    }];
}
复制代码

2 to obtain multiplexing pool webview

- (WKWebView *)getReusedWebViewForHolder:(id)holder {
    if (!holder) {
#if DEBUG
        NSLog(@"WKWebViewPool must have a holder");
#endif
        return nil;
    }
    
    WKWebView *webView;
    
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    
    if (_reusableWebViewSet.count > 0) {
        webView = [_reusableWebViewSet anyObject];
        [_reusableWebViewSet removeObject:webView];
        [_visiableWebViewSet addObject:webView];
        
    } else {
        [_visiableWebViewSet removeAllObjects];
        webView = [[WKWebView alloc] init];
        [_visiableWebViewSet addObject:webView];
    }
    webView.holderObject = holder;
    
    dispatch_semaphore_signal(_lock);
    
    return webView;
}
复制代码

Wherein the holder is WKWebView runtime using added property, the incoming VC to use the current cell multiplex, subsequent recovery for reuse is determined whether the pool is being used.

3, run out of recycling

- (void)recycleReusedWebView:(WKWebView *)webView {
    if (!webView) {
        return;
    }
    
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    
    if ([_visiableWebViewSet containsObject:webView]) {
        //将webView重置为初始状态
        [webView webViewEndReuse];
        
        [_visiableWebViewSet removeObject:webView];
        [_reusableWebViewSet addObject:webView];
        
    } else {
        if (![_reusableWebViewSet containsObject:webView]) {
#if DEBUG
            NSLog(@"Don't use the webView");
#endif
        }
    }
    dispatch_semaphore_signal(_lock);
}

其中webViewEndReuse为WKWebView的扩展方法:
- (void)webViewEndReuse {
    self.holderObject = nil;
    
    if ([self isKindOfClass:[WKWebView class]]) {
        WKWebView *webView = (WKWebView *)self.webView;
        webView.delegate = nil;
        webView.scrollView.delegate = nil;
        [webView stopLoading];
        [webView setUIDelegate:nil];
        [webView loadHTMLString:@"" baseURL:nil];
    }
}
复制代码

Multiplexing pool principle is very simple, in addition to clearing after receiving warning web cache memory and other recycling and so on, not repeat them here.

WebViewController transformation

WebViewController projects usually deal with H5 page will be placed in a uniform, so to combine switch to optimize operations to separate multiplex pools and the use of ordinary webView avoid problems.

1, replace url scheme

  NSString *urlString = @"https://www.test.com/abc?id=123456";
  if ([YH_Global sharedInstance].isGrassLocalOpen && SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) {             
    urlString = [urlString stringByReplacingOccurrencesOfString:@"https" withString:@"customScheme"];
  }
  WebViewController *vc = [[WebViewController alloc] initWithUrl:urlString];
  [self.navigationController pushViewController:vc animated:YES];
复制代码

Replace here url scheme http (s) for a custom protocol, using interception effect. Of particular note here is that the front end H5 request js, css and other resources using an adaptive protocol, such as: src='//www.test.com/abc.js'so that use different native end request scheme, H5 scheme will use the corresponding load request. Another important point is that, ajax request the front, scheme using http (s), native not to intercept, H5 entirely to interact with the server, so you do not send a post request occurs, the case of the missing body.

2, initialize webView

- (instancetype)initWithUrl:(NSString *)url {
    if (self = [super init]) {
        if ([self checkMatchingWithUrl:url]) {//符合条件,使用复用池
            self.webView = [[WKWebViewPool sharedInstance] getReusedWebViewForHolder:self];
        }
        self.url = url;
    }
    return self;
}
复制代码

Here, without acquiring webView in viewDidLoad in initWithUrl in, because init, the page opening speed will be much faster.

3, pre-script add data to enhance the experience

This step is all based on business characteristics of the author's app: User Community list of posts (native) => Post the details (H5 achieve) => Personal centers (H5). When clicking through H5 details from the list, will be part of the pre-post data, such as Avatar, the first map thumbnails, content to pass the front end (the front end to get the data, this part of the pre-loaded data, while the first increase gradient map thumbnail appears the effect, then open the H5, page thumbnails to high gradient map blur from National Tsing Hua University, in order to achieve a native experience to open the page (final renderings end of this document). Note that this picture is passed to the front of the url, not the picture data below will continue to explain how to use the picture data.

H5 interact with native part of the code:

Model *modelMake = model;//列表点击的item数据
NSString *key = [NSString stringWithFormat:@"native_list_%@", modelMake.articleId];
NSData *data = [NSJSONSerialization dataWithJSONObject:[modelMake dictionaryValue] options:NSJSONWritingPrettyPrinted error:nil];
NSString *value = [[NSString alloc] initWithData:data?data:[NSData data] encoding:NSUTF8StringEncoding];
NSString *javaScript = [NSString stringWithFormat:@"!window.predatas && (window.predatas = []);predatas.push({key: \"%@\", value: %@ })", key, value];

WKUserContentController *userContentController = wkWebView.configuration.userContentController;
WKUserScript *userScript = [[WKUserScript alloc] initWithSource:javaScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:userScript];

复制代码

4, sliding return

Due to the use of H5 business development, from the list to the personal details and then to the center, then sliding directly back to the list page, not as a primary navigation as layers of return. To solve this problem, the first thought of using WKWebView allowsBackForwardNavigationGestures attributes, combined with goBack method webView can indeed return sliding layers, but the last occurrence will first return to first open the details page, and then will return to the list as well Some other abnormalities. After trying a number of programs, add their own gestures to achieve the eventual adoption of sliding return function.

Gesture created:

self.leftSwipGes = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(leftSwipGesAction:)];
self.leftSwipGes.edges = UIRectEdgeLeft;
self.leftSwipGes.delegate = self;
[self.webView addGestureRecognizer:self.leftSwipGes];
        
复制代码

achieve:

- (void)leftSwipGesAction:(UIScreenEdgePanGestureRecognizer *)ges {
    if (UIGestureRecognizerStateEnded == ges.state) {
        if (self.webView.backForwardList.backList.count > 0) {
            WKBackForwardListItem *item = webView.backForwardList.backList.lastObject;
            if (![self.webView.URL.absoluteString isEqualToString:self.url]) {
                [webView goToBackForwardListItem:item];
            } else {
                [self nativeBack:nil];
                [webView goToBackForwardListItem:item];
            }
        } else {
            [self nativeBack:nil];
        }
    }
}
复制代码

Wherein, nativeBack () method to return the native. Principle: When sliding, when url url of the current page is not the initial webview of H5, webView of backForwardList back level when retreated to the initial page, direct return list. Also, pay attention to deal with custom gestures with other gestures of conflict; while also disabling return skid system, as well as other third-party libraries disable sliding FDFullscreenPopGesture return.

Interception loaded offline package

Sign up good premise custom protocol, combined with their own specific projects to achieve when creating WKWebview, registered as long as you can guarantee create WKWebView:

WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];  
[configuration setURLSchemeHandler:[CustomURLSchemeHandler new] forURLScheme: @"customScheme"];    
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
复制代码

Interception

Also analyzed above, H5 open a page there will be a time of black and white, because it does a lot of things: 

初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片

So when opened to custom protocols customScheme as the scheme of H5 page, webview request page, native will in turn receive html, js, css, image type interception response:

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){
    
    NSDictionary *headers = urlSchemeTask.request.allHTTPHeaderFields;
    NSString *accept = headers[@"Accept"];
    
    //当前的requestUrl的scheme都是customScheme
    NSString *requestUrl = urlSchemeTask.request.URL.absoluteString;
    NSString *fileName = [[requestUrl componentsSeparatedByString:@"?"].firstObject componentsSeparatedByString:@"/"].lastObject;

    //Intercept and load local resources.
    if ((accept.length >= @"text".length && [accept rangeOfString:@"text/html"].location != NSNotFound)) {//html 拦截
      [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
    } else if ([self isMatchingRegularExpressionPattern:@"\\.(js|css)" text:requestUrl]) {//js、css
        [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
    } else if (accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound) {//image
      NSString *replacedStr = [requestUrl stringByReplacingOccurrencesOfString:kUrlScheme withString:@"https"];
      NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:replacedStr]];
      [[SDWebImageManager sharedManager].imageCache queryCacheOperationForKey:key done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
          if (image) {
              NSData *imgData = UIImageJPEGRepresentation(image, 1);
              NSString *mimeType = [self getMimeTypeWithFilePath:fileName] ?: @"image/jpeg";
              [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:mimeType requestData:imgData];
          } else {
              [self loadLocalFile:fileName urlSchemeTask:urlSchemeTask];
          }
      }];
    } else {//return an empty json.
        NSData *data = [NSJSONSerialization dataWithJSONObject:@{ } options:NSJSONWritingPrettyPrinted error:nil];
        [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:@"text/html" requestData:data];
    }
}
    
    //Load local resources, eg: html、js、css...
- (void)loadLocalFile:(NSString *)fileName urlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0)){
    if (fileName.length == 0 || !urlSchemeTask) {
        return;
    }
    
    //If the resource do not exist, re-send request by replacing to http(s).
    NSString *filePath = [kGrassH5ResourcesFiles stringByAppendingPathComponent:fileName];
    if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        if ([replacedStr hasPrefix:kUrlScheme]) {
            replacedStr = [replacedStr stringByReplacingOccurrencesOfString:kUrlScheme withString:@"https"];
        }
        
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:replacedStr]];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            [urlSchemeTask didReceiveResponse:response];
            [urlSchemeTask didReceiveData:data];
            if (error) {
                [urlSchemeTask didFailWithError:error];
            } else {
                [urlSchemeTask didFinish];
                
                NSString *accept = urlSchemeTask.request.allHTTPHeaderFields[@"Accept"];
                if (!(accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound)) { //图片不下载
                    [data writeToFile:filePath atomically:YES];
                }
            }
        }];
        [dataTask resume];
        [session finishTasksAndInvalidate];
    } else {
        NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil];
        [self resendRequestWithUrlSchemeTask:urlSchemeTask mimeType:[self getMimeTypeWithFilePath:filePath] requestData:data];
    }
}

- (void)resendRequestWithUrlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
                              mimeType:(NSString *)mimeType
                           requestData:(NSData *)requestData  API_AVAILABLE(ios(11.0)) {
    if (!urlSchemeTask || !urlSchemeTask.request || !urlSchemeTask.request.URL) {
            return;
        }
        
        NSString *mimeType_local = mimeType ? mimeType : @"text/html";
        NSData *data = requestData ? requestData : [NSData data];
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL
                                                            MIMEType:mimeType_local
                                               expectedContentLength:data.length
                                                    textEncodingName:nil];
        [urlSchemeTask didReceiveResponse:response];
        [urlSchemeTask didReceiveData:data];
        [urlSchemeTask didFinish];
    }
}

复制代码

I am here simply attached to the portion of the processing resource request intercept code: after receiving the intercept request packet to access resources corresponding to local resources, webView converted back to data rendering process; if not local, then substitute https customScheme the url retransmission request notification webview, which is the basic flow. The actual development and debugging process, there are a lot of details need to be addressed, such as when there is no local resources, retransmission request based on pre-match rule server issued; Another example is the use of different load replace html, and if the page has been opened black and white and so on, not listed here.

But I would also like to make two points:

1, replace the code in the picture logic, to find the local picture is designed to achieve the above-mentioned WebViewController transformation of Article III: Pre-script add data to enhance the experience , get a list showing thumbnails that have been passed SDWebImage cache webView pre-loaded in order to achieve the effect of the gradient appears. Then retransmission request notification to the local webview. Here, you should understand and optimize the realization seconds to open, the central idea is to reduce network resource request, to show the first page of the original elements as possible pre-loaded. 

2, during the test, the number of models on the poor machine, quickly open frequently H5 page, there will be a crash. Check WKURLSchemeTask official explanation:

An exception will be thrown if you try to send a new response object after the task has already been completed.
An exception will be thrown if your app has been told to stop loading this task via the registered WKURLSchemeHandler object.

The analysis found that when working with local non-existent pictures, first determine whether there is a local and then initiate the request, the time span is longer, the current urlSchemeTask ahead of the end for some reason (receive stopURLSchemeTask callback), then retransmitted WKURLSchemeTask requested by a visit of an example method (didReceiveResponse etc.) has led to the crash.
Solution: Add NSMutableDictionary member variables to the current urlSchemeTask do key, interception set up at the beginning of YES, NO stop setting notification received, before webview determine whether the current urlSchemeTask end, ahead of the end do not deal with each notice. To do so, the impact is the current picture will not be displayed, there will still quit come in again, combined with abnormal scenario occurs and crashes, this impact is acceptable.

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){
    
    dispatch_sync(self.serialQueue, ^{
        [self.holderDicM setObject:@(YES) forKey:urlSchemeTask.description];
    });
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){
    dispatch_sync(self.serialQueue, ^{
        [self.holderDicM setObject:@(NO) forKey:urlSchemeTask.description];
    });
}
复制代码

Note To add a serial queue for data protection against multiple threads simultaneously access to modify data, cause data anomalies.

to sum up

Here, the optimization is basically completed, open the page H5 indeed a lot faster. Our program is roughly like this, this is certainly not the best solution, more or less be some problems, I believe readers will have better optimization program, or encounter problems described above occur more reasonable solution, everyone welcome discuss.

Finally, we demonstrate to us optimize the effect (iPhone 7) to open the page after H5:



Guess you like

Origin blog.csdn.net/weixin_34414650/article/details/91370776