IM UI性能優(yōu)化之異步繪制

原文地址:http://zeeyang.com/2016/07/05/IM-UI-optimize/


重構(gòu)完Socket之后,最近我們也開始針對(duì)IM的UI做了優(yōu)化,這次的優(yōu)化我們主要是參考了YYKit對(duì)于性能方面的優(yōu)化,前期我的另一個(gè)小伙伴西蘭花也對(duì)AsyncDisplayKit做了調(diào)研,不過這個(gè)庫理解起來確實(shí)要費(fèi)一番功夫,后面西蘭花也答應(yīng)會(huì)對(duì)AsyncDisplayKit做一個(gè)講解,大家可以到他的博客了解一下,由于YYkit的核心思路基本上都是學(xué)習(xí)AsyncDisplayKit的,,相信YYkit這個(gè)庫大家都已經(jīng)很熟悉了,不過可能還沒有看過這個(gè)庫,那下面我做一個(gè)簡單的介紹


YYKit的作者是郭曜源,YYKit實(shí)際上是將它那些單獨(dú)的iOS組件整合在了一起,類似于集合一樣組成功能比較全面的組件,你也可以根據(jù)自己業(yè)務(wù)的需要單獨(dú)使用其中的某些部分

0x00 前期準(zhǔn)備

我們首先閱讀了郭曜源在對(duì)界面流暢性方面的見解,里面提到了異步繪制
,但是文字表述畢竟是抽象的,然后我們簡單看了下他的YYText和YYAsyncLayer組件,看完之后實(shí)際上對(duì)如何使用他的YYAsyncLayer這個(gè)組件來實(shí)現(xiàn)異步繪制還是有點(diǎn)模糊的,后來我們直接看他的微博demo,我們逐漸理清了他是如何實(shí)現(xiàn)異步繪制以及幾個(gè)性能優(yōu)化方面的點(diǎn)

因?yàn)閅YLabel Async Display里面加了是否異步繪制開關(guān),所以我們直接用這個(gè)例子作為對(duì)比,首先我們來看下異步繪制的效果,開始的時(shí)候我們關(guān)閉異步繪制的開關(guān),你會(huì)發(fā)現(xiàn)FPS瞬間掉到6了,屏幕滾動(dòng)開始非常卡,但是打開開關(guān)之后,滾動(dòng)時(shí)雖然FPS還是會(huì)掉到30-40,但是滑動(dòng)的流暢度比之前要好很多,感覺這異步繪制的效果杠杠的好啊,那我們一定要看看他是怎么做的了


0x01 分析

其實(shí)整一個(gè)性能優(yōu)化關(guān)鍵的點(diǎn)及流程有三個(gè):

1.數(shù)據(jù)源的異步處理

當(dāng)我們獲取到數(shù)據(jù)源的時(shí)候,我們需要對(duì)數(shù)據(jù)源進(jìn)行計(jì)算處理,計(jì)算出UI繪制所需要的屬性比如寬高、顏色等等,而且這些計(jì)算要異步去做,否則會(huì)卡住主線程,等這些數(shù)據(jù)源計(jì)算完成之后,再去處理繪制,但是如果數(shù)據(jù)源過大,計(jì)算的耗時(shí)還是在的,所以會(huì)有較長時(shí)間的等待時(shí)間,此時(shí)我們需要考慮加上等待的友好處理

2.采用更輕量級(jí)的繪制

在繪制時(shí),對(duì)于不需要響應(yīng)觸摸事件的控件,我們應(yīng)該盡量避免創(chuàng)建UIView對(duì)象,取而代之的是使用更為輕量的CALayer,并且對(duì)于一個(gè)layer包含多個(gè)subLayer的情況時(shí),我們可以通過圖層預(yù)合成的方法,將多個(gè)subLayer合成渲染成一張圖片,通過上述的處理,不僅能減少CPU在創(chuàng)建UIKit對(duì)象的消耗,還能減少GPU在合成和渲染上的消耗,內(nèi)存的占用也會(huì)少很多

3.異步繪制

我們將使用YYAsyncLayer
組件實(shí)現(xiàn)異步繪制

