App界面优化的小Tips。


本来这篇文章只是阅读YYAsyncLayer后的体会,后来一再扩充,变成了App界面优化的小Tips。。。

一、列表视图

定高Cell

1,复用

1
2
3
4
5
6
7
8
static NSString * const kWBStatusCellIdentifier = @"kWBStatusCellIdentifier";

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
WBStatusCell *cell = [tableView dequeueReusableCellWithIdentifier:kWBStatusCellIdentifier forIndexPath:indexPath];
[cell setLayout:_layouts[indexPath.row]];
return cell;
}

标准化的写法是这样的。dequeueReusableCellWithIdentifier:forIndexPath:方法比dequeueReusableCellWithIdentifier:方法多了“自动创建”。cell的重用ID最好使用静态常量,尽管直接使用@"kWBStatusCellIdentifier",编译器层面也会做优化,帮我们生成静态常量,但是意义不一样。

cell的种类不要太多,尽量通过hidden subview来区别。

2,设置行高

1
_tableView.rowHeight = 60;

由于是固定行高,直接在配置TableView的懒加载中直接设置rowHeight属性即可。如果使用代理,会使TableView多次询问,增加不必要的调用。

1
2
3
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 60;
}

除此之外,能使用整数的地方就不要使用小数,60要比60.2更好一点,CPU不喜欢小数。不仅仅指rowHeight这个属性,其他视图的width、height等属性,在满足UE要求的情况下,都可以尽可能地采用整数。

3,模型生成

采用MVVM,需要字典转模型;去Model化,需要格式化字典;无论怎样,都要把数据转换成视图喜欢的样子,这些操作就可以放到子线程中进行。尤其是遇到DateFormatter、stringWithFormat这些稍微耗性能的方法,该缓存就缓存,该替换就替换。

1
2
3
4
5
6
7
8
9
10
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 字典转模型
WBTimelineItem *item = [WBTimelineItem modelWithJSON:data];
_statuses = item.statuses;

// 主线程刷新
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
});

4,subviews操作

1,子视图越少,出现问题的可能性也越小。所以在构思的时候,一个视图能解决的问题,没必要多写几个视图。比如需要在同一行展示内容a和内容b,可以考虑只使用一个Label。
2,视图的动态创建和销毁也是很heavily的,考虑设置hidden属性来控制显示和隐藏。
3,尽量选用轻量级的控件,不需要用户响应的可以换成CALayer,比如CALayer替代UIImageView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (CALayer *)photoImageViewLayer {
if (!_photoImageViewLayer) {
_photoImageViewLayer = [[CALayer alloc] init];
_photoImageViewLayer.contentsGravity = @"resizeAspectFill";
_photoImageViewLayer.masksToBounds = YES;
}
return _photoImageViewLayer;
}

// 设置
[self.photoImageViewLayer yy_setImageWithURL:url
placeholder:UIImage.randomColorImage
options:options
completion:nil];

但是CALayer不能设置约束,这就需要自己计算frame了。

5,触发离屏渲染

离屛渲染需要开辟一块新的缓冲区,在渲染的过程中还会有多次的切换上下文,这些很消耗性能。

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。

下面是常见的可能触发离屏渲染的操作:

1, layer.cornerRadius + layer.masksToBounds一起设置
2, 设置图层阴影layer.shadow
3, 设置蒙层layer.mask
4, layer.allowsGroupOpacity 设置为YES同时layer.opacity小于1.0
5, layer.shouldRasterize设置为YES。
开启 Rasterization 后,GPU 只合成一次内容,然后复用合成的结果;合成的内容超过100ms没有使用会从缓存里移除,所以对于不连续使用的内容进行光栅化是既没有意义又浪费资源的,在更新内容时还会产生更多的离屏渲染。对于内容不发生变化的视图,进行光栅化会使原本拖后腿的离屏渲染就成为了助力;如果视图内容是动态变化的,使用这个方案有可能让性能变得更糟。不要过度使用,系统限制缓存的大小为 2.5x screen size,过度使用的话也会造成离屏渲染。(一般用在单独的视图上,而不是cell的layer)
6, 使用UIBlurEffect

