iOS app秒开H5优化总结

为了快速迭代,更新,大部分公司都用了h5去实现公司部分模块功能,而公司使用h5实现的模块的性能和原生还是有很大的差距,就衍生了如何优化h5的加载速度,和体验问题。

首先对wkwebview初始化优化

创建缓存池,CustomWebViewPool ,减少wkwebview 创建花销的时间
.h文件

@interface CustomWebViewPool : NSObject

+ (instancetype)sharedInstance;

/**
 预初始化若干WKWebView
 @param count 个数
 */
- (void)prepareWithCount:(NSUInteger)count;

/**
 从池中获取一个WKWebView
 
 @return WKWebView
 */

- (CustomWebView *)getWKWebViewFromPool;

.m文件

@interface CustomWebViewPool()
@property (nonatomic) NSUInteger initialViewsMaxCount;  //最多初始化的个数
@property (nonatomic) NSMutableArray *preloadedViews;

@end

@implementation CustomWebViewPool

+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static CustomWebViewPool *instance = nil;
    dispatch_once(&onceToken,^{
        instance = [[super allocWithZone:NULL] init];
    });
    return instance;
}

+ (id)allocWithZone:(struct _NSZone *)zone{
    return [self sharedInstance];
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.initialViewsMaxCount = 20;
        self.preloadedViews = [NSMutableArray arrayWithCapacity:self.initialViewsMaxCount];
    }
    return self;
}

/**
 预初始化若干WKWebView
 
 @param count 个数
 */
- (void)prepareWithCount:(NSUInteger)count {
    
    NSTimeInterval start = CACurrentMediaTime();
    
    // Actually does nothing, only initialization must be called.
    while (self.preloadedViews.count < MIN(count,self.initialViewsMaxCount)) {
        id preloadedView = [self createPreloadedView];
        if (preloadedView) {
            [self.preloadedViews addObject:preloadedView];
        } else {
            break;
        }
    }
    
    NSTimeInterval delta = CACurrentMediaTime() - start;
    NSLog(@"=======初始化耗时:%f",  delta);
}

/**
 从池中获取一个WKWebView
 @return WKWebView
 */
- (CustomWebView *)getWKWebViewFromPool {
    if (!self.preloadedViews.count) {
        NSLog(@"不够啦!");
        return [self createPreloadedView];
    } else {
        id preloadedView = self.preloadedViews.firstObject;
        [self.preloadedViews removeObject:preloadedView];
        return preloadedView;
    }
}

/**
 创建一个WKWebView
 @return WKWebView
 */
- (CustomWebView *)createPreloadedView {
    
    WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *wkUController = [[WKUserContentController alloc] init];
    wkWebConfig.userContentController = wkUController;
    
    CustomWebView *wkWebView = [[CustomWebView alloc]initWithFrame:CGRectZero configuration:wkWebConfig];
    //根据自己的业务需求初始化WKWebView
    wkWebView.opaque = NO;
    wkWebView.scrollView.scrollEnabled = YES;
    wkWebView.scrollView.showsVerticalScrollIndicator = YES;
    wkWebView.scrollView.scrollsToTop = YES;
    wkWebView.scrollView.userInteractionEnabled = YES;
    if (@available(iOS 11.0,*)) {
        wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    }
    wkWebView.scrollView.bounces = NO;
    wkWebView.backgroundColor = [UIColor clearColor];
    
    return wkWebView;
}

@end

创建CustomWebView

    CustomWebViewPool *webViewPool = [CustomWebViewPool sharedInstance];
    [webViewPool prepareWithCount:20];
    self.webView = [webViewPool getWKWebViewFromPool];

通过修改初始化缓存策略,从而实现初始化的优化(公司有人修改了缓存策略不使用本地缓存,哎)

NSMutableURLRequest *requst=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
[requst setCachePolicy: NSURLRequestUseProtocolCachePolicy];

