大数据

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 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做进一步了解~

发表评论