0x02 YYAsyncLayer介紹

前面兩個(gè)優(yōu)化點(diǎn),平時(shí)在做的時(shí)候可能也都會(huì)去做,但是異步繪制這個(gè)該怎么去實(shí)現(xiàn)呢?我們直接來看下YYAsyncLayer的代碼,YYAsyncLayer組件里面一共包含了三個(gè)類:YYAsyncLayer、YYSentinel、YYTransaction

YYAsyncLayer類是我們主要用的類,它是CALayer的子類,是用來異步渲染layer內(nèi)容

YYSentinel類是用來給線程安全計(jì)數(shù)的,用于在多線程處理的場景

YYTransaction類是利用runloop在休眠前的空閑時(shí)間來觸發(fā)你預(yù)設(shè)的方法

因?yàn)槲覀儧]有用到YYTransaction類,所以我們直接YYAsyncLayer、YYSentinel合成一個(gè)類,并做了混淆,這樣可以少引用一個(gè)庫

我們首先來看YYAsyncLayer的頭文件

YYAsyncLayer類只有一個(gè)displaysAsynchronously屬性,就是設(shè)置渲染是否是異步執(zhí)行的

@property BOOL displaysAsynchronously;

然后還有個(gè)代理方法,這個(gè)代理方法的觸發(fā)時(shí)機(jī)是在layer的內(nèi)容需要更新的時(shí)候,此時(shí)你有個(gè)新的繪制任務(wù),然后返回的是個(gè)YYAsyncLayerDisplayTask對(duì)象

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;

YYAsyncLayerDisplayTask類只有三個(gè)block,即將繪制、繪制中、繪制完成

@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);
@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);

看到實(shí)現(xiàn)文件里面,觸發(fā)這個(gè)代理的方法是- setNeedsDisplay方法,就是當(dāng)layer需要更新內(nèi)容的時(shí)候,它會(huì)向代理發(fā)起一個(gè)異步繪制的請(qǐng)求,將內(nèi)容的渲染放到后臺(tái)隊(duì)列去做,所以我們?cè)谑褂?code>YYAsyncLayer類時(shí),我們需要重寫+ layerClass方法,返回YYAsyncLayer類,否則會(huì)直接調(diào)用CALayer的方法,不會(huì)觸發(fā)代理

- (void)setNeedsDisplay {

   [self _cancelAsyncDisplay];
   [super setNeedsDisplay];
}
?
- (void)display {
   super.contents = super.contents;
   [self _displayAsync:_displaysAsynchronously];
}
?
#pragma mark - Private
?
- (void)_displayAsync:(BOOL)async {
   __strong id<YYAsyncLayerDelegate> delegate = self.delegate;
   YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
 // ...
}

- _displayAsync方法里面主要分成三部分:

如果沒有設(shè)置display回調(diào),layer的內(nèi)容會(huì)被清空

if (!task.display) {

   if (task.willDisplay) task.willDisplay(self);
   self.contents = nil;
   if (task.didDisplay) task.didDisplay(self, YES);
   return;
}

根據(jù)之前displaysAsynchronously屬性設(shè)置判斷,如果是同步繪制的話,實(shí)際上的操作就是在調(diào)用完displayblock之后,將sublayer合成一張圖作為layer的內(nèi)容

[self increase];

if (task.willDisplay) task.willDisplay(self);
UIGraphicsBeginImageContextWithOptions(self.bounds.size,self.opaque,self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
task.display(context, self.bounds.size, ^{return NO;});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);

而異步渲染的處理和同步渲染大同小異,第一,多了一個(gè)BOOL (^isCancelled)()block,這個(gè)block的好處是,在displayblock調(diào)用繪制前,可以通過判斷isCancelled布爾值的值來停止繪制,減少性能上的消耗,以及避免出現(xiàn)線程阻塞的情況,比如TableView快速滑動(dòng)的時(shí)候,就可以通過這樣的判斷,來避免不必要的繪制,提升滑動(dòng)的流暢性,第二,將上面同步的繪制處理放到了異步去做,繪制方式是一樣的