注释

  1. NSURLRequestUseProtocolCachePolicy(常用)
    这个是默认缓存策略也是一个比较有用的缓存策略,它会根据HTTP头中的信息进行缓存处理。
    此处都说一句,缓存会将获取到的数据缓存的disk。具体验证和详细解析可以看NSURLCache Uses a Disk Cache as of iOS 5
    服务器可以在HTTP头中加入Expires和Cache-Control等来告诉客户端应该施行的缓存策略。在后面会详细介绍。
    1> NSURLRequestUseProtocolCachePolicy = 0, 默认的缓存策略, 如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端.

  2. NSURLRequestReloadIgnoringCacheData(偶尔使用)
    顾名思义 忽略本地缓存。使用场景就是要忽略本地缓存的情况下使用。3.NSURLRequestReturnCacheDataElseLoad(不用)
    这个策略比较有趣,它会一直偿试读取缓存数据,直到无法没有缓存数据的时候,才会去请求网络。这个策略有一个重大的缺陷导致它根本无法被使用,即它根本没有对缓存的刷新时机进行控制,如果你要去使用它,那么需要额外的进行对缓存过期进行控制。

  3. NSURLRequestReturnCacheDataDontLoad(不用)
    这个选项只读缓存,无论何时都不会进行网络请求

使用默认的话,wkwebview 能做到自动做相应的缓存,并在恰当的时间清除缓存
如果用 NSURLRequestReturnCacheDataElseLoad ,需要写相应的缓存策略
在AppDelegate的application: didFinishLaunchingWithOptions: 方法中写

NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024
                                                         diskCapacity:50 * 1024 * 1024
                                                             diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];

腾讯bugly发表的一篇文章《移动端本地 H5 秒开方案探索与实现》中分析,H5体验糟糕,是因为它做了很多事:

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


9a2f8beb.png
屏幕快照 2019-08-20 下午8.45.16.png

一般页面在 dom 渲染后才能展示,可以发现,H5 首屏渲染白屏问题的原因关键在于,如何优化减少从请求下载页面到渲染之间这段时间的耗时。 所以,减少网络请求,采用加载离线资源加载方案来做优化。

核心原理:

在app打开时,从服务端下载h5源文件zip包,下载到本地,通过url地址做本地拦截,判断该地址本地是否有源文件,有的话,直接加载本地资源文件,减少从请求下载页面到渲染之间这段时间的耗时,如果没有的话,在请求网址

将h5源文件打包成zip包下载到本地

为此可以把所有资源文件(js/css/html等)整合成zip包,一次性下载至本地,使用SSZipArchive解压到指定位置,更新version即可。 此外,下载时机在app启动和前后台切换都做一次检查更新,效果更好。

注释:
zip包内容:css,js,html,通用的图片等
下载时机:在app启动的时候,开启线程下载资源,注意不要影响app的启动。
存放位置:选用沙盒中的Library/Caches。
因为资源会不定时更新,而/Library/Documents更适合存放一些重要的且不经常更新的数据。
更新逻辑:把所有资源文件(js/css/html等)整合成zip包,一次性下载至本地,使用SSZipArchive解压到指定位置,更新version即可

//获取沙盒中的Library/Caches/路径
 NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
 NSString *dirPath = [docPath componentsSeparatedByString:@"loadH5.zip"];
 NSFileManager *fileManager = [NSFileManager defaultManager];
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];

WKURLSchemeHandler

iOS 11上, WebKit 团队终于开放了WKWebView加载自定义资源的API:WKURLSchemeHandler。

根据 Apple 官方统计结果,目前iOS 11及以上的用户占比达95%。又结合自己公司的业务特性和面向的用户,决定使用WKURLSchemeHandler来实现拦截,而iOS 11以前的不做处理。

着手前,要与前端统一URL-Scheme,如:customScheme,资源定义成customScheme://xxx/path/xxxx.css。native端使用时,先注册customScheme,WKWebView请求加载网页,遇到customScheme的资源,就会被hock住,然后使用本地已下载好的资源进行加载。

客户端使用直接上代码:

注册

@implementation ViewController
- (void)viewDidLoad {    
    [super viewDidLoad];    
    WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
    //设置URLSchemeHandler来处理特定URLScheme的请求,URLSchemeHandler需要实现WKURLSchemeHandler协议
    //本例中WKWebView将把URLScheme为customScheme的请求交由CustomURLSchemeHandler类的实例处理    
    [configuration setURLSchemeHandler:[CustomURLSchemeHandler new] forURLScheme: @"customScheme"];    
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];    
    self.view = webView;    
    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"customScheme://www.test.com"]]];
}
@end