即刻App的优化思路是这样:
1,对于图片的圆角,统一采用“precomposite”的策略,也就是不经由容器来做剪切,而是预先使用CoreGraphics为图片裁剪圆角
2,对于视频的圆角,由于实时剪切非常消耗性能,我们会创建四个白色弧形的layer盖住四个角,从视觉上制造圆角的效果
3,对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
4,对于所有的阴影,使用shadowPath来规避离屏渲染
5,对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存
6,对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果

6,圆角

“cornerRadius和maskToBounds一起设置会触发离屏渲染”,这句话需要讨论一下了:

1,UIView这般设置圆角不会产生离屏渲染。
2,iOS9.0之后UIImageView中的png图片这般设置圆角不会触发离屏渲染,但是如果UIImageView的backgroundColor不是clearColor或者不是nil,则会触发离屏渲染。

上面两条是我搜了很多资料发现的,亲自验证了一下,在iOS13上第一条是真的。第二条试着不设置backgroundColor果然也没有出现离屏渲染。但是,是不是从iOS9开始的,我没有真机也没法做判断,而且也没有找到官方的说明。

下面是之前的UIImageView设置圆角的处理方式
直接使用layer.mask来达到圆角效果同样会触发离屏渲染,对于网络图片的圆角处理通常是先下载后圆角化。
下载逻辑(以我喜欢的YYWebImage为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSString *radiusKey = [url.absoluteString stringByAppendingString:@"radiusCache"];
UIImage *radiusImage = [YYImageCache.sharedCache getImageForKey:radiusKey];
if (radiusImage) {
self.photoImageView.image = radiusImage;
} else {
[YYWebImageManager.sharedManager requestImageWithURL:url options:YYWebImageOptionShowNetworkActivity progress:nil transform:nil completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError * error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!error && image) {
[YYImageCache.sharedCache setImage:radiusImage imageData:nil forKey:radiusKey withType:YYImageCacheTypeAll];
self.photoImageView.image = image.radiusImage;
}
});
}];
}

图片下载完毕后做圆角处理并保存,但是有几个问题需要思考:
1,这张图片的圆角半径在业务上是固定的吗,如果不固定(比如需要10px、20px两种)那就需要存储多份了;
2,这张图片的展示效果是否只有圆角一种,是否同时存在有方图和圆角图两种形式?需要保留原图吗;

这个是使用Core Graphics创建圆角图片的不会触发离屏渲染的示例,可以子线程绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (UIImage *)radiusImage {
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGFloat radius = 100.f; // 这个度量是图片的,而不是imageView的
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
CGContextAddPath(context, path.CGPath);
CGContextClip(context);
[self drawInRect:rect];
CGContextDrawPath(context, kCGPathFillStroke);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

7,阴影

假如需要这么设置阴影效果:

1
2
3
4
button.layer.shadowColor = UIColor.blackColor.CGColor;
button.layer.shadowOpacity = 0.5;
button.layer.shadowRadius = 10;
button.layer.shadowOffset = CGSizeMake(10, 10);

只需要再加上一句话就能避免离屏渲染了:

1
button.layer.shadowPath = [UIBezierPath bezierPathWithRect:button.bounds].CGPath;

但是有个小问题,如果这个具有阴影效果的视图需要做frame动画,那它的shadow效果是不会改变的,这还得再想办法解决:显式指定shadowPath的动画效果

不定高cell

不定高的cell需要计算cell的高度。有两种方式:一是自己在后台根据模型数据配置子视图的布局,手动计算,并把高度存储到对应的模型中;二是借助自动布局,保证子视图“撑满”cell,自动更新cell的高度。

如果对性能要求不高,或者开发周期短,可以直接使用Auto Layout,再配合FDTemplateLayoutCell的高度缓存机制,可以说已经很方便了。不然只能自己计算并做缓存处理。

二、异步渲染

TableView和CollectionView的优化也可以借助异步渲染,YYAsyncLayer描述了异步渲染的核心原理。

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface YYAsyncLayer : CALayer
// Default is YES.
@property BOOL displaysAsynchronously;
@end

@protocol YYAsyncLayerDelegate <NSObject>
@required
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end

// 将要绘制、正在绘制、绘制完毕 对应的block
@interface YYAsyncLayerDisplayTask : NSObject
@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);
@end

使用

视图在初始化过程之前调用UIView类方法layerClass,并且使用返回的类来创建layer对象。通过创建UIView的子类,重写layerClass类函数可以改变创建图层时的默认的CALayer类。在自定义视图对象中,返回一个YYAsyncLayerDisplayTask,这个task承担着绘制的任务。

