概述
在客户端开发中,列表类型页面大多都依赖网络恳求,需要等网络数据恳求下来后再改写页面。但遇到网络恳求慢的场景,就会导致页面加载很慢乃至加载失利。
我担任会员的产品列表页面,在事务场景中,页面元素比较杂乱,而且涉及多个接口。最开始涉及十个左右的接口,经过我推进聚合后还有三个接口。所以,在进入产品列表页面时,需要等三个接口都恳求完结才能改写页面,这样会导致进入页面速度很慢。
整体思路
关于页面加载慢乃至失利的状况,能够对页面进行预加载,预加载也细化为prefetch
和preload
两部分,这两部分在计划中都包含。
计划先经过缓存数据将页面进行烘托,进入产品列表页后,再依据恳求下来的服务器数据,决定是否用服务器数据对页面进行改写。而且,用服务器的数据替换本地数据,供下次运用。看似比较简略,实际上做起来有很多细节需要处理和优化。后边的文章,将产品列表页统称为产品页。
prefetch
接口缓存
现在进入产品页有三个接口,接口数据恳求下来后,会经过SVRequest
网络库自带的缓存功用,对页面数据进行缓存。下次进入页面时,会先读取本地缓存数据,假如本地有缓存数据,会先用缓存数据改写页面。改写后,等网络数据恳求下来,会判别网络数据和本地数据的一致性,依据成果决定是否用网络数据进行reload
。
[self startRequestWithType:SVRequestTypeGet requestURL:requestURL cacheable:YES params:params cacheBlock:^(SVNetworkCache * _Nullable cache, SVRequestControl * _Nonnull requestControl) {
if (cache.cacheObj && [Reachability currentNetworkType] == NotReachable) {
[requestControl stop];
} else {
[requestControl goOn];
}
[self parseData:data successBlock:cacheBlock];
} successBlock:^(id _Nonnull responseObject) {
if (successBlock) {
NSDictionary *data = [responseObject as:[NSDictionary class]];
[self parseData:data successBlock:successBlock];
}
} failureBlock:^(SVRequestError * _Nonnull error, id _Nonnull responseObject) {
if (failedBlock) {
failedBlock(error, responseObject);
}
}];
新用户
可是,关于新安装的用户,或许旧版别升级上来的用户,他们并没有本地缓存数据,之前版别也没有敞开缓存功用。这样第一次进入产品页,仍然要等候网络数据恳求下来后,再改写页面。
为了提升这一部分用户的体会,在产品页的主要进口的方位,在进口页面显现后,会在后台现成check
缓存数据状态。假如没有缓存数据,则会预先恳求服务器数据,并写入本地缓存,这样能够确保进入产品页,页面不会为空。由于仅针对新用户和旧版别升级上来的用户,所以恳求数量添加有限,不会导致过多的后台压力。
而且,由于是多个接口,所以做的是每个接口的按需加载,只有没缓存数据的接口才会恳求。一般,一个接口没数据其他两个也都没数据。可是,这个战略是防止阅读进口页面时,其中有一个接口恳求失利,但没进入产品页,下次再阅读到进口页面还会继续恳求,提高了缓存命中率。
preload
布局
为了确保push
进入页面时,用户看到的便是已经烘托好的页面,所以需要对页面进行preload
并烘托,时机选在初始化页面时进行。在初始化页面后,会依据本地读取的cacheData
对页面进行layout
,而且会调用layoutIfNeeded
强制触发图形树中每个节点的布局。
这个进程相对比较顺滑,依据页面FPS
的监测,并没有出现明显的FPS
下降。而且,为了防止preload
的进程影响埋点的准确性,将埋点和preload
的进程进行剥离,当页面真正显现的时候才会进行上报。
从功能的角度,假如想在preload
进程中坚持比较高的FPS
,应该防止产生离屏烘托和杂乱的布局,这两项都是比较耗费CPU
的,CPU
耗费的添加就会影响主线程的运行,然后导致卡顿。而体系烘托操作是经过GPU
进行的,不会过多耗费CPU
功能,而且烘托操作相关于animation
和交互式的gesture
所带来的功能耗费,会少很多。
烘托原理
上面讲到了preload
的话题,这儿正好简略剖析下页面烘托的原理。
先简略说明一些常见的关键词,UIView
担任布局和事情响应,CALayer
担任页面的烘托。先对视图进行制作,例如三角型、纹路的核算,最后再烘托成bitmap
交给帧缓冲区,制作和烘托是一个先后次序。
iOS
体系上采用双缓冲区机制,frame buffer
前帧缓冲区,以及back buffer
后帧缓冲区。CALayer
不会直接跟frame buffer
打交道,一般都是提交给back buffer
。
烘托的进程,总的来说分为三步。
- 当收到
VSync
信号后,App
会经过CPU
在主线程,核算显现内容,例如视图的创立和布局。 - 随后将核算成果提交到
GPU
,进行改换、组成、烘托等操作,GPU
会将烘托后的成果交给back buffer
。 - 视频控制器收到下一个
VSync
信号后,会将上次烘托的back buffer
中的bitmap
显现到屏幕上。但假如CPU
和GPU
没有核算完结,这一阵就会被丢弃,然后导致掉帧。
上述烘托逻辑对应到iOS
体系上便是如下逻辑。
- 当
VSync
信号到来时,视频控制器会从CALayer
的contents
中取走bitmap
,并显现在屏幕上。 -
contents
的bitmap
核算逻辑如下。 -
UIView
担任布局,当页面布局产生改动后,由UIView
的layoutSubviews
来完结核算,这个进程是经过CPU
进行的。 - 布局完结后,
UIView
会调用setNeedsDisplay
,而且调用CALayer
的同名办法setNeedsDisplay
,这个进程相当于做一个标记,下次runloop
循环会进行烘托。 -
CALayer
的display
办法会判别是否完结了displayLayer:
办法,在办法中咱们能够完结异步制作办法,没有完结则进入体系默认制作办法。 -
CALayer
会经过CGContextRef
创立一个backing store
,后续的制作都在这个context
上进行,包含自界说的drawRect
。 - 调用
drawInContext:
办法进行体系制作,由Core Graphics
的API
在context
上完结制作操作。 - 将制作的成果烘托后的
bitmap
存储在contents
特点中,bitmap
也便是一张位图。
reload
改写逻辑
为了确保用户体会,用缓存数据展现页面后,当网络数据恳求下来,会对网络数据进行比对,假如网络数据不同则用网络数据改写页面,以确保页面的准确性。假如网络数据和本地数据相同,则没必要进行一次无谓的改写,会带来额定的功能耗费,以及欠好的用户体会。
可是,产品页和其他事务还不太一样,并不是单一数据接口,所以规划一套灵活且适用于多个接口,进行hash
比对的manager
就比较重要。为了处理这个问题,规划了一套简略的多接口hash
比对的计划。
多接口hash
计划用SVPCacheManager
类来完结,能够对多个接口的hash
进行管理。主要有几个职责,搜集缓存hash
、搜集网络数据hash
、多个hash
的比对。整体是经过两个数组完结的,cacheHash
担任搜集缓存数据hash
的,netHash
担任搜集接口数据hash
。由于涉及多个接口,所以采用数组的规划,每个接口对应一个index
,相同接口的缓存和网络数据核算的hash
,搜集时对应同一个index
,即可确保次序的问题。
为了确保通用性,也能够应用在其他接口的处理上,所以在初始化数组时是经过config
配置count
的。
@objcMembers class SVPCacheManager: NSObject {
var cacheHash: [String]?
var netHash: [String]?
@objc static let shared = SVPCacheManager()
func config(count: Int) {
cacheHash = [String](repeating: "", count: count)
netHash = [String](repeating: "", count: count)
}
func appendCache(index: Int, hash: String) {
if hash.length > 0 {
cacheHash?[index] = hash
}
}
func appendNet(index: Int, hash: String) {
if hash.length > 0 {
netHash?[index] = hash
}
}
func isEqual() -> Bool {
return cacheHash == netHash
}
}
易变参数
产品页接口有很多简单产生改动的字段,例如活动模版会有和时刻相关的expire time
时刻戳,或许H5
页面用的html
标签字符串,以及一些用不到的play count format
。这些字段都很简单产生变化,而且会导致cacheManager
的hash
值匹配失利。
为了添加匹配度,关于缓存数据和网络数据中,这些没用的易变参数,经过KVC
的办法去掉。对处理后的Dictionary
核算hash
,这样能够使网络数据和缓存的匹配率大大提升,提升用户体会。
有序字典
字典是一个无序的数据结构,在生成hash
时,是经过接口数据去除易变参数后,对Dictionary
字符串生成的md5
作为hash
。但由于体系对json
转化Dictionary
的进程并不安稳,导致每次key
的先后次序都是不同的,而且这个进程没有规则。
下面便是一个相同接口,两次不同恳求转为Dictionary
后,key
、value
的对比。这样的次序,相同的数据根本每次比对都会导致匹配失利,最后核算的hash
值也是不同的。
这时候重要的便是把无序的字典变为“有序”,做法是自界说字典的遍历办法,界说一个可变字符串,从根节点出发,一层一层进行递归遍历。
先对根节点的key
数组进行排序,并将排序成果转为字符串后,append
给可变字符串。再经过有序key
数组取出对应的value
,先将非字典和数组value
,逐个append
到可变字符串上,随后再递归调用该办法,并将value
为字典和数组的值传入。假如传入的是数组对象,则先遍历非字典和数组的value
,逐个append
到可变字符串上,再进行递归调用并传入参数。
一向递归重复上面的动作,直到叶子节点停止,整体思路便是经过可变字符串,一层层拼接排序后的key
和value
,最后用拼接后的可变字符串核算md5
作为hash
。为了确保成果的准确性,不能只对value
进行遍历,因为要考虑相同value
但取值逻辑不同的状况。
analyze
为了方便进行数据分析,在之前的版别中已经对接口恳求速度添加了埋点,计算规则是接口开始恳求前,到三个接口都恳求结束的时刻,来核算恳求接口总计耗费的时刻。由于做了数据缓存后,刚进入页面时不需要恳求完数据再展现页面,而是直接从本地读取数据。所以,这个计算点的数值根本为0
。
我认为,优化后应该重视的,应该是缓存匹配度的问题。假如用本地数据改写页面后,网络数据和本地匹配,进入页面后没有reload
也便是二次改写的问题,这样关于用户体会便是好的,优化目的也是为了有更好的用户体会。