簡(jiǎn)介
LNAsyncKit是一個(gè)異步渲染工具,它提供了便捷的方法幫助你將多個(gè)元素(Element)異步渲染到一張圖片上,讓這個(gè)過程代替UIKit的視圖構(gòu)建過程,進(jìn)而優(yōu)化App性能;Prender提供預(yù)加載策略幫助你在Feed流中彌補(bǔ)異步渲染帶來的延時(shí);除構(gòu)建視圖外,Transaction提供更優(yōu)雅的方式讓主線程與子線程交互,并能根據(jù)機(jī)器狀態(tài)控制并發(fā)數(shù)和主線程回調(diào)時(shí)機(jī)。
LNAsyncKit借(ji)鑒(cheng)了很多YYKit和Texture,如果對(duì)它們不是很了解可以戳這個(gè)比較詳細(xì)的文章,這篇文章的作者是YY大神:iOS保持頁(yè)面流暢的技巧。流暢性優(yōu)化的思想基本上都如這篇文章所述。
它可以提供哪些幫助
- 還沒有找到方案優(yōu)化圓角、邊框、漸變的優(yōu)化方案,LNAsyncKit可以異步解決這些。
- Feed流需要預(yù)加載策略,LNAsyncKit提供預(yù)加載區(qū)域計(jì)算方案(這個(gè)方案也用來預(yù)合成)。
- 提供一種與UIKit十分接近的方式構(gòu)建需要預(yù)合成的圖層,讓你的復(fù)雜圖層構(gòu)建都放在子線程進(jìn)行,且不會(huì)創(chuàng)建那么多UIView。
- Demo展示了使用:AFNetworking/SDWebImage/IGListKit/YYModel/MJRefresh + LNAsyncKit搭建feed流的方法。除去LNAsyncKit,前面5個(gè)構(gòu)成的這套體系已經(jīng)比較完整,Demo中也提供了沒有使用LNAsyncKit構(gòu)建的Feed。因此,需要快速學(xué)習(xí)如何搭建一套Feed流的初學(xué)者可以參考這套三方。
Github鏈接
你可以直接下載這個(gè)鏈接并運(yùn)行上面的Demo參考代碼實(shí)現(xiàn)自己的異步列表,也可以直接使用Cocoapods??
pod 'LNAsyncKit'
流暢性優(yōu)化
網(wǎng)絡(luò)上已經(jīng)有了很多流暢性優(yōu)化的文章,再逐一復(fù)述這些優(yōu)化點(diǎn)意義不大;這個(gè)文章是為了表達(dá)如何在Feed流中實(shí)現(xiàn)那些優(yōu)化思想,并把這個(gè)過程簡(jiǎn)化;所以,不再贅述這些優(yōu)化點(diǎn)為什么好、好多少,只談怎么實(shí)現(xiàn)它們;如果對(duì)這些優(yōu)化點(diǎn)有疑問可以參考上面鏈接的文章,以下這些觀點(diǎn)成立:
- 圖層少的列表比圖層多的列表好。
- 沒有圓角、邊框、漸變等復(fù)雜圖層的比有的好。
- 圖片尺寸和控件尺寸一樣大的好。
- 模型解析放在子線程比放在主線程好。
- 布局計(jì)算放在子線程比放在主線程好。
- 有預(yù)加載比沒有預(yù)加載好(見仁見智,也有喜歡無(wú)預(yù)加載列表的)。
- Layer比View好(無(wú)手勢(shì)時(shí))。
- 不透明圖層比透明圖層好。
在業(yè)務(wù)復(fù)雜度不變的前提下讓這些優(yōu)化工作變簡(jiǎn)單、自由就是LNAsyncKit的目標(biāo)。
優(yōu)化一個(gè)Cell
我們將一個(gè)Cell視為Feed流的最小優(yōu)化單元,以一個(gè)Bilibili推薦Feed流中一個(gè)常規(guī)的Cell為例:
這樣一個(gè)小Cell中包含了:封面圖、人數(shù)圖標(biāo)、人數(shù)Label、主播昵稱、直播間名、[直播]、直播內(nèi)容分類、負(fù)反饋按鈕8個(gè)元素;除了這些元素外,還包括封面圖底部一個(gè)黑色漸變的圖層、[直播]的圓角、邊框和整個(gè)Cell的圓角(好像還有些陰影);這個(gè)小Cell已經(jīng)包含比較多的小元素了,我們?cè)贒emo中嘗試復(fù)原一下并查看視圖層級(jí)大致如下:
具體構(gòu)建代碼這里不贅述了,使用LNAsyncKit可以簡(jiǎn)化這個(gè)Cell為如下這個(gè)樣子:
(右下角反饋Bug需要響應(yīng)事件,通常這種控件會(huì)保持獨(dú)立)
以“直播”標(biāo)簽為例,視圖構(gòu)建方式區(qū)別如下:
UIKit:
self.liveTagLabel.layer.cornerRadius = 3.f;
self.liveTagLabel.layer.borderColor = [UIColor colorWithRed:239.f/255.f green:91.f/255.f blue:156.f/255.f alpha:1.f].CGColor;
self.liveTagLabel.layer.borderWidth = 1.f;
self.liveTagLabel.text = @"直播";
self.liveTagLabel.font = [UIFont systemFontOfSize:12.f];
self.liveTagLabel.textColor = [UIColor colorWithRed:239.f/255.f green:91.f/255.f blue:156.f/255.f alpha:1.f];
self.liveTagLabel.textAlignment = NSTextAlignmentCenter;
[self.cellContentView addSubview:self.liveTagLabel];
LNAsyncKit:
LNAsyncTextElement *liveTagElement = [[LNAsyncTextElement alloc] init];
liveTagElement.cornerRadius = 3.f;
liveTagElement.borderColor = [UIColor colorWithRed:239.f/255.f green:91.f/255.f blue:156.f/255.f alpha:1.f];
liveTagElement.borderWidth = 1.f;
liveTagElement.text = @"直播";
liveTagElement.font = [UIFont systemFontOfSize:12.f];
liveTagElement.textColor = [UIColor colorWithRed:239.f/255.f green:91.f/255.f blue:156.f/255.f alpha:1.f];
liveTagElement.textAligment = NSTextAlignmentCenter;
[cellContentElement addSubElement:liveTagElement];
經(jīng)過LNAsyncKit渲染出與需要展示視圖面積一樣大的一張完整圖片,復(fù)雜渲染邏輯全部被子線程消化,反饋到主線程只表現(xiàn)為一張與目標(biāo)控件大小一致的圖片。
原理
與UIKit類似,LNAsyncKit也使用視圖樹構(gòu)建最終視圖。區(qū)別是:
A. Element繼承自NSObject,這些Element可以在子線程創(chuàng)建、渲染、銷毀??梢詫lement理解為“一個(gè)需要繪制圖層”的描述物,它并不是一個(gè)實(shí)體,它與UIView/CALayer的區(qū)別就好像:UIView是你要買的一件物品;Element則是下單信息,里面包含這件物品的各種描述信息,多大、什么顏色等。
B. 所有的Element都是臨時(shí)的,這些信息在構(gòu)建出結(jié)果后就會(huì)被銷毀,你可以在進(jìn)入子線程之后創(chuàng)建這些Element,在渲染出真正的圖片后銷毀這些Element,然后在主線程返回需要的圖片,像這樣:
dispatch_queue_t queue = dispatch_queue_create(0, 0);
dispatch_async(queue, ^{
LNAsyncElement *contentElement = [weakSelf rebuildElements];
[LNAsyncRenderer traversalElement:contentElement];
UIImage *image = contentElement.renderResult;
contentElement.renderResult = nil;
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.imageView.image = image;
});
});
rebuildElement的過程可以構(gòu)建出很復(fù)雜的一棵樹,但對(duì)主線程來說,這并不會(huì)造成問題!不在主線程出現(xiàn)Element也是LNAsyncKit推薦的使用方法(拿到resultImage后,把Element.resultImage置為空),當(dāng)然,出現(xiàn)了一般也無(wú)所謂,NSObject的消耗相對(duì)于UIView來講是很小的。
C.Element是逐層渲染的:實(shí)際上是后續(xù)遍歷,把A的子Element先渲染出來,然后渲染A,再把A當(dāng)做一個(gè)子節(jié)點(diǎn)渲染父節(jié)點(diǎn),LNAsyncRendererTraversalStack就是遍歷時(shí)使用的棧、LNAsyncRenderer.traversal函數(shù)是遍歷方法。遍歷中自帶了環(huán)檢測(cè),不會(huì)渲染重復(fù)Element,像這樣:
LNAsyncRendererTraversalStack *stack = [[LNAsyncRendererTraversalStack alloc] init];
[stack pushElements:@[element]];
NSMutableSet <LNAsyncElement *> *repeatDetectMSet = [[NSMutableSet alloc] init];
while (!stack.isEmpty) {
LNAsyncElement *topElement = [stack top];
if (topElement.getSubElements.count > 0 && (![repeatDetectMSet containsObject:topElement])) {
[repeatDetectMSet addObject:topElement];
[stack pushElements:topElement.getSubElements.reverseObjectEnumerator.allObjects];
} else {
[stack pop];
[self renderElement:topElement];
for (LNAsyncElement *subElement in topElement.getSubElements) {
subElement.renderResult = nil;
}
}
}
LNAsync自帶了一些Element:
- LNAsyncElement: 對(duì)應(yīng)于UIKit的UIView,是其他Element的基類,包含了背景色、frame、和常用的邊界、圓角等屬性。
- LNASyncImageElement: 對(duì)應(yīng)于UIImageView,渲染一張圖片、提供三種填充方式。
- LNAsyncTextElement: 對(duì)應(yīng)于UILabel,渲染一段文字,提供常規(guī)文字屬性、支持折行。
- LNAsyncLinerGradientElement: 對(duì)應(yīng)于CAGradientLayer,渲染一段漸變色。
自定義Element:
除原生Element外,我們也推薦封裝自己的Element,例如:一個(gè)AvatarElement,可以將用戶頭像、VIP標(biāo)識(shí)、頭像邊框等修飾物渲染在一起,重寫- (void)renderSelfWithContext:(CGContextRef)context,在這個(gè)方法中分別繪制這三個(gè)元素。
自定義Element的意義在于,所有自定義過的Element都是可復(fù)用、可組合的,這樣方便保持整個(gè)App風(fēng)格統(tǒng)一,也會(huì)適當(dāng)減少開發(fā)成本。
Feed流
上面我們已經(jīng)介紹過單個(gè)Cell、單張圖片如何異步渲染以優(yōu)化性能,但性能問題往往不是單張圖片所能引發(fā),LNAsyncKit更傾向于性能敏感的場(chǎng)景:Feed流;渲染Feed流相比渲染單個(gè)視圖需要考慮的事情要多一些:Cell復(fù)用、渲染好的圖片緩存、多張圖片下載和結(jié)果合并等問題;除此之外,也考慮使用預(yù)加載、預(yù)渲染功能來優(yōu)化用戶體驗(yàn)。
使用到的三方庫(kù):
- AFNetworking 網(wǎng)絡(luò)
- IGListKit Feed流框架,可以拆分各個(gè)模塊業(yè)務(wù)
- SDWebImage 圖片下載
- YYModel 字典轉(zhuǎn)模型
- MJRefresh 上拉/下拉刷新組件
- 一位大佬寫的免費(fèi)API ,雖然我不認(rèn)識(shí)這位大佬,但這些接口確實(shí)非常方便,在這里面朝空氣感謝一下~
這些都是非常成熟的三方框架,直接拿來用會(huì)減少不少開發(fā)時(shí)間;這里主要是介紹如何將LNAsyncKit融進(jìn)這個(gè)體系中去。Demo中已經(jīng)提供了默認(rèn)的Feed流和異步的Feed流代碼,如果遇到了一些奇怪的Bug可以參考Demo中的實(shí)現(xiàn),目前這兩個(gè)Demo都可以正常運(yùn)行。
- 默認(rèn)Demo:我們用此Demo展示一個(gè)常規(guī)Feed流實(shí)現(xiàn)過程,沒有使用任何修飾手法或設(shè)計(jì)思想,可以理解為實(shí)現(xiàn)一個(gè)Feed流所需要做的最少工作。
- 異步Demo:我們用此Demo將使用LNAsyncKit實(shí)現(xiàn)Feed流時(shí)與通常情況下的實(shí)現(xiàn)的進(jìn)行對(duì)比,了解從普通Feed轉(zhuǎn)異步Feed的修改點(diǎn)和差異之處。
默認(rèn)Feed流實(shí)現(xiàn):
- ViewDidLoad中使用AFNetworking請(qǐng)求一頁(yè)數(shù)據(jù),使用YYModel解析成Model類型數(shù)據(jù),賦值給VC。
- VC調(diào)用CollectionView/IGList刷新列表,將Model賦值到Cell內(nèi)部。
- Cell內(nèi)部賦值懶加載的Label、ImageView調(diào)用sd_setImage下載圖片展示。
異步Feed流的優(yōu)化:
1.圖片下載放在Model中進(jìn)行
A.因?yàn)楫惒紽eed不僅僅需要下載圖片,也需要將多個(gè)原始圖片進(jìn)行預(yù)合成,所以這個(gè)過程在Model中進(jìn)行可以保證不會(huì)因Cell復(fù)用問題導(dǎo)致同一時(shí)間合成多次,如果你在Cell中異步進(jìn)行圖層合成,那可能每次賦值Model都會(huì)合成一次,但在Model中合成后可以一直存放在Model中(Model只持有弱引用,存在全局的NSCache中)。
B.考慮預(yù)加載,我們認(rèn)為圖層的預(yù)加載和預(yù)合成是兩種優(yōu)先級(jí)的事情,通常距離屏幕焦點(diǎn)區(qū)域較遠(yuǎn)的區(qū)域只需要進(jìn)行圖片預(yù)下載,而距離較近的地方則需要預(yù)合成,不論是哪種方式,Cell通常只會(huì)在展示在屏幕上的時(shí)間點(diǎn)附近才能拿到,如果圖片下載放在Cell中進(jìn)行,是很難實(shí)現(xiàn)“預(yù)”的。
MVC中Model的職責(zé)之一是提供View展示需要的數(shù)據(jù),所以在Model中下載圖片并非錯(cuò)誤或不恰當(dāng)?shù)淖龇ā?/p>
2.模型解析和布局計(jì)算視為網(wǎng)絡(luò)請(qǐng)求的一部分
通常,在使用AFNetworking進(jìn)行網(wǎng)絡(luò)請(qǐng)求時(shí),我們通常在成功回調(diào)中進(jìn)行模型解析和列表刷新,列表刷新時(shí)走CollectionView的dataSource協(xié)議計(jì)算布局。
異步列表不推薦這樣做:模型解析的過程沒有想象中的那樣簡(jiǎn)單,通常進(jìn)行模型解析時(shí)需要逐層遍歷Dictionary,然后創(chuàng)建大量Model和子Model,雖然單個(gè)NSObject開銷不大,但列表視圖的模型總是堆積起來的,創(chuàng)建如此多的對(duì)象也是個(gè)不小的開銷。
計(jì)算布局的耗時(shí)是公認(rèn)的,所以一般表視圖優(yōu)化都推薦緩存行高,但即便緩存行高,第一次在主線程中的計(jì)算也是有一定耗時(shí)的。
我們推薦在AFNetworking回調(diào)中異步進(jìn)行模型解析和布局計(jì)算,將這兩個(gè)操作視為網(wǎng)絡(luò)請(qǐng)求的一部分,這并不會(huì)對(duì)網(wǎng)絡(luò)請(qǐng)求的整體響應(yīng)時(shí)間有較大的影響,因?yàn)榫W(wǎng)絡(luò)回調(diào)時(shí)間單位通常要比屏幕刷新時(shí)間單位高出一個(gè)數(shù)量級(jí)。況且,預(yù)加載技術(shù)完全可以彌補(bǔ)這段小延時(shí)。
在請(qǐng)求回調(diào)中賦值給Model的LayoutObj就是對(duì)這個(gè)過程的封裝,像這樣:
- (void)transferFeedData:(NSDictionary *)dic comletion:(DemoFeedNetworkCompletionBlock)completion
{
LNAsyncTransaction *transaction = [[LNAsyncTransaction alloc] init];
[transaction addOperationWithBlock:^id _Nullable{
DemoFeedModel *feedModel = [DemoFeedModel yy_modelWithDictionary:dic];
for (DemoFeedItemModel *item in feedModel.result) {
DemoAsyncFeedDisplayLayoutObjInput *layoutInput = [[DemoAsyncFeedDisplayLayoutObjInput alloc] init];
layoutInput.contextString = item.title;
layoutInput.hwScale = 0.3f + ((random()%100)/100.f)*0.5f;
DemoAsyncFeedDisplayLayoutObj *layoutObj = [[DemoAsyncFeedDisplayLayoutObj alloc] initWithInput:layoutInput];
item.layoutObj = layoutObj;
}
return feedModel;
} priority:1 queue:_transferQueue completion:^(id _Nullable value, BOOL canceled) {
if (completion) {
completion(YES, value, nil);
}
}];
[transaction commit];
}
3.在Model中布局
這聽起來有點(diǎn)詭異,在Model中下載圖片也就算了,為什么視圖操作也在Model中進(jìn)行?
我們已經(jīng)解釋了Element的職責(zé),它只是負(fù)責(zé)描述的類。使用element構(gòu)建視圖的過程就是:Model想好要怎么構(gòu)建(Element),把想法交付LNAsyncRenderer,renderer交付我們image,Model把image反回給View顯示出來。就像我們?cè)陂_始的時(shí)候講述的那樣。
4.預(yù)加載
預(yù)加載主要內(nèi)容包括兩個(gè)方面:預(yù)加載下一頁(yè)信息和預(yù)加載圖片。這里提到的預(yù)加載主要是指預(yù)加載圖片:
根據(jù)上面我們提到了圖片加載都是在Model中進(jìn)行的,所以,每個(gè)Model都需要一個(gè)必要的參數(shù)來標(biāo)記自身所持有的資源已經(jīng)到了那種緊急的程度了,如果距離當(dāng)前用戶焦點(diǎn)還很遠(yuǎn),說明自己的資源目前不是很緊急,可以先靜觀其變;如果距離用戶焦點(diǎn)有點(diǎn)近了,說明自己可能需要開始考慮先把圖片下載下來;如果距離用戶焦點(diǎn)已經(jīng)相當(dāng)近了,就要立刻開始準(zhǔn)備把已有資源預(yù)合成了。類似這樣:
- (void)setStatus:(DemoFeedItemModelStatus)status
{
if (status > _status) {
_status = status;
}
[self checkCurrentStatus];
}
- (void)checkCurrentStatus
{
if (self.status >= DemoFeedItemModelStatusPreload) {
//需要預(yù)加載圖片
[self preloadImage];
}
if (self.status >= DemoFeedItemModelStatusDisplay) {
//需要渲染視圖
[self renderView];
}
}
LNAsyncCollectionViewPrender提供了一套資源緊急程度標(biāo)記策略,將距離當(dāng)前屏幕中心較遠(yuǎn)的資源標(biāo)記為不緊急,較近的資源標(biāo)記為緊急,Model受緊急程度標(biāo)記影響自主進(jìn)行預(yù)加載或預(yù)渲染。
這套智能預(yù)加載機(jī)制來自于Texture,非常的實(shí)用,我將它修改為Objective-C實(shí)現(xiàn),并做了簡(jiǎn)化處理。你甚至可以參照這個(gè)區(qū)間計(jì)算思路制作一個(gè)滾動(dòng)列表曝光打點(diǎn)類,來計(jì)算那些更符合用戶視距的曝光區(qū)間,而不是僅僅簡(jiǎn)單依賴cell/View的生命周期,說到這不得不提一嘴:我曾經(jīng)見過一個(gè)埋點(diǎn)系統(tǒng)迭代了兩年多依然沒啥卵用。
5.圖片一致性校驗(yàn)
異步Cell渲染圖片回調(diào)設(shè)置圖片需要進(jìn)行渲染的模型與當(dāng)前模型是否一致的校驗(yàn),復(fù)用可能會(huì)導(dǎo)致一個(gè)Cell先后被設(shè)置兩個(gè)Model,這樣兩個(gè)Model在異步渲染結(jié)束后都可能通知Cell刷新數(shù)據(jù),所以需要一致性校驗(yàn)。同步不存在這個(gè)問題,后來的內(nèi)容總是會(huì)覆蓋掉先來的圖片。像這樣:
NSObject *model = self.model;
__weak DemoAsyncFeedCell *weakSelf = self;
[self.model demoAsyncFeedItemLoadRenderImage:^(BOOL isCanceled, UIImage * _Nullable resultImage) {
if (!isCanceled && resultImage && model == weakSelf.model) {
weakSelf.contentView.layer.contents = (__bridge id)resultImage.CGImage;
}
}];
6.渲染緩存
與SDWebImage下載的原生Image不同,渲染后的圖片存儲(chǔ)在額外的一個(gè)渲染緩存中,Model弱引用持有,緩存內(nèi)部使用LRU管理;不能使用Model強(qiáng)引用,因?yàn)橛行〧eed流是常駐的,我們不希望內(nèi)存浪費(fèi)在不是主要消費(fèi)場(chǎng)景的常駐頁(yè)面中。LNAsyncCache是統(tǒng)一的存放的地方,你可以在渲染成功后把圖片存在這里,使用弱指針指向它,如果被刪除了,就重新渲染、存儲(chǔ)。
7.減少渲染次數(shù)
SD下載圖片時(shí)附帶AvoidDecode參數(shù),因?yàn)楹铣蛇^程會(huì)將Image渲染到一塊內(nèi)存中,這個(gè)過程本身就包含了解碼,且也是在子線程中進(jìn)行;使用這個(gè)參數(shù)可以減少圖片剛下載好時(shí)的那次渲染。像這樣:
[[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:weakSelf.image]
options:SDWebImageAvoidDecodeImage
progress:nil
completed:nil];
總結(jié)
LNAsyncKit優(yōu)化的內(nèi)容就如上所述:
- 從主線程的角度來看:除了刷新CollectionView和計(jì)算預(yù)加載區(qū)域外基本上沒有耗時(shí)工作,布局計(jì)算和模型解析轉(zhuǎn)移到了子線程統(tǒng)一進(jìn)行,Element創(chuàng)建銷毀操作主線程基本上沒有感知。
- 從CPU的角度來看:圓角、邊框、漸變等工作都在圖層合成的時(shí)候異步消化了,返回的圖片大小和Layer控件大小也是一致的,圖層的復(fù)雜層級(jí)也被子線程異步消化。
- 從子線程角度看:子線程有很多。
寫異步Feed流比普通Feed流難度要稍微大一些,平均開發(fā)的時(shí)間成本也會(huì)有所上升;從效率上來講,每個(gè)需求的開發(fā)效率確實(shí)降低了,但這將會(huì)省去在未來單獨(dú)成立一個(gè)性能優(yōu)化小組進(jìn)行優(yōu)化的效率要高得多。平臺(tái)類型的開發(fā)人員往往沒有業(yè)務(wù)開發(fā)對(duì)業(yè)務(wù)更熟悉,因此需要頻繁交流確認(rèn)優(yōu)化點(diǎn)、改動(dòng)范圍、影響等等。而且,有時(shí)遇到優(yōu)化點(diǎn)時(shí)業(yè)務(wù)受限,可能不敢大刀闊斧地糾正,導(dǎo)致優(yōu)化后的結(jié)果和優(yōu)化前對(duì)比并不明顯。LNAsyncKit讓業(yè)務(wù)線從一開始做需求時(shí)就考慮到優(yōu)化內(nèi)容,從而省去了專項(xiàng)優(yōu)化的時(shí)間。當(dāng)然,如果App整體不考慮性能問題,選擇正常的開發(fā)方式就好。
雜談
iPhone手機(jī)硬件越來越強(qiáng),常規(guī)業(yè)務(wù)不進(jìn)行優(yōu)化一般也能達(dá)到流暢性標(biāo)準(zhǔn),端內(nèi)的卡頓只要不是特別嚴(yán)重產(chǎn)品經(jīng)理通常也都能接受;我在需求中使用了類似的方式進(jìn)行性能優(yōu)化,開發(fā)時(shí)間確實(shí)很緊。當(dāng)然,如果你的公司只考慮需求產(chǎn)出,他們通常不會(huì)給你這些時(shí)間,你可以在自己的編碼追求和實(shí)際情況之間決定是否要額外做這些事。
LNAsyncKit可以直接使用,也可以將它當(dāng)做你更深層次了解性能優(yōu)化、Texture的墊腳石;總之,它能起到任何幫助,我都將十分榮幸。