异步绘制文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@interface YALabel : UIView <YYAsyncLayerDelegate>
@property (nonatomic, copy) NSAttributedString *attributedString;
@end



@implementation YALabel

- (void)setAttributedString:(NSAttributedString *)attributedString {
_attributedString = attributedString;
// YYTransaction let you perform a selector once before current runloop sleep.
[[YYTransaction transactionWithTarget:self selector:@selector(setTextNeedsDisplay)] commit];
}

- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(setTextNeedsDisplay)] commit];
}

- (void)setTextNeedsDisplay {
[self.layer setNeedsDisplay];
}

+ (Class)layerClass {
return YYAsyncLayer.class;
}

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
if (isCancelled() || self.attributedString.string.length <= 0) return;
// 修复绘制文字会颠倒
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
}];
NSAttributedString *str = self.attributedString;
[str enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, str.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(UIFont *font, NSRange range, BOOL *stop) {
// 根据字体设置pointSize
CGContextSetTextPosition(context, 0, font.pointSize);
}];
// 绘制
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)str);
CTLineDraw(line, context);
CFRelease(line);
};
return task;
}
@end

使用的时候很简单了,设置一个AttributedString即可:

1
2
3
4
5
6
YALabel *label = [YALabel new];
NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"😀😁🤣😂Label的异步绘制" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:15]}];
label.attributedString = str;
label.backgroundColor = UIColor.orangeColor;
label.frame = CGRectMake(100, 100, 200, 50);
[self.view addSubview:label];

这里的newAsyncDisplayTask比较简单,只绘制了单行文本,如果真的要应用到项目中,可以利用CoreText更加完善些,加上自动换行,自适应size,图文混排等功能。

有前辈写了一个IM的Demo,里面有用到异步绘制:https://github.com/Yuzeyang/GCAsyncDisplayDemo ,读一读还是很棒的。

源码阅读

源码不是很多,就几个文件,但是跟作者的其他框架一样,代码特别优秀。

YYSentinel

这是一个线程安全的计数器,内部维护了一个实例变量。如何做到线程安全? 主要是依赖于原子函数OSAtomicIncrement32()

如果我们想要初始化一个共享的数据结构,然后自动增加某个变量值来标识初始化操作完成,则我们必须使用OSAtomicIncrement32Barrier来确保数据结构的存储操作在变量自动增加前完成。

使用这个类的目的: 标记某个操作是否完成,也即判断或者标记异步渲染操作的完成情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <libkern/OSAtomic.h>
// 线程安全的计数器
@interface YYSentinel : NSObject
@property (readonly) int32_t value;
- (int32_t)increase;
@end


@implementation YYSentinel {
int32_t _value;
}

- (int32_t)value {
return _value;
}

- (int32_t)increase {
return OSAtomicIncrement32(&_value);
}
@end

YYTransaction

只有两个方法接口。
方法一: 包装targetselector并生成一个YYTransaction对象。
方法二: commit这个对象。
包装成YYTransaction对象的方法不用多说,其内部使用实例变量引用着传递的参数。
最关键的是commit方法。It will perform the selector on the target once before main runloop’s current loop sleep. If the same transaction (same target and same selector) has already commit to runloop in this loop, this method do nothing. 也就是说在Runloop每次休眠之前只调用一次targetselector方法。

commit方法内部使用transactionSet集合添加了这个YYTransaction对象。作者给Runloop添加了一个observer,在 kCFRunLoopBeforeWaitingkCFRunLoopExit这两种状态下会调用指定的YYRunLoopObserverCallBack()函数。这个函数的作用很简单:逐个遍历transactionSet集合中的所有元素,使其target调用对应的selector

YYTransaction重写了- (NSUInteger)hash方法和- (BOOL)isEqual:(id)object方法来确定“对象相等”。这保证了Set集合中的元素唯一性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@interface YYTransaction : NSObject
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;
- (void)commit;
@end

@interface YYTransaction()
@property (nonatomic, strong) id target;
@property (nonatomic, assign) SEL selector;
@end

static NSMutableSet *transactionSet = nil;

// 逐个遍历transactionSet中的所有元素,使其target调用对应的selector
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
transactionSet = [NSMutableSet new];
[currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}