if (task.willDisplay) task.willDisplay(self);

int32_t value = self.value;
BOOL (^isCancelled)() = ^BOOL() {
   return value != self.value;
};
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
if (size.width < 1 || size.height < 1) {
   CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
   self.contents = nil;
   if (image) {
       dispatch_async(FIMAsyncLayerGetReleaseQueue(), ^{
           CFRelease(image);
       });
   }
   if (task.didDisplay) task.didDisplay(self, YES);
   return;
}
?
dispatch_async(FIMAsyncLayerGetDisplayQueue(), ^{
   if (isCancelled()) return;
   UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
   CGContextRef context = UIGraphicsGetCurrentContext();
   task.display(context, size, isCancelled);
   if (isCancelled()) {
       UIGraphicsEndImageContext();
       dispatch_async(dispatch_get_main_queue(), ^{
           if (task.didDisplay) task.didDisplay(self, NO);
       });
       return;
   }
   UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext();
   if (isCancelled()) {
       dispatch_async(dispatch_get_main_queue(), ^{
           if (task.didDisplay) task.didDisplay(self, NO);
       });
       return;
   }
   dispatch_async(dispatch_get_main_queue(), ^{
       if (isCancelled()) {
           if (task.didDisplay) task.didDisplay(self, NO);
       } else {
           self.contents = (__bridge id)(image.CGImage);
           if (task.didDisplay) task.didDisplay(self, YES);
       }
   });
});

這個(gè)異步的隊(duì)列也是自己創(chuàng)建的,在預(yù)設(shè)了一個(gè)隊(duì)列最大值之后,通過獲取運(yùn)行該進(jìn)程的系統(tǒng)處于激活狀態(tài)的處理器數(shù)量來創(chuàng)建隊(duì)列,使得繪制的效率達(dá)到最高

static dispatch_queue_t FIMAsyncLayerGetDisplayQueue() {

#define MAX_QUEUE_COUNT 16
   static int queueCount;
   static dispatch_queue_t queues[MAX_QUEUE_COUNT];
   static dispatch_once_t onceToken;
   static int32_t counter = 0;
   dispatch_once(&onceToken, ^{
       queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
       queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
       if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
           for (NSUInteger i = 0; i < queueCount; i++) {
               dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
               queues[i] = dispatch_queue_create("com.ibireme.FIMkit.render", attr);
           }
       } else {
           for (NSUInteger i = 0; i < queueCount; i++) {
               queues[i] = dispatch_queue_create("com.ibireme.FIMkit.render", DISPATCH_QUEUE_SERIAL);
               dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
           }
       }
   });
   int32_t cur = OSAtomicIncrement32(&counter);
   if (cur < 0) cur = -cur;
   return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
}

0x03 補(bǔ)充

文本的實(shí)現(xiàn)上,我們更加推薦使用CoreText,CoreText對(duì)象占用的內(nèi)存少,而且適用于文本排版復(fù)雜的情況,雖然在實(shí)現(xiàn)上較為復(fù)雜,但是所帶來的好處遠(yuǎn)遠(yuǎn)要多

在渲染圖片時(shí),我們應(yīng)該在后臺(tái)把圖片繪制到CGBitmapContext
中,然后從Bitmap直接創(chuàng)建圖片,因?yàn)槿绻褂迷瓉鞩mageView讀取Image的方式是,在創(chuàng)建Image或者CGImageSource對(duì)象時(shí),圖片數(shù)據(jù)并不會(huì)立即解碼,而是等到設(shè)置到ImageView或者layer.contents,layer被提交到GPU之前,才解碼,并且這些操作都是在主線程進(jìn)行,是相當(dāng)耗性能的,所以我們應(yīng)該用推薦的方式去繪制,而且AFNetworking在對(duì)圖片處理的時(shí)候也是這么做的

0x04 簡單實(shí)現(xiàn)demo

對(duì)于上述優(yōu)化點(diǎn),我實(shí)現(xiàn)了一個(gè)簡單的CoreText demo,可以看一下這個(gè)demo做進(jìn)一步了解~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容