复制代码

注意:

  1. setURLSchemeHandler注册时机只能在WKWebView创建WKWebViewConfiguration时注册。
  2. WKWebView 只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。
  3. 【补充】WKWebView加载网页前,要在user-agent添加个标志,H5遇到这个标识就使用customScheme,否则就是用原来的http或https。

拦截

#import "ViewController.h"
#import <WebKit/WebKit.h>

@interface CustomURLSchemeHandler : NSObject<WKURLSchemeHandler>
@end

@implementation CustomURLSchemeHandler
//当 WKWebView 开始加载自定义scheme的资源时,会调用
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){

    //加载本地资源
    NSString *fileName = [urlSchemeTask.request.URL.absoluteString componentsSeparatedByString:@"/"].lastObject;
    fileName = [fileName componentsSeparatedByString:@"?"].firstObject;
    NSString *dirPath = [kPathCache stringByAppendingPathComponent:kCssFiles];
    NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];

    //文件不存在
    if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        NSString *replacedStr = @"";
        NSString *schemeUrl = urlSchemeTask.request.URL.absoluteString;
        if ([schemeUrl hasPrefix:kUrlScheme]) {
            replacedStr = [schemeUrl stringByReplacingOccurrencesOfString:kUrlScheme withString:@"http"];
        }

        NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:replacedStr]];
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

        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];
            }
        }];
        [dataTask resume];
    } else {
        NSData *data = [NSData dataWithContentsOfFile:filePath];

        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:urlSchemeTask.request.URL
                                                            MIMEType:[self getMimeTypeWithFilePath:filePath]
                                               expectedContentLength:data.length
                                                    textEncodingName:nil];
        [urlSchemeTask didReceiveResponse:response];
        [urlSchemeTask didReceiveData:data];
        [urlSchemeTask didFinish];
    }
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id)urlSchemeTask {
}

//根据路径获取MIMEType
- (NSString *)getMimeTypeWithFilePath:(NSString *)filePath {
    CFStringRef pathExtension = (__bridge_retained CFStringRef)[filePath pathExtension];
    CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, NULL);
    CFRelease(pathExtension);

    //The UTI can be converted to a mime type:
    NSString *mimeType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType);
    if (type != NULL)
        CFRelease(type);

    return mimeType;
}

@end
复制代码

分析,这里拦截到URLScheme为customScheme的请求后,读取本地资源,并返回给WKWebView显示;若找不到本地资源,要将自定义 Scheme 的请求转换成 http 或 https 请求用NSURLSession重新发出,收到回包后再将数据返回给WKWebView。

综上总结:此方案需要服务端,h5端,iOS端共同合作讨论完成,此方案具有一定风险性,最后在做这种方式的是有有个开关处理,防止线上因为这种改动导致大规模网页加载不了的问题。
服务端:
需要把所涉及到的h5页面的源文件上传到服务端,并为客户端提供接口,供前端下载
h5:
1.需要做iOS和安卓的来源判断,可以通过user-agent,加个特殊字段做,也可以通过js直接写个本地方法
2.需要前端统一URL-Scheme,且注意跨域问题

腾讯UIWebView秒开框架
轻量级高性能Hybrid框架VasSonic秒开实现解析
Github: Tencent/VasSonic

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 注:本篇研究重点不在于某个离线方案的具体使用,而在于对方案的优缺点分析、探究和选型,以及一些我个人的看法。 前言 ...
    LotLewis阅读 9,594评论 7 16
  • iOS app秒开H5实战总结 在《iOS app秒开H5优化探索》一文中简单介绍了优化的方案以及一些知识点,本文...
    叩首问路梦码为生阅读 2,509评论 2 8
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 《好 喜欢你》 好喜欢你 初次相遇的时候 好喜欢你 睡前想你的时候 好喜欢你 当你在生气的时候 好喜欢你 当我们聊...
    XIESHI阅读 252评论 0 2
  • 士兵来报:“大人,皇室建议城中开设聚贤馆。” “聚贤馆?已然开了,不过挂了别的名字而已。”城主边说...
    两晋阅读 171评论 0 0