// 当Runloop处于kCFRunLoopBeforeWaiting或者kCFRunLoopExit的状态的时候调用YYRunLoopObserverCallBack()函数
static void YYTransactionSetup() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;

observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}


@implementation YYTransaction

+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{
if (!target || !selector) return nil;
YYTransaction *t = [YYTransaction new];
t.target = target;
t.selector = selector;
return t;
}

// 把生成的YYTransaction对象放入transactionSet中
- (void)commit {
if (!_target || !_selector) return;
YYTransactionSetup();
[transactionSet addObject:self];
}

// isEqual: 是通过hash方法来判等的
- (NSUInteger)hash {
long v1 = (long)((void *)_selector);
long v2 = (long)_target;
return v1 ^ v2;
}

// 只要_target和_selector都一致,就说明'YYTransaction'是同一个
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if (![object isMemberOfClass:self.class]) return NO;
YYTransaction *other = object;
return other.selector == _selector && other.target == _target;
}

@end

YYAsyncLayer

最后是YYAsyncLayer的实现,最核心的逻辑就在这里了。我一年之前就曾经读过这块源码,当时有好多疑惑,现在再来看,有的疑惑已经解开了,有的还没有,在这记录一下。

1,子线程的处理

异步,自然要放到子线程。主队列对应的是主线程,4个不同优先级的全局队列并不对应4个子线程。毅然放在全局队列不太可取,可能会出现App在同一时刻存在几十个线程同时运行、创建、销毁的情况。“当大量线程同时创建运行销毁时,这些操作仍然会挤占掉主线程的 CPU 资源。”。使用串行队列又无法充分利用多核CPU的资源。作者在YYAsyncLayer中的思路是:创建和 CPU 数量相同的串行queue(最多16个),放到一个数组中,每次从数组中随机获取其中的一个queue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// Global display queue, used for content rendering.
static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {
#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, ^{
// 获取运行该进程的系统的处于激活状态的处理器数量, iPhone 6s 是2个
queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_attr_t attr =
// QOS_CLASS_USER_INITIATED 用户发起并等待的优先级
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
// 创建串行队列
queues[i] = dispatch_queue_create("com.ibireme.yykit.render", attr);
}
});
// 大量使用 OSAtomicIncrement32,由于该函数对某个值进行自增计算,而且是线程安全的,所以被用来作为绘制时的标记位(哨兵).异步绘制的关键在于每次调用 setNeedDisplay 时都会将哨兵变量自增,在 display 方法中,根据这个哨兵变量来确定是否继续绘制还是停止绘制.一致则继续绘制, 不一致则停止绘制
int32_t cur = OSAtomicIncrement32(&counter); // counter自增一, 并取出该值,
if (cur < 0) cur = -cur; // 啥时候会出现?? counter取INT_MAX的时候出现
return queues[(cur) % queueCount]; // 取余
#undef MAX_QUEUE_COUNT
}

单纯就子线程绘制这个问题而言,思考一下,既然会出现“同一时刻存在几十个线程同时运行、创建、销毁”的情况,那如果直接操作线程呢,直接创建三五个NSThread对象,让它们处理App中的所有异步渲染操作。如果这样的话,就需要对这三五个NSThread对象进行线程保活,保证它们永远存在。再者只能通过performSelector:onThread:进行方法调用(或者再做一个block方式的封装)。最后也是最重要的是,没办法利用设备多核的优势。单核中的多线程实际上是每个时间片上只有一个线程在运行,而多核实际上是真的多线程,每一个时间片每个核都有一个线程在执行。dispatch_async是多核级别编程框架调度函数,这才真正决定了为啥要使用GCD。

2,异步绘制开关控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 重写修改CALayer或其子类属性的默认值,key为属性名称,如果没有该属性则返回nil。
+ (id)defaultValueForKey:(NSString *)key {
if ([key isEqualToString:@"displaysAsynchronously"]) {
return @(YES); // 仅仅针对displaysAsynchronously这个属性修改
} else {
return [super defaultValueForKey:key];
}
}

- (instancetype)init {
self = [super init];
// 设置屏幕缩放比例
static CGFloat scale; // global
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
scale = [UIScreen mainScreen].scale;
});
self.contentsScale = scale;
// 计数器, 控制多线程访问
_sentinel = [YYSentinel new];
// 默认是YES
_displaysAsynchronously = YES;
return self;
}

