IM UI性能优化之异步绘制

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


重构完Socket之后,最近我们也开始针对IM的UI做了优化,这次的优化我们主要是参考了YYKit对于性能方面的优化,前期我的另一个小伙伴西兰花也对AsyncDisplayKit做了调研,不过这个库理解起来确实要费一番功夫,后面西兰花也答应会对AsyncDisplayKit做一个讲解,大家可以到他的博客了解一下,由于YYkit的核心思路基本上都是学习AsyncDisplayKit的,,相信YYkit这个库大家都已经很熟悉了,不过可能还没有看过这个库,那下面我做一个简单的介绍


YYKit的作者是郭曜源,YYKit实际上是将它那些单独的iOS组件整合在了一起,类似于集合一样组成功能比较全面的组件,你也可以根据自己业务的需要单独使用其中的某些部分

0x00 前期准备

我们首先阅读了郭曜源在对界面流畅性方面的见解,里面提到了异步绘制
,但是文字表述毕竟是抽象的,然后我们简单看了下他的YYText和YYAsyncLayer组件,看完之后实际上对如何使用他的YYAsyncLayer这个组件来实现异步绘制还是有点模糊的,后来我们直接看他的微博demo,我们逐渐理清了他是如何实现异步绘制以及几个性能优化方面的点

因为YYLabel Async Display里面加了是否异步绘制开关,所以我们直接用这个例子作为对比,首先我们来看下异步绘制的效果,开始的时候我们关闭异步绘制的开关,你会发现FPS瞬间掉到6了,屏幕滚动开始非常卡,但是打开开关之后,滚动时虽然FPS还是会掉到30-40,但是滑动的流畅度比之前要好很多,感觉这异步绘制的效果杠杠的好啊,那我们一定要看看他是怎么做的了


0x01 分析

其实整一个性能优化关键的点及流程有三个:

1.数据源的异步处理

当我们获取到数据源的时候,我们需要对数据源进行计算处理,计算出UI绘制所需要的属性比如宽高、颜色等等,而且这些计算要异步去做,否则会卡住主线程,等这些数据源计算完成之后,再去处理绘制,但是如果数据源过大,计算的耗时还是在的,所以会有较长时间的等待时间,此时我们需要考虑加上等待的友好处理

2.采用更轻量级的绘制

在绘制时,对于不需要响应触摸事件的控件,我们应该尽量避免创建UIView对象,取而代之的是使用更为轻量的CALayer,并且对于一个layer包含多个subLayer的情况时,我们可以通过图层预合成的方法,将多个subLayer合成渲染成一张图片,通过上述的处理,不仅能减少CPU在创建UIKit对象的消耗,还能减少GPU在合成和渲染上的消耗,内存的占用也会少很多

3.异步绘制

我们将使用YYAsyncLayer
组件实现异步绘制

0x02 YYAsyncLayer介绍

前面两个优化点,平时在做的时候可能也都会去做,但是异步绘制这个该怎么去实现呢?我们直接来看下YYAsyncLayer的代码,YYAsyncLayer组件里面一共包含了三个类:YYAsyncLayerYYSentinelYYTransaction

YYAsyncLayer类是我们主要用的类,它是CALayer的子类,是用来异步渲染layer内容

YYSentinel类是用来给线程安全计数的,用于在多线程处理的场景

YYTransaction类是利用runloop在休眠前的空闲时间来触发你预设的方法

因为我们没有用到YYTransaction类,所以我们直接YYAsyncLayerYYSentinel合成一个类,并做了混淆,这样可以少引用一个库

我们首先来看YYAsyncLayer的头文件

YYAsyncLayer类只有一个displaysAsynchronously属性,就是设置渲染是否是异步执行的

@property BOOL displaysAsynchronously;

然后还有个代理方法,这个代理方法的触发时机是在layer的内容需要更新的时候,此时你有个新的绘制任务,然后返回的是个YYAsyncLayerDisplayTask对象

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;

YYAsyncLayerDisplayTask类只有三个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);

看到实现文件里面,触发这个代理的方法是- setNeedsDisplay方法,就是当layer需要更新内容的时候,它会向代理发起一个异步绘制的请求,将内容的渲染放到后台队列去做,所以我们在使用YYAsyncLayer类时,我们需要重写+ layerClass方法,返回YYAsyncLayer类,否则会直接调用CALayer的方法,不会触发代理

- (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方法里面主要分成三部分:

如果没有设置display回调,layer的内容会被清空

if (!task.display) {

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

根据之前displaysAsynchronously属性设置判断,如果是同步绘制的话,实际上的操作就是在调用完displayblock之后,将sublayer合成一张图作为layer的内容

[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);

而异步渲染的处理和同步渲染大同小异,第一,多了一个BOOL (^isCancelled)()block,这个block的好处是,在displayblock调用绘制前,可以通过判断isCancelled布尔值的值来停止绘制,减少性能上的消耗,以及避免出现线程阻塞的情况,比如TableView快速滑动的时候,就可以通过这样的判断,来避免不必要的绘制,提升滑动的流畅性,第二,将上面同步的绘制处理放到了异步去做,绘制方式是一样的

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);
       }
   });
});

这个异步的队列也是自己创建的,在预设了一个队列最大值之后,通过获取运行该进程的系统处于激活状态的处理器数量来创建队列,使得绘制的效率达到最高

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 补充

文本的实现上,我们更加推荐使用CoreText,CoreText对象占用的内存少,而且适用于文本排版复杂的情况,虽然在实现上较为复杂,但是所带来的好处远远要多

在渲染图片时,我们应该在后台把图片绘制到CGBitmapContext
中,然后从Bitmap直接创建图片,因为如果使用原来ImageView读取Image的方式是,在创建Image或者CGImageSource对象时,图片数据并不会立即解码,而是等到设置到ImageView或者layer.contents,layer被提交到GPU之前,才解码,并且这些操作都是在主线程进行,是相当耗性能的,所以我们应该用推荐的方式去绘制,而且AFNetworking在对图片处理的时候也是这么做的

0x04 简单实现demo

对于上述优化点,我实现了一个简单的CoreText demo,可以看一下这个demo做进一步了解~

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

推荐阅读更多精彩内容