簡介
LNAsyncKit是一個異步渲染工具掏膏,它提供了便捷的方法幫助你將多個元素(Element)異步渲染到一張圖片上饰豺,讓這個過程代替UIKit的視圖構(gòu)建過程蜗侈,進而優(yōu)化App性能鲤拿;Prender提供預(yù)加載策略幫助你在Feed流中彌補異步渲染帶來的延時蚂四;除構(gòu)建視圖外沥阳,Transaction提供更優(yōu)雅的方式讓主線程與子線程交互跨琳,并能根據(jù)機器狀態(tài)控制并發(fā)數(shù)和主線程回調(diào)時機。
LNAsyncKit借(ji)鑒(cheng)了很多YYKit和Texture桐罕,如果對它們不是很了解可以戳這個比較詳細的文章脉让,這篇文章的作者是YY大神:iOS保持頁面流暢的技巧。流暢性優(yōu)化的思想基本上都如這篇文章所述功炮。
它可以提供哪些幫助
- 還沒有找到方案優(yōu)化圓角溅潜、邊框、漸變的優(yōu)化方案薪伏,LNAsyncKit可以異步解決這些滚澜。
- Feed流需要預(yù)加載策略,LNAsyncKit提供預(yù)加載區(qū)域計算方案(這個方案也用來預(yù)合成)嫁怀。
- 提供一種與UIKit十分接近的方式構(gòu)建需要預(yù)合成的圖層设捐,讓你的復(fù)雜圖層構(gòu)建都放在子線程進行,且不會創(chuàng)建那么多UIView塘淑。
- Demo展示了使用:AFNetworking/SDWebImage/IGListKit/YYModel/MJRefresh + LNAsyncKit搭建feed流的方法萝招。除去LNAsyncKit,前面5個構(gòu)成的這套體系已經(jīng)比較完整存捺,Demo中也提供了沒有使用LNAsyncKit構(gòu)建的Feed槐沼。因此,需要快速學(xué)習(xí)如何搭建一套Feed流的初學(xué)者可以參考這套三方捌治。
Github鏈接
你可以直接下載這個鏈接并運行上面的Demo參考代碼實現(xiàn)自己的異步列表岗钩,也可以直接使用Cocoapods??
pod 'LNAsyncKit'
流暢性優(yōu)化
網(wǎng)絡(luò)上已經(jīng)有了很多流暢性優(yōu)化的文章,再逐一復(fù)述這些優(yōu)化點意義不大肖油;這個文章是為了表達如何在Feed流中實現(xiàn)那些優(yōu)化思想兼吓,并把這個過程簡化;所以构韵,不再贅述這些優(yōu)化點為什么好周蹭、好多少趋艘,只談怎么實現(xiàn)它們;如果對這些優(yōu)化點有疑問可以參考上面鏈接的文章凶朗,以下這些觀點成立:
- 圖層少的列表比圖層多的列表好瓷胧。
- 沒有圓角、邊框棚愤、漸變等復(fù)雜圖層的比有的好搓萧。
- 圖片尺寸和控件尺寸一樣大的好。
- 模型解析放在子線程比放在主線程好宛畦。
- 布局計算放在子線程比放在主線程好瘸洛。
- 有預(yù)加載比沒有預(yù)加載好(見仁見智,也有喜歡無預(yù)加載列表的)次和。
- Layer比View好(無手勢時)反肋。
- 不透明圖層比透明圖層好。
在業(yè)務(wù)復(fù)雜度不變的前提下讓這些優(yōu)化工作變簡單踏施、自由就是LNAsyncKit的目標石蔗。
優(yōu)化一個Cell
我們將一個Cell視為Feed流的最小優(yōu)化單元,以一個Bilibili推薦Feed流中一個常規(guī)的Cell為例:
這樣一個小Cell中包含了:封面圖畅形、人數(shù)圖標养距、人數(shù)Label、主播昵稱日熬、直播間名棍厌、[直播]、直播內(nèi)容分類竖席、負反饋按鈕8個元素耘纱;除了這些元素外,還包括封面圖底部一個黑色漸變的圖層怕敬、[直播]的圓角揣炕、邊框和整個Cell的圓角(好像還有些陰影)帘皿;這個小Cell已經(jīng)包含比較多的小元素了东跪,我們在Demo中嘗試復(fù)原一下并查看視圖層級大致如下:
具體構(gòu)建代碼這里不贅述了,使用LNAsyncKit可以簡化這個Cell為如下這個樣子:
(右下角反饋Bug需要響應(yīng)事件鹰溜,通常這種控件會保持獨立)
以“直播”標簽為例虽填,視圖構(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)為一張與目標控件大小一致的圖片斋日。
原理
與UIKit類似,LNAsyncKit也使用視圖樹構(gòu)建最終視圖。區(qū)別是:
A. Element繼承自NSObject墓陈,這些Element可以在子線程創(chuàng)建恶守、渲染第献、銷毀⊥酶郏可以將Element理解為“一個需要繪制圖層”的描述物庸毫,它并不是一個實體,它與UIView/CALayer的區(qū)別就好像:UIView是你要買的一件物品衫樊;Element則是下單信息飒赃,里面包含這件物品的各種描述信息,多大科侈、什么顏色等载佳。
B. 所有的Element都是臨時的,這些信息在構(gòu)建出結(jié)果后就會被銷毀臀栈,你可以在進入子線程之后創(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ù)雜的一棵樹藕漱,但對主線程來說,這并不會造成問題崭闲!不在主線程出現(xiàn)Element也是LNAsyncKit推薦的使用方法(拿到resultImage后肋联,把Element.resultImage置為空),當然刁俭,出現(xiàn)了一般也無所謂橄仍,NSObject的消耗相對于UIView來講是很小的。
C.Element是逐層渲染的:實際上是后續(xù)遍歷牍戚,把A的子Element先渲染出來侮繁,然后渲染A,再把A當做一個子節(jié)點渲染父節(jié)點如孝,LNAsyncRendererTraversalStack就是遍歷時使用的棧宪哩、LNAsyncRenderer.traversal函數(shù)是遍歷方法。遍歷中自帶了環(huán)檢測第晰,不會渲染重復(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: 對應(yīng)于UIKit的UIView,是其他Element的基類茁瘦,包含了背景色品抽、frame、和常用的邊界甜熔、圓角等屬性圆恤。
- LNASyncImageElement: 對應(yīng)于UIImageView,渲染一張圖片腔稀、提供三種填充方式盆昙。
- LNAsyncTextElement: 對應(yīng)于UILabel羽历,渲染一段文字,提供常規(guī)文字屬性淡喜、支持折行窄陡。
- LNAsyncLinerGradientElement: 對應(yīng)于CAGradientLayer,渲染一段漸變色拆火。
自定義Element:
除原生Element外跳夭,我們也推薦封裝自己的Element,例如:一個AvatarElement们镜,可以將用戶頭像币叹、VIP標識、頭像邊框等修飾物渲染在一起模狭,重寫- (void)renderSelfWithContext:(CGContextRef)context颈抚,在這個方法中分別繪制這三個元素。
自定義Element的意義在于嚼鹉,所有自定義過的Element都是可復(fù)用贩汉、可組合的,這樣方便保持整個App風(fēng)格統(tǒng)一锚赤,也會適當減少開發(fā)成本匹舞。
Feed流
上面我們已經(jīng)介紹過單個Cell、單張圖片如何異步渲染以優(yōu)化性能线脚,但性能問題往往不是單張圖片所能引發(fā)赐稽,LNAsyncKit更傾向于性能敏感的場景:Feed流;渲染Feed流相比渲染單個視圖需要考慮的事情要多一些:Cell復(fù)用浑侥、渲染好的圖片緩存姊舵、多張圖片下載和結(jié)果合并等問題;除此之外寓落,也考慮使用預(yù)加載括丁、預(yù)渲染功能來優(yōu)化用戶體驗。
使用到的三方庫:
- AFNetworking 網(wǎng)絡(luò)
- IGListKit Feed流框架伶选,可以拆分各個模塊業(yè)務(wù)
- SDWebImage 圖片下載
- YYModel 字典轉(zhuǎn)模型
- MJRefresh 上拉/下拉刷新組件
- 一位大佬寫的免費API 史飞,雖然我不認識這位大佬,但這些接口確實非常方便考蕾,在這里面朝空氣感謝一下~
這些都是非常成熟的三方框架祸憋,直接拿來用會減少不少開發(fā)時間会宪;這里主要是介紹如何將LNAsyncKit融進這個體系中去肖卧。Demo中已經(jīng)提供了默認的Feed流和異步的Feed流代碼,如果遇到了一些奇怪的Bug可以參考Demo中的實現(xiàn)掸鹅,目前這兩個Demo都可以正常運行塞帐。
- 默認Demo:我們用此Demo展示一個常規(guī)Feed流實現(xiàn)過程拦赠,沒有使用任何修飾手法或設(shè)計思想,可以理解為實現(xiàn)一個Feed流所需要做的最少工作葵姥。
- 異步Demo:我們用此Demo將使用LNAsyncKit實現(xiàn)Feed流時與通常情況下的實現(xiàn)的進行對比荷鼠,了解從普通Feed轉(zhuǎn)異步Feed的修改點和差異之處。
默認Feed流實現(xiàn):
- ViewDidLoad中使用AFNetworking請求一頁數(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中進行
A.因為異步Feed不僅僅需要下載圖片鳞陨,也需要將多個原始圖片進行預(yù)合成,所以這個過程在Model中進行可以保證不會因Cell復(fù)用問題導(dǎo)致同一時間合成多次瞻惋,如果你在Cell中異步進行圖層合成厦滤,那可能每次賦值Model都會合成一次,但在Model中合成后可以一直存放在Model中(Model只持有弱引用歼狼,存在全局的NSCache中)掏导。
B.考慮預(yù)加載,我們認為圖層的預(yù)加載和預(yù)合成是兩種優(yōu)先級的事情羽峰,通常距離屏幕焦點區(qū)域較遠的區(qū)域只需要進行圖片預(yù)下載碘菜,而距離較近的地方則需要預(yù)合成,不論是哪種方式限寞,Cell通常只會在展示在屏幕上的時間點附近才能拿到忍啸,如果圖片下載放在Cell中進行,是很難實現(xiàn)“預(yù)”的履植。
MVC中Model的職責之一是提供View展示需要的數(shù)據(jù)计雌,所以在Model中下載圖片并非錯誤或不恰當?shù)淖龇ā?/p>
2.模型解析和布局計算視為網(wǎng)絡(luò)請求的一部分
通常,在使用AFNetworking進行網(wǎng)絡(luò)請求時玫霎,我們通常在成功回調(diào)中進行模型解析和列表刷新凿滤,列表刷新時走CollectionView的dataSource協(xié)議計算布局。
異步列表不推薦這樣做:模型解析的過程沒有想象中的那樣簡單庶近,通常進行模型解析時需要逐層遍歷Dictionary翁脆,然后創(chuàng)建大量Model和子Model,雖然單個NSObject開銷不大鼻种,但列表視圖的模型總是堆積起來的反番,創(chuàng)建如此多的對象也是個不小的開銷。
計算布局的耗時是公認的,所以一般表視圖優(yōu)化都推薦緩存行高罢缸,但即便緩存行高篙贸,第一次在主線程中的計算也是有一定耗時的。
我們推薦在AFNetworking回調(diào)中異步進行模型解析和布局計算枫疆,將這兩個操作視為網(wǎng)絡(luò)請求的一部分爵川,這并不會對網(wǎng)絡(luò)請求的整體響應(yīng)時間有較大的影響,因為網(wǎng)絡(luò)回調(diào)時間單位通常要比屏幕刷新時間單位高出一個數(shù)量級息楔。況且寝贡,預(yù)加載技術(shù)完全可以彌補這段小延時。
在請求回調(diào)中賦值給Model的LayoutObj就是對這個過程的封裝值依,像這樣:
- (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中布局
這聽起來有點詭異兔甘,在Model中下載圖片也就算了,為什么視圖操作也在Model中進行鳞滨?
我們已經(jīng)解釋了Element的職責洞焙,它只是負責描述的類。使用element構(gòu)建視圖的過程就是:Model想好要怎么構(gòu)建(Element)拯啦,把想法交付LNAsyncRenderer澡匪,renderer交付我們image,Model把image反回給View顯示出來褒链。就像我們在開始的時候講述的那樣唁情。
4.預(yù)加載
預(yù)加載主要內(nèi)容包括兩個方面:預(yù)加載下一頁信息和預(yù)加載圖片。這里提到的預(yù)加載主要是指預(yù)加載圖片:
根據(jù)上面我們提到了圖片加載都是在Model中進行的甫匹,所以甸鸟,每個Model都需要一個必要的參數(shù)來標記自身所持有的資源已經(jīng)到了那種緊急的程度了,如果距離當前用戶焦點還很遠兵迅,說明自己的資源目前不是很緊急抢韭,可以先靜觀其變;如果距離用戶焦點有點近了恍箭,說明自己可能需要開始考慮先把圖片下載下來刻恭;如果距離用戶焦點已經(jīng)相當近了,就要立刻開始準備把已有資源預(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提供了一套資源緊急程度標記策略鳍贾,將距離當前屏幕中心較遠的資源標記為不緊急,較近的資源標記為緊急交洗,Model受緊急程度標記影響自主進行預(yù)加載或預(yù)渲染骑科。
這套智能預(yù)加載機制來自于Texture,非常的實用构拳,我將它修改為Objective-C實現(xiàn)咆爽,并做了簡化處理梁棠。你甚至可以參照這個區(qū)間計算思路制作一個滾動列表曝光打點類,來計算那些更符合用戶視距的曝光區(qū)間伍掀,而不是僅僅簡單依賴cell/View的生命周期掰茶,說到這不得不提一嘴:我曾經(jīng)見過一個埋點系統(tǒng)迭代了兩年多依然沒啥卵用暇藏。
5.圖片一致性校驗
異步Cell渲染圖片回調(diào)設(shè)置圖片需要進行渲染的模型與當前模型是否一致的校驗蜜笤,復(fù)用可能會導(dǎo)致一個Cell先后被設(shè)置兩個Model,這樣兩個Model在異步渲染結(jié)束后都可能通知Cell刷新數(shù)據(jù)盐碱,所以需要一致性校驗把兔。同步不存在這個問題,后來的內(nèi)容總是會覆蓋掉先來的圖片瓮顽。像這樣:
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不同县好,渲染后的圖片存儲在額外的一個渲染緩存中,Model弱引用持有暖混,緩存內(nèi)部使用LRU管理缕贡;不能使用Model強引用,因為有些Feed流是常駐的拣播,我們不希望內(nèi)存浪費在不是主要消費場景的常駐頁面中晾咪。LNAsyncCache是統(tǒng)一的存放的地方,你可以在渲染成功后把圖片存在這里贮配,使用弱指針指向它谍倦,如果被刪除了,就重新渲染泪勒、存儲昼蛀。
7.減少渲染次數(shù)
SD下載圖片時附帶AvoidDecode參數(shù),因為合成過程會將Image渲染到一塊內(nèi)存中圆存,這個過程本身就包含了解碼叼旋,且也是在子線程中進行;使用這個參數(shù)可以減少圖片剛下載好時的那次渲染沦辙。像這樣:
[[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:weakSelf.image]
options:SDWebImageAvoidDecodeImage
progress:nil
completed:nil];
總結(jié)
LNAsyncKit優(yōu)化的內(nèi)容就如上所述:
- 從主線程的角度來看:除了刷新CollectionView和計算預(yù)加載區(qū)域外基本上沒有耗時工作送淆,布局計算和模型解析轉(zhuǎn)移到了子線程統(tǒng)一進行,Element創(chuàng)建銷毀操作主線程基本上沒有感知怕轿。
- 從CPU的角度來看:圓角偷崩、邊框、漸變等工作都在圖層合成的時候異步消化了撞羽,返回的圖片大小和Layer控件大小也是一致的阐斜,圖層的復(fù)雜層級也被子線程異步消化。
- 從子線程角度看:子線程有很多诀紊。
寫異步Feed流比普通Feed流難度要稍微大一些谒出,平均開發(fā)的時間成本也會有所上升;從效率上來講,每個需求的開發(fā)效率確實降低了笤喳,但這將會省去在未來單獨成立一個性能優(yōu)化小組進行優(yōu)化的效率要高得多为居。平臺類型的開發(fā)人員往往沒有業(yè)務(wù)開發(fā)對業(yè)務(wù)更熟悉,因此需要頻繁交流確認優(yōu)化點杀狡、改動范圍蒙畴、影響等等。而且呜象,有時遇到優(yōu)化點時業(yè)務(wù)受限膳凝,可能不敢大刀闊斧地糾正,導(dǎo)致優(yōu)化后的結(jié)果和優(yōu)化前對比并不明顯恭陡。LNAsyncKit讓業(yè)務(wù)線從一開始做需求時就考慮到優(yōu)化內(nèi)容蹬音,從而省去了專項優(yōu)化的時間。當然休玩,如果App整體不考慮性能問題著淆,選擇正常的開發(fā)方式就好。
雜談
iPhone手機硬件越來越強拴疤,常規(guī)業(yè)務(wù)不進行優(yōu)化一般也能達到流暢性標準永部,端內(nèi)的卡頓只要不是特別嚴重產(chǎn)品經(jīng)理通常也都能接受;我在需求中使用了類似的方式進行性能優(yōu)化遥赚,開發(fā)時間確實很緊扬舒。當然,如果你的公司只考慮需求產(chǎn)出凫佛,他們通常不會給你這些時間讲坎,你可以在自己的編碼追求和實際情況之間決定是否要額外做這些事。
LNAsyncKit可以直接使用愧薛,也可以將它當做你更深層次了解性能優(yōu)化晨炕、Texture的墊腳石;總之毫炉,它能起到任何幫助瓮栗,我都將十分榮幸。