通过displaysAsynchronously来控制是否开启异步渲染。
但是这里有个问题想不通,YYAsyncLayer的init方法中已经初始化_displaysAsynchronously的值为YES了,但是作者又重写defaultValueForKey方法并在其中再次设置displaysAsynchronously属性为YES。这两个方式的效果一样呀,会不会多此一举了呢?

3,绘制时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)dealloc {
// layer销毁的时候, 计数器加1, 表示原先的绘制需要取消
[_sentinel increase];
}

- (void)setNeedsDisplay {
// 被标记需要重新绘制, 则取消上次的异步调用
[self _cancelAsyncDisplay];
[super setNeedsDisplay];
}

// Reload the content of this layer.
// Subclasses can override this method and use it to set the layer’s contents property directly. You might do this if your custom layer subclass handles layer updates differently.
- (void)display {
// 这个赋值是因为这样吗??
// Assigning a value to this property causes the layer to use your image rather than create a separate backing store.
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}

- (void)_cancelAsyncDisplay {
// increase增加1, 与目前的不一致了, 表示目前的渲染已取消
[_sentinel increase];
}

在实际的绘制逻辑中,是通过判断计数器_sentinel的值来确定是否取消绘制:

1
2
3
4
5
6
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)(void) = ^BOOL() {
// 值相等, 说明正在绘制没有取消, 否则说明已经取消啦
return value != sentinel.value;
};

反过来,只需要把计数器的值自增([_sentinel increase])就可以表示目前的绘制操作取消了。在Layer销毁的时候(dealloc)和被标记需要重新绘制的时候(setNeedsDisplay)应该取消原先的绘制操作。

按照苹果的说明,CALayer的子类可以重写display方法并在其中直接设置contents。YYAsyncLayer便在其中做了异步绘制操作。这里的super.contents = super.contents;一句话确实当然没看懂,后来发现也有小伙伴跟我一样没弄明白,然后查阅资料发现了苹果的解释:Assigning a value to this property causes the layer to use your image rather than create a separate backing store.

4,绘制操作

最后便是绘制的操作了,写了很详细的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 核心方法
- (void)_displayAsync:(BOOL)async {
// 强引用代理, 避免释放
__strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate;
// 获取一个Display Task
YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
if (!task.display) { // 没有需要绘制的内容, 设置contents为空
if (task.willDisplay) task.willDisplay(self);
self.contents = nil;
if (task.didDisplay) task.didDisplay(self, YES);
return;
}

if (async) { // 异步绘制
if (task.willDisplay) task.willDisplay(self);
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)(void) = ^BOOL() {
// 值相等, 说明正在绘制没有取消, 否则说明已经取消啦
return value != sentinel.value;
};
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
// 获取背景色
CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
if (size.width < 1 || size.height < 1) { // 宽或者高小于1的情况
// 获取图片再释放掉, 为啥要多此一举?
// 因为赋值给contents的就是一个CGImageRef, 如果直接self.contents = nil;会造成这个image在当前线程(主线程)释放。作者手动把这个image取出来的目的就是把'release'操作放在YYAsyncLayerGetReleaseQueue()这个队列中
CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
self.contents = nil;
if (image) {
dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
CFRelease(image);
});
}
if (task.didDisplay) task.didDisplay(self, YES);
CGColorRelease(backgroundColor);
return;
}

// 从这里开始进入子线程
dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
// 进入子线程后, 第1次取消绘制判断
if (isCancelled()) {
CGColorRelease(backgroundColor);
return;
}
// 开启图形上下文
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (opaque) {
// UIGraphicsPushContext:压栈当前的绘制对象, 生成新的绘制图层。UIKit的绘制必须在当前的上下文中绘制,而UIGraphicsPushContext可以将当前的参数context转化为可以UIKit绘制的上下文,进行绘制图片。
// CGContextSaveGState:压栈当前的绘制状态
// 这里是为了不"污染"原先的context内容, 保存状态后绘制新的内容
CGContextSaveGState(context);
{
// 没有背景色或者 透明度小于1, 使用白色填充
if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
// 这里使用backgroundColor填充
if (backgroundColor) {
CGContextSetFillColorWithColor(context, backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
}
CGContextRestoreGState(context);
CGColorRelease(backgroundColor);
}
task.display(context, size, isCancelled);
// display完毕后, 第2次取消绘制判断
if (isCancelled()) {
// 如果确定已经取消绘制了, 关闭图形上下文
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
// 从当前上下文中获取生成的新内容
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 从上下文中获取图片后, 第3次取消绘制判断
if (isCancelled()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// 转到主线程对contents赋值,第4次取消绘制判断
if (isCancelled()) {
if (task.didDisplay) task.didDisplay(self, NO);
} else {
// 在这里对contents赋值
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
});
});
} else { // 非异步绘制
// 不需要进行线程安全判断了
[_sentinel increase];
// 下面的代码都是和异步绘制相同的套路
if (task.willDisplay) task.willDisplay(self);
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (self.opaque) {
CGSize size = self.bounds.size;
size.width *= self.contentsScale;
size.height *= self.contentsScale;
CGContextSaveGState(context); {
if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
if (self.backgroundColor) {
CGContextSetFillColorWithColor(context, self.backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
} CGContextRestoreGState(context);
}
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);
}
}

