界面优化解析
前言
我们经常在面试中,会被问及关于界面优化相关的问题,比如为什么界面会出现卡顿?如何监控卡顿?接着如何解决卡顿?那么本篇文章将重点分析一下卡顿的原理
和解决的措施
。
一、卡顿的原理
我们先看看界面显示出来的一个完整流程,如下图👇
CPU
负责事务的计算,GPU
负责渲染,将渲染的结果交给FrameBuffer帧缓冲区
,帧缓冲区将处理的数据传递给UI层(Video 或 Conrtroller)
,UI层
交给Monitor
显示出来。
问题1:GPU效率
那么问题来了,帧缓冲区需要处理数据,那么就会有耗时,耗时长就会影响效率,如何解决这个效率问题呢?人们就提出了一个双帧缓冲区
的概念,如下图👇
双帧缓冲区
,我们可以理解分为一个前帧区
和一个后帧区
,CPU首先在前帧区处理数据,处理完成后紧接着去后帧区处理数据,然后GPU先去前帧区取CPU第1次处理的数据,然后去后帧区取CPU第2次数据,CPU和GPU二者交替进行写和读
,这样就腾出了一个缓冲的时间。
问题2:变化因子影响
但是又有问题,缓冲区内数据处理会受到外界环境因素的影响,例如当前手机在充电时,cpu发烫严重,就会导致数据处理变慢,这些外界因素可称之为变化因子
,变化因子
的影响就会导致一个现象-->丢帧
。
因为cpu这边的变化因子
太多太多,即受外部环境的因素影响的可变性太多,所以一般不会将CPU作为优化的对象,转而我们一般将GPU这边规定一个固定的标准,是什么标准呢,例如:每秒渲染60帧图片
进行显示,那么问题来了,GPU这边如何知道这个渲染的时间点呢?就是谁来通知GPU渲染
去显示?答案就是显示器
了,这时就又引入一个概念垂直同步信号VSync,显示器发送一个垂直同步信号VSync
给monitor,monitor拿到该信号后,就去拿帧缓冲区的数据,开始渲染屏幕,显示画面,要是没有接收到该信号,就不显示。
问题3:丢帧
那么还是有问题,要是monitor接收到垂直同步信号
后,去帧缓冲区
拿数据,数据没有
的话,如下图👇
借用上图解释一下掉帧卡顿
的情况(假设显示器在接收VSync信号时,只显示1帧
画面)👇
- 正常
不掉帧
的情况下,在两个VSync信号之间
,CPU能处理完
数据,GPU也能处理完
数据,这样在下一个VSync
信号来的时候,显示器(Display)
能完整的拿到帧数据进行显示,如上图第1个VSync和第2个VSync之间,第1帧的画面显示正常。 - 当
CPU
和GPU
处理数据的时长超过
了两个VSync信号之间的时长,如上图第2个VSync和第3个VSync之间,GPU
处理时长(红色条)已经超过了第3个VSync的时间点,那么显示器(Display)
在第三个VSync的时间点去取第2帧数据的时候,发现GPU那边没有数据
,因为此时GPU还在处理数据,这时显示器(Display)
仍然在显示第1帧
的画面,然后接着继续往下走。 - 在第3个VSync和第4个VSync之间,
CPU
和GPU
在第四个VSync时间点之前,又都处理完
了数据,和第1点的情况一样,那么显示器(Display)
能拿到第3帧
的数据显示了,那么此时显示器(Display)只显示了第1帧和第3帧
,没有显示第2帧
,于是第2帧就丢了,这就是丢帧
。
VSync间隔时长
再解释VSync之间的时间间隔,大约是16.67ms
,怎么来的,之前我们说过,最完美的显示是每秒60帧图片
,那么每一帧
的耗时
就是1000/60 ≈ 16.67ms
。
二、卡顿的监测
2.1 CADisplayLink
查看文档,搜索CADisplayLink,找到相关的VSync,说明CADisplayLink就是和显示器刷新频率一样的。我们再来看看YYKit中的YYFPSLabel,它就是封装了CADisplayLink,显示FPS。(每秒传输帧数(Frames Per Second))👇
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
UIFont *_font;
UIFont *_subFont;
NSTimeInterval _llll;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (frame.size.width == 0 && frame.size.height == 0) {
frame.size = kSize;
}
self = [super initWithFrame:frame];
self.layer.cornerRadius = 5;
self.clipsToBounds = YES;
self.textAlignment = NSTextAlignmentCenter;
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
_font = [UIFont fontWithName:@"Menlo" size:14];
if (_font) {
_subFont = [UIFont fontWithName:@"Menlo" size:4];
} else {
_font = [UIFont fontWithName:@"Courier" size:14];
_subFont = [UIFont fontWithName:@"Courier" size:4];
}
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
return self;
}
- (void)dealloc {
[_link invalidate];
}
- (CGSize)sizeThatFits:(CGSize)size {
return kSize;
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;
CGFloat progress = fps / 60.0;
UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
[text setColor:color range:NSMakeRange(0, text.length - 3)];
[text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
text.font = _font;
[text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
self.attributedText = text;
}
@end
2.2 RunLoop监控
事务的卡顿,事务是交给RunLoop处理的,于是我们可以通过监控RunLoop,监控RunLoop的运行状态kCFRunLoopBeforeSources & kCFRunLoopAfterWaiting,判断事务是否处理完成。源码如下👇
@interface LGBlockMonitor (){
CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LGBlockMonitor
+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)start{
[self registerObserver];
[self startMonitor];
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}
2.3 其它卡顿监测方案
微信matrix、滴滴DoraemonKit,感兴趣的童鞋,可以去官方文档上下载源码,自行查看具体的实现流程。这里大致说一下其中的实现原理
-
微信的方案
:和上面讲的RunLoop监控的原理
是一样
的,不多做说明。 -
滴滴的方案
:是一种轮询
的机制,每隔一段时间主动去查询主线程的状态,如果在你的设置的阈值时间内,没有返回信息,那么就能判定卡顿
,如下图👇
worker工作线程
每隔一小段时间(delta)
ping
一下主线程
,如果主线程此时有空,必然能接收到这个通知,并pong
一下(发送另一个NSNotification
),如果worker线程超过delta时间
没有收到pong的回复
,那么可以推测UI线程必然在处理其他任务
了,此时我们暂停UI线程
,并打印出当前UI线程的函数调用栈
。
三、卡顿的解决措施
我们在面试过程中,经常会被问此类的问题,我们一般的回答就是UITableView的复用啊,CellRowHeight的计算啊,切圆角的处理优化啊,这些都是表明的应用,很难回答到点上,今天我们将在以下几点分析下卡顿的解决方案:
- 预排版
- 预解码 & 预渲染
- 按需加载
- 异步渲染
3.1 预排版
以网络请求为案例,一般的网络请求,我们获取到response后,解析完数据后直接回调给主线程刷新TableView,那么主线程上就得计算cell子控件的布局,cell的高低等,这样比较损耗GPU的性能
。
如何避免这些损耗呢?
基于这些损耗问题,我们可以单独在一个预排版的子线程
去做一些事情:
- frame的计算
- 控件层级的部署
- 渲染所需数据的处理
- Model模型的数据解析等
这些都可以专门自定义一个layout类
去处理,然后主线程上直接使用这些已经处理好layout的数据,这样可以减少
很多不必要的数据处理
(麻烦),这就是预排版的作用
。
案例演示
例如我们要实现这样一个UITableView,首先我们先分析cell的布局,cell包含的子控件有:
- 头像
- 昵称Label
- 内容Label
- 收起/展开 Button
- 图片(注意:也可以是多张图)
- 分割view(底部灰色部分)
然后,根据对这个cell子控件的分析,可以定义一个layout类👇
接着看看cell类的定义👇
然后扩展类里的子控件的定义👇
继续看看configureLayout方法具体做了什么👇
很简单,一些frame的赋值,图片的赋值,圆角的处理,文案的赋值。根本没有任何数据的计算,全部是赋值。那所有的数据的预处理
交给谁去做呢?--> 当然是layout类
👇
- (instancetype)initWithModel:(LGTimeLineModel *)timeLineModel{
if (!timeLineModel) return nil;
self = [super init];
if (self) {
_timeLineModel = timeLineModel;
[self layout];
}
return self;
}
- (void)setTimeLineModel:(LGTimeLineModel *)timeLineModel{
_timeLineModel = timeLineModel;
[self layout];
}
- (void)layout{
CGFloat sWidth = [UIScreen mainScreen].bounds.size.width;
self.iconRect = CGRectMake(10, 10, 45, 45);
CGFloat nameWidth = [self calcWidthWithTitle:_timeLineModel.name font:titleFont];
CGFloat nameHeight = [self calcLabelHeight:_timeLineModel.name fontSize:titleFont width:nameWidth];
self.nameRect = CGRectMake(CGRectGetMaxX(self.iconRect) + nameLeftSpaceToHeadIcon, 17, nameWidth, nameHeight);
CGFloat msgWidth = sWidth - 10 - 16;
CGFloat msgHeight = 0;
//文本信息高度计算
NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineSpacing:5];
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:msgFont],
NSForegroundColorAttributeName: [UIColor colorWithRed:26/255.0 green:26/255.0 blue:26/255.0 alpha:1]
,NSParagraphStyleAttributeName: paragraphStyle
,NSKernAttributeName:@0
};
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:_timeLineModel.msgContent attributes:attributes];
msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
if (attrStr.length > msgExpandLimitHeight) {
if (_timeLineModel.isExpand) {
self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
} else {
attrStr = [[NSMutableAttributedString alloc] initWithString:[_timeLineModel.msgContent substringToIndex:msgExpandLimitHeight] attributes:attributes];
msgHeight = [self caculateAttributeLabelHeightWithString:attrStr width:msgWidth];
self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
}
} else {
self.contentRect = CGRectMake(10, CGRectGetMaxY(self.iconRect) + 10, msgWidth, msgHeight);
}
if (attrStr.length < msgExpandLimitHeight) {
self.expandHidden = YES;
self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) - 20, 30, 20);
} else {
self.expandHidden = NO;
self.expandRect = CGRectMake(10, CGRectGetMaxY(self.contentRect) + 10, 30, 20);
}
CGFloat timeWidth = [self calcWidthWithTitle:_timeLineModel.time font:timeAndLocationFont];
CGFloat timeHeight = [self calcLabelHeight:_timeLineModel.time fontSize:timeAndLocationFont width:timeWidth];
self.imageRects = [NSMutableArray array];
if (_timeLineModel.contentImages.count == 0) {
// self.timeRect = CGRectMake(10, CGRectGetMaxY(self.expandRect) + 10, timeWidth, timeHeight);
} else {
if (_timeLineModel.contentImages.count == 1) {
CGRect imageRect = CGRectMake(11, CGRectGetMaxY(self.expandRect) + 10, 250, 150);
[self.imageRects addObject:@(imageRect)];
} else if (_timeLineModel.contentImages.count == 2 || _timeLineModel.contentImages.count == 3) {
for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
CGRect imageRect = CGRectMake(11 + i * (10 + 90), CGRectGetMaxY(self.expandRect) + 10, 90, 90);
[self.imageRects addObject:@(imageRect)];
}
} else if (_timeLineModel.contentImages.count == 4) {
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
CGRect imageRect = CGRectMake(11 + j * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + i * (10 + 90), 90, 90);
[self.imageRects addObject:@(imageRect)];
}
}
} else if (_timeLineModel.contentImages.count == 5 || _timeLineModel.contentImages.count == 6 || _timeLineModel.contentImages.count == 7 || _timeLineModel.contentImages.count == 8 || _timeLineModel.contentImages.count == 9) {
for (int i = 0; i < _timeLineModel.contentImages.count; i++) {
CGRect imageRect = CGRectMake(11 + (i % 3) * (10 + 90), CGRectGetMaxY(self.expandRect) + 10 + (i / 3) * (10 + 90), 90, 90);
[self.imageRects addObject:@(imageRect)];
}
}
}
if (self.imageRects.count > 0) {
CGRect lastRect = [self.imageRects[self.imageRects.count - 1] CGRectValue];
self.seperatorViewRect = CGRectMake(0, CGRectGetMaxY(lastRect) + 10, sWidth, 15);
}
self.height = CGRectGetMaxY(self.seperatorViewRect);
}
#pragma mark -- Caculate Method
- (CGFloat)calcWidthWithTitle:(NSString *)title font:(CGFloat)font {
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
CGRect rect = [title boundingRectWithSize:CGSizeMake(MAXFLOAT,MAXFLOAT) options:options attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:font]} context:nil];
CGFloat realWidth = ceilf(rect.size.width);
return realWidth;
}
- (CGFloat)calcLabelHeight:(NSString *)str fontSize:(CGFloat)fontSize width:(CGFloat)width {
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
CGRect rect = [str boundingRectWithSize:CGSizeMake(width,MAXFLOAT) options:options attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize]} context:nil];
CGFloat realHeight = ceilf(rect.size.height);
return realHeight;
}
- (int)caculateAttributeLabelHeightWithString:(NSAttributedString *)string width:(int)width {
int total_height = 0;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string); //string 为要计算高度的NSAttributedString
CGRect drawingRect = CGRectMake(0, 0, width, 100000); //这里的高要设置足够大
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, drawingRect);
CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0), path, NULL);
CGPathRelease(path);
CFRelease(framesetter);
NSArray *linesArray = (NSArray *) CTFrameGetLines(textFrame);
CGPoint origins[[linesArray count]];
CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
int line_y = (int) origins[[linesArray count] -1].y; //最后一行line的原点y坐标
CGFloat ascent;
CGFloat descent;
CGFloat leading;
CTLineRef line = (__bridge CTLineRef) [linesArray objectAtIndex:[linesArray count]-1];
CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
total_height = 100000 - line_y + (int) descent +1; //+1为了纠正descent转换成int小数点后舍去的值
CFRelease(textFrame);
return total_height;
}
在设置数据模型
LGTimeLineModel的同时,就去计算布局[self layout]
。
最后看看调用方,一般是ViewController,是如何使用layout的?👇
上图可见,tableView是直接拿着layout数据直接使用的,很简单。
3.2 预解码 & 预渲染
除了上面讲的预排版可以解决部分性能问题外,当我们碰到一些需要解码处理的数据时,该怎么办呢?例如:读取二进制流数据,生成一张图片,再渲染显示出来。
- DataBuffer就是二进制流数据,必须通过decode解码生成像素data。
- 像素data存储在ImageBuffer像素缓冲区中
- 然后帧缓冲区FrameBuffer拿到数据后,就去渲染显示
那么,其中最重要
的一步就是decode解码
,这个解码
非常消耗性能
。
iOS中视图展示的完整流程
-
Layout
:Layout 阶段主要进行视图构建
,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等。 -
Display
:Display 阶段主要进行视图绘制
,这里仅仅是设置最要成像的图元数据。重载视图的drawRect
: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。 -
Prepare
:Prepare 阶段属于附加步骤
,一般处理图像的解码和转换
等操作。 -
Commit
:Commit 阶段主要将图层进行打包
,并将它们发送至 Render Server
。该过程会递归
执行,因为图层和视图
都是以树形结构
存在。
那么decode解码就在prepare预备
这一阶段,这个解码依赖苹果底层的一个插件库:图形编解码插件
。
案例演示
接下来我们以SDWebImage
为例,看看其核心的图像从download到加载
的一个大致完整过程。
首先来到SDWebImageDownloaderOperation.m
中,我们知道,SDWebImage
中的下载是依赖网络底层URLSession
的,其Delegate方法URLSession:dataTask:didReceiveData:
是接收二进制流数据👇
接下来,我们看看解码的流程SDImageLoaderDecodeProgressiveImageData
👇
最终我们来到decodedImageWithImage
,看看SDWebImage
中如何进行渲染
的同时再做内存缓存
的👇
渲染
的核心代码👇(这个就不详细说明了,可以直接拿来使用的)
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
}
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
if (!cgImage) {
return NULL;
}
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
if (width == 0 || height == 0) return NULL;
size_t newWidth;
size_t newHeight;
switch (orientation) {
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationRightMirrored: {
// These orientation should swap width & height
newWidth = height;
newHeight = width;
}
break;
default: {
newWidth = width;
newHeight = height;
}
break;
}
BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
// iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
// Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
// But since our build-in coders use this bitmapInfo, this can have a little performance benefit
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
return NULL;
}
// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
return newImageRef;
}
从SDWebImage处理下载和解码图片的流程中,我们知道了在下载图片的数据接收delegate方法中,新开辟了子线程
去处理解码图片
,在子线程上做解码做渲染,不会影响主线程的交互,可以降低主线程runloop
执行任务的复杂度
,提高性能,这就是预解码预渲染
!解码的同时又以关联属性
的方式进行内存缓存
,这种手法十分巧妙,值得借鉴!
setImage为何影响性能
我们可以通过案例演示给大家看:给UIImageView设置一张本地的图片文件(图片尽可能大些,可超过20M
),测试代码如下👇
- (void)testImageView {
NSData *data = [NSData dataWithContentsOfFile:@"/Users/Aron/Desktop/RunLoop.png"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 压缩过后的二进制数据
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
// 解码还是在主线程上
self.imageView.image = image;
});
});
}
代码很简单:首先我们读取一张本地的图片
,然后开启子线程
将图片文件的二进制数据转换成image,再切回主线程
,赋值给imageView.image
显示出来。
接着,我们运行代码,然后打开Instrument
里的Time Profile
工具👇
再选择你的项目,选择模拟器运行,点击开始👇
接着查看主线程👇
上图可知,我们平时的imageView.image
赋值操作中,系统对图形的解码及渲染操作,全是在主线程执行的,这就是影响性能的根本原因。
3.3 按需加载
除了上面说的预排版
,预解码和预渲染
以外,我们比较常用的还有按需加载
,这个实现起来也不难。现在以UITableView为例,实现一个按需加载
的流程。UITableView在滚动过程中会不断的加载cell,虽然系统会对cell进行复用,但是假如你的cell布局很复杂,有很多图片得显示,那么势必会出现卡顿,此时我们可以利用按需加载,其核心的思路就是👇
在UITableView的滚动过程中,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行进行预先的加载,且在用户触摸时第一时间加载内容。
示例演示
接下来,我们看看是如何实现的👇
首先,看看自定义的UITableView源码,先看.h👇
再看.m👇
接着看看监控TableView的滚动,是如何处理按需加载的
完整.m代码
@implementation VVeboTableView{
NSMutableArray *datas;
NSMutableArray *needLoadArr;
BOOL scrollToToping;
}
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style{
self = [super initWithFrame:frame style:style];
if (self) {
self.separatorStyle = UITableViewCellSeparatorStyleNone;
self.dataSource = self;
self.delegate = self;
datas = [[NSMutableArray alloc] init];
needLoadArr = [[NSMutableArray alloc] init];
[self loadData];
[self reloadData];
}
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return datas.count;
}
- (NSInteger)numberOfSections{
return 1;
}
- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
NSDictionary *data = [datas objectAtIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell clear];
cell.data = data;
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
}
if (scrollToToping) {
return;
}
[cell draw];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell==nil) {
cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"cell"];
}
[self drawCell:cell withIndexPath:indexPath];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSDictionary *dict = datas[indexPath.row];
float height = [dict[@"frame"] CGRectValue].size.height;
return height;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
[needLoadArr removeAllObjects];
}
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if (labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
if (velocity.y<0) {
NSIndexPath *indexPath = [temp lastObject];
if (indexPath.row+3<datas.count) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
}
} else {
NSIndexPath *indexPath = [temp firstObject];
if (indexPath.row>3) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
scrollToToping = YES;
return YES;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
scrollToToping = NO;
[self loadContent];
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
scrollToToping = NO;
[self loadContent];
}
//用户触摸时第一时间加载内容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!scrollToToping) {
[needLoadArr removeAllObjects];
[self loadContent];
}
return [super hitTest:point withEvent:event];
}
- (void)loadContent{
if (scrollToToping) {
return;
}
if (self.indexPathsForVisibleRows.count<=0) {
return;
}
if (self.visibleCells&&self.visibleCells.count>0) {
for (id temp in [self.visibleCells copy]) {
VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
[cell draw];
}
}
}
//读取信息
- (void)loadData{
NSArray *temp = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"data" ofType:@"plist"]];
for (NSDictionary *dict in temp) {
NSDictionary *user = dict[@"user"];
NSMutableDictionary *data = [NSMutableDictionary dictionary];
data[@"avatarUrl"] = user[@"avatar_large"];
data[@"name"] = user[@"screen_name"];
NSString *from = [dict valueForKey:@"source"];
if (from.length>6) {
NSInteger start = [from indexOf:@"\">"]+2;
NSInteger end = [from indexOf:@"</a>"];
from = [from substringFromIndex:start toIndex:end];
} else {
from = @"未知";
}
data[@"time"] = @"2015-05-25";
data[@"from"] = from;
[self setCommentsFrom:dict toData:data];
[self setRepostsFrom:dict toData:data];
data[@"text"] = dict[@"text"];
NSDictionary *retweet = [dict valueForKey:@"retweeted_status"];
if (retweet) {
NSMutableDictionary *subData = [NSMutableDictionary dictionary];
NSDictionary *user = retweet[@"user"];
subData[@"avatarUrl"] = user[@"avatar_large"];
subData[@"name"] = user[@"screen_name"];
subData[@"text"] = [NSString stringWithFormat:@"@%@: %@", subData[@"name"], retweet[@"text"]];
[self setPicUrlsFrom:retweet toData:subData];
{
float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
CGSize size = [subData[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_SUBCONTENT) lineSpace:5];
NSInteger sizeHeight = (size.height+.5);
subData[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_BIG, width, sizeHeight)];
sizeHeight += SIZE_GAP_BIG;
if (subData[@"pic_urls"] && [subData[@"pic_urls"] count]>0) {
sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
}
sizeHeight += SIZE_GAP_BIG;
subData[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
}
data[@"subData"] = subData;
} else {
[self setPicUrlsFrom:dict toData:data];
}
{
float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
CGSize size = [data[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_CONTENT) lineSpace:5];
NSInteger sizeHeight = (size.height+.5);
data[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG, width, sizeHeight)];
sizeHeight += SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG;
if (data[@"pic_urls"] && [data[@"pic_urls"] count]>0) {
sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
}
NSMutableDictionary *subData = [data valueForKey:@"subData"];
if (subData) {
sizeHeight += SIZE_GAP_BIG;
CGRect frame = [subData[@"frame"] CGRectValue];
CGRect textRect = [subData[@"textRect"] CGRectValue];
frame.origin.y = sizeHeight;
subData[@"frame"] = [NSValue valueWithCGRect:frame];
textRect.origin.y = frame.origin.y+SIZE_GAP_BIG;
subData[@"textRect"] = [NSValue valueWithCGRect:textRect];
sizeHeight += frame.size.height;
data[@"subData"] = subData;
}
sizeHeight += 30;
data[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
}
[datas addObject:data];
}
}
- (void)setCommentsFrom:(NSDictionary *)dict toData:(NSMutableDictionary *)data{
NSInteger comments = [dict[@"reposts_count"] integerValue];
if (comments>=10000) {
data[@"reposts"] = [NSString stringWithFormat:@" %.1fw", comments/10000.0];
} else {
if (comments>0) {
data[@"reposts"] = [NSString stringWithFormat:@" %ld", (long)comments];
} else {
data[@"reposts"] = @"";
}
}
}
- (void)setRepostsFrom:(NSDictionary *)dict toData:(NSMutableDictionary *)data{
NSInteger comments = [dict[@"comments_count"] integerValue];
if (comments>=10000) {
data[@"comments"] = [NSString stringWithFormat:@" %.1fw", comments/10000.0];
} else {
if (comments>0) {
data[@"comments"] = [NSString stringWithFormat:@" %ld", (long)comments];
} else {
data[@"comments"] = @"";
}
}
}
- (void)setPicUrlsFrom:(NSDictionary *)dict toData:(NSMutableDictionary *)data{
NSArray *pic_urls = [dict valueForKey:@"pic_urls"];
NSString *url = [dict valueForKey:@"thumbnail_pic"];
NSArray *pic_ids = [dict valueForKey:@"pic_ids"];
if (pic_ids && pic_ids.count>1) {
NSString *typeStr = @"jpg";
if (pic_ids.count>0||url.length>0) {
typeStr = [url substringFromIndex:url.length-3];
}
NSMutableArray *temp = [NSMutableArray array];
for (NSString *pic_url in pic_ids) {
[temp addObject:@{@"thumbnail_pic": [NSString stringWithFormat:@"http://ww2.sinaimg.cn/thumbnail/%@.%@", pic_url, typeStr]}];
}
data[@"pic_urls"] = temp;
} else {
data[@"pic_urls"] = pic_urls;
}
}
- (void)removeFromSuperview{
for (UIView *temp in self.subviews) {
for (VVeboTableViewCell *cell in temp.subviews) {
if ([cell isKindOfClass:[VVeboTableViewCell class]]) {
[cell releaseMemory];
}
}
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
[datas removeAllObjects];
datas = nil;
[self reloadData];
self.delegate = nil;
[needLoadArr removeAllObjects];
needLoadArr = nil;
[super removeFromSuperview];
}
@end
3.4 异步渲染
如果上面的所有UI优化的方式都没法解决你的卡顿问题,那么接下来就讲一个终极大招:异步渲染
。
3.4.1异步渲染的原理
这个异步渲染
方式,可能在平时的开发中极少会用到
,虽然使用率低,但是我们也要掌握异步渲染的原理
。我们通过一个面试题
来切入👇
UIView 和CALayer之间是什么关系?
答案相信大家都知道,大致有以下几点:
- view通过layer驱动来完成显示
- view可以交互,而layer不可以
- view负责内容的管理,而layer负责内容的绘制
- view是layer的代理delegate
接着我们自定义一个View:LGView,在drawRect:
方法中打断点,bt查看其调用栈
是什么样的?👇
调用栈信息里从下至上
,依次调用了
[CALayer _display] --> [CALayer drawInContext:] --> [UIView(CALayerDelegate) drawLayer:inContext:] --> [LGView drawRect:]
伪代码模拟CALayer 和 UIView的交互过程
根据上面的调用栈信息
,我们伪代码模拟一下这个过程👇
- 首先定义一个CALayer子类:LGLayer,主要是覆写
display
方法
@interface LGLayer : CALayer
@end
我们从调用栈信息中,查看display中调用的是[CALayer drawInContext:]
方法,我们去到帮助文档搜索该方法👇
- 方法的入参是
CGContextRef
类型 - layer的delegate(即UIView,也就是我们的LGView)需要实现
drawLayer:inContext
方法
所以,我们在LGLayer的display方法的代码大致是这样👇
#import "LGLayer.h"
@implementation LGLayer
//前面断点调用写下的代码
- (void)layoutSublayers{
if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {
//UIView
[self.delegate layoutSublayersOfLayer:self];
}else{
[super layoutSublayers];
}
}
//绘制流程的发起函数
- (void)display{
// 创建CGContextRef上下文对象,给drawInContext:使用
CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
// 告知UIView即将展示Layer
[self.delegate layerWillDraw:self];
// Layer开始绘制内容
[self drawInContext:context];
// 告知UIView展示Layer
[self.delegate displayLayer:self];
// 关闭CGContextRef上下文对象
[self.delegate performSelector:@selector(closeContext)];
}
@end
- 再来看看LGView里需要做的事情
- 因为
LGView
需要Layer调用
关于上下文对象CGContextRef
的创建
和关闭
,所以在头文件中定义一下2个方法👇
@interface LGView : UIView
- (CGContextRef)createContext;
- (void)closeContext;
@end
然后方法的实现👇(我们只是简单的做一个图像的展示,所以采用ImageContext)
- (CGContextRef)createContext{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
return context;
}
- (void)closeContext{
UIGraphicsEndImageContext();
}
- LGView指定自定义的CALayer👇
+ (Class)layerClass{
return [LGLayer class];
}
- 因为需要
将UIView和CALayer关联
起来,所有需实现一下layoutSublayersOfLayer
方法
- (void)layoutSublayersOfLayer:(CALayer *)layer{
[super layoutSublayersOfLayer:layer];
[self layoutSubviews];
}
- UIView中还需实现关于
Layer的绘制
相关的方法:
//绘制的准备工作,do nontihing
- (void)layerWillDraw:(CALayer *)layer{
}
// 绘制的操作
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
[super drawLayer:layer inContext:ctx];
[[UIColor redColor] set];
// Core Graphics
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width / 2- 20, self.bounds.size.height / 2- 20, 40, 40)];
CGContextAddPath(ctx, path.CGPath);
CGContextFillPath(ctx);
}
// layer.contents = (位图)
- (void)displayLayer:(CALayer *)layer{
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
layer.contents = (__bridge id)(image.CGImage);
});
}
那么,LGView.m的完整版代码👇
#import "LGView.h"
#import "LGLayer.h"
@implementation LGView
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code, 绘制的操作, BackingStore(额外的存储区域产于的) -- GPU
}
+ (Class)layerClass{
return [LGLayer class];
}
- (void)layoutSublayersOfLayer:(CALayer *)layer{
[super layoutSublayersOfLayer:layer];
[self layoutSubviews];
}
- (CGContextRef)createContext{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
return context;
}
//绘制的准备工作,do nontihing
- (void)layerWillDraw:(CALayer *)layer{
}
//绘制的操作
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
[super drawLayer:layer inContext:ctx];
[[UIColor redColor] set];
// Core Graphics
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width / 2- 20, self.bounds.size.height / 2- 20, 40, 40)];
CGContextAddPath(ctx, path.CGPath);
CGContextFillPath(ctx);
}
// layer.contents = (位图)
- (void)displayLayer:(CALayer *)layer{
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
layer.contents = (__bridge id)(image.CGImage);
});
}
- (void)closeContext{
UIGraphicsEndImageContext();
}
@end
- run👇
从以上的案例可以知道,我们完全能实现CALayer和UIView之间的剥离
,LGView
就好比消费者
,LGLayer
是生产者
,LGView
收集我们需要绘制的内容
(包含CGContextRef的创建与关闭),而LGLayer负责渲染
(也就是display),渲染又很耗时
,这样我们就有了一个思路 --> 实现一个异步渲染的功能
,如果将渲染工作交给一个异步子线程处理,就能减轻GPU的负担了。
3.4.2 异步渲染案例演示
我们以Graver为例,看看它是怎么实现异步渲染的流程的👇
ps:
Graver
2年多时间没有更新了,我们只是看看它的实现的思路和原理。
- 首先主队列发起
网络请求
,请求页面需要展示的数据,这个网络请求是交给网络队列子线程
进行 - 接收到请求Response后,再交给
排版队列子线程
,实现预排版
,再将排版后的数据丢给主队列
-
主队列
中UIView接收到了排版数据
,再交给CALayer,CALayer开启绘制队列子线程
,渲染
生成位图
丢给主队列
-
主队列
接收位图
并展示
最终效果
Graver源码的Demo中,运行起来的最终效果👇
可以看到,cell里的布局就一个层级,一张图,这样展示起来会卡顿吗?当然不会!那么,这是怎样实现的呢?既然是一张图,它又是如何区分cell上子控件的交互事件呢?带着这两个疑问,我们逐一看看Graver
的异步渲染
和交互事件处理
的代码流程。
渲染流程
首先我们来到WMGAsyncDrawView
,这是负责异步渲染
的类,就好比我们之前定义的LGView
类
其中有两个重要的属性:BOOL contentsChangedAfterLastAsyncDrawing
和dispatch_queue_t dispatchDrawQueue
,标识是否异步绘制
和 异步绘制的队列
(由调用方指定
的)。
+ (Class)layerClass
{
return [WMGAsyncDrawLayer class];
}
WMGAsyncDrawView
的layer是类WMGAsyncDrawLayer
。接着我们看看displayLayer:方法中,WMGAsyncDrawView
是如何调用WMGAsyncDrawLayer
去异步绘制的👇
接着来到私有方法_displayLayer
中👇
//异步线程当中操作的~
- (void)_displayLayer:(WMGAsyncDrawLayer *)layer
rect:(CGRect)rectToDraw
drawingStarted:(WMGAsyncDrawCallback)startCallback
drawingFinished:(WMGAsyncDrawCallback)finishCallback
drawingInterrupted:(WMGAsyncDrawCallback)interruptCallback
{
BOOL drawInBackground = layer.isAsyncDrawsCurrentContent && ![[self class] globalAsyncDrawingDisabled];
[layer increaseDrawingCount]; //计数器,标识当前的绘制任务
NSUInteger targetDrawingCount = layer.drawingCount;
NSDictionary *drawingUserInfo = [self currentDrawingUserInfo];
//Core Graphic & Core Text
void (^drawBlock)(void) = ^{
void (^failedBlock)(void) = ^{
if (interruptCallback)
{
interruptCallback(drawInBackground);
}
};
//不一致,进入下一个绘制任务
if (layer.drawingCount != targetDrawingCount)
{
failedBlock();
return;
}
CGSize contextSize = layer.bounds.size;
BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
CGContextRef context = NULL;
BOOL drawingFinished = YES;
if (contextSizeValid) {
UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
context = UIGraphicsGetCurrentContext();
if (!context) {
WMGLog(@"may be memory warning");
}
CGContextSaveGState(context);
if (rectToDraw.origin.x || rectToDraw.origin.y)
{
CGContextTranslateCTM(context, rectToDraw.origin.x, -rectToDraw.origin.y);
}
if (layer.drawingCount != targetDrawingCount)
{
drawingFinished = NO;
}
else
{
//子类去完成啊~父类的基本行为来说~YES
drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
}
CGContextRestoreGState(context);
}
// 所有耗时的操作都已完成,但仅在绘制过程中未发生重绘时,将结果显示出来
if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
// 让 UIImage 进行内存管理
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
void (^finishBlock)(void) = ^{
// 由于block可能在下一runloop执行,再进行一次检查
if (targetDrawingCount != layer.drawingCount)
{
failedBlock();
return;
}
//赋值的操作~
layer.contents = (id)image.CGImage;
[layer setContentsChangedAfterLastAsyncDrawing:NO];
[layer setReserveContentsBeforeNextDrawingComplete:NO];
if (finishCallback)
{
finishCallback(drawInBackground);
}
// 如果当前是异步绘制,且设置了有效fadeDuration,则执行动画
if (drawInBackground && layer.fadeDuration > 0.0001)
{
layer.opacity = 0.0;
[UIView animateWithDuration:layer.fadeDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
layer.opacity = 1.0;
} completion:NULL];
}
};
if (drawInBackground)
{
dispatch_async(dispatch_get_main_queue(), finishBlock);
}
else
{
finishBlock();
}
}
if (CGImage) {
CGImageRelease(CGImage);
}
}
else
{
failedBlock();
}
UIGraphicsEndImageContext();
};
if (startCallback)
{
startCallback(drawInBackground);
}
if (drawInBackground)
{
// 清空 layer 的显示
if (!layer.reserveContentsBeforeNextDrawingComplete)
{
layer.contents = nil;
}
//[self drawQueue] 异步绘制队列,绘制任务
dispatch_async([self drawQueue], drawBlock);
}
else
{
void (^block)(void) = ^{
//
@autoreleasepool {
drawBlock();
}
};
if ([NSThread isMainThread])
{
// 已经在主线程,直接执行绘制
block();
}
else
{
// 不应当在其他线程,转到主线程绘制
dispatch_async(dispatch_get_main_queue(), block);
}
}
}
其中最最核心的一句代码就是dispatch_async([self drawQueue], drawBlock);
通过异步在绘制队列中执行layer的渲染👇
- 准备上下问对象👇
UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
context = UIGraphicsGetCurrentContext();
- 交由
子类
进行绘制👇
drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
- 绘制完毕,则转成位图👇
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
再生产UIImage
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
- 绘制完成交给主队列
dispatch_async(dispatch_get_main_queue(), finishBlock);
注意:CGImageRef用完之后需要释放
CGImageRelease(CGImage);
- 关闭上下文
UIGraphicsEndImageContext();
这个渲染的流程,和LGView的基本上一样。其中,第2步交由子类
完成绘制,视图类从父到子主要为 WMGAsynceDrawView
,WMGCanvasView
,WMGMixedView
。
-
WMGCanvasView 继承自 WMGAsyncDrawView, 主要负责圆角,边框,阴影和背景图片的绘制,绘制通过 CoreGraphics API 。
-
WMGMixedView 则是上层视图,属性仅有水平/垂直对齐方式,行数和绘制内容 attributedItem 。drawInRect 中则根据对齐方式来决定绘制文字位置, 然后调用 textDrawer 来进行文字渲染,如果其中有图片则会读取后直接通过 drawInRect: 方法来渲染图片(通过 TextDrawer 的 delegate)。
交互事件处理WMGCanvasControl
WMGCanvasControl
继承自 WMGCanvasView
,在这层处理事件响应,自定义实现了一套 Target-Action 模式,重写了 touchesBegin/Moved/Cancelled/Moved
一系列方法,来进行响应状态决议,然后将事件发给缓存的 targets 对象看能否响应指定的 control events
。
- (void)_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event
{
for(__WMGCanvasControlTargetAction *t in [self _targetActions])
{
if(t.controlEvents == controlEvents)
{
if(t.target && t.action)
{
[self sendAction:t.action to:t.target forEvent:nil];
}
}
}
}
其中__WMGCanvasControlTargetAction
类定义如下👇
@interface __WMGCanvasControlTargetAction : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL action;
@property (nonatomic, assign) UIControlEvents controlEvents;
@end
然后对外提供方法,并用数组去管理,对target-action的增、删、查👇
- (NSMutableArray *)_targetActions
{
if(!_targetActions)
_targetActions = [[NSMutableArray alloc] init];
return _targetActions;
}
总结
本篇文章针对UI卡顿这一常见现象,首先分析了卡顿的原理,然后提供了几种卡顿监测的方案,最后重点提供了几种常用的方案去优化卡顿,包括预排版
、预解码 & 预渲染
、按需加载
、异步渲染
。