看起来貌似很多,实际的绘制原理三行代码可以说清楚:

1
2
3
4
5
6
// 1,从当前图形上下文中获取Image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 2,关闭图形上下文
UIGraphicsEndImageContext();
// 3,将image赋值给layer的contents属性
self.contents = (__bridge id)(image.CGImage);

在这之外更多的是 是否取消绘制的判断、没有背景色或者透明度小于1的处理、size极小值的处理等等。

当然还有那个巧妙的子线程异步释放对象的逻辑,记得也见过好多次了:

1
2
3
4
5
6
7
CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
self.contents = nil;
if (image) {
dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
CFRelease(image);
});
}

平时的业务开发中我们也可以这么用,比如:

1
2
3
4
5
6
// 含有大量元素的数组
NSMutableArray *array = self.itemArray;
self.itemArray = nil;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
array = nil; // 子线程释放
});

三、小结

《iOS 保持界面流畅的技巧》这篇文章想必很多人都有人阅读过,也都能从中受益。
这里回顾一下ibireme大神的微博Demo性能优化技巧:
1,预排版
这里是指不定高cell的预排版,本文也已经提过,frame布局,可以子线程手动计算并把layout数据缓存在模型中,autolayout布局,可以借助FDTemplateLayoutCell做高度缓存。
2,预渲染
作者举了一个圆角图片预先裁剪并做缓存处理来避免离屏渲染的例子,也正如上文所说,layer.cornerRadius + layer.masksToBounds一起设置在新版本系统上已经不会触发离屏渲染(大多数人说是iOS9,我还没有确认),但是这个思路可以举一反三,很多可以提前的工作没必要等到cell要展示了才开始做。
3,异步绘制+全局并发控制

  • 借助YYDispatchQueuePool把App内所有异步操作按优先级不同放入了全局的 serial queue 中执行,尽量避免了过多线程导致的性能问题。
  • 借助YYAsyncLayer和CoreText实现富文本的异步绘制(当然也可以直接使用功能更强大的YYText,作者把路都给铺好了😂)。

4,异步图片加载
大多时候,只是显示简单的单张图片,可以直接使用UIView.layer.contents,而不需要使用UIImageView。当然,SDWebImage和YYWebImage把很多工作都做好了。
5,不需要触摸事件的UIView换成CALayer
可以这么做,但是CALayer没法设置约束了,如果使用自动布局就很蛋疼了。
6,多个视觉元素合成一张图
也是一种思路,甚至也一定条件下可以把cell的全部内容合成一张图片,但是可能性比较小,需要根据业务来走。
以微博为例,“来自iPhone”、会员图标、话题、转发微博原作者昵称、微博内容的链接这些元素都需要响应点击事件,根本不可能用一种图片代替。
百度的feed流,很多新闻的样式是相同的,而且不像微博需要处理很多子元素的点击事件,很多情况下只需要处理cell的点击事件就可以了,这才有合成一张图片的可能性。

过早的优化是万恶之源,所有的tips都只是建议,需要具体问题具体分析。很多前辈给我们提供了诸多便利的工具,对于我们,平时踏踏实实写代码,少写bug,不写crash,这才是最重要的。

参考资料:
《iOS 保持界面流畅的技巧》
《提升UITableView性能-复杂页面的优化》
《iOS 阴影,圆角,避免离屏渲染》
《UITableView 性能优化》