前言
在我们的项目,我们有时候会遇到UI不太流畅,有时卡顿,给用户的感觉不那么友好,降低了体验感,那么这些问题是怎么产生的,以及如何解决这些问题,我们今天就来看下我们的UI如何优化。
1 卡顿的原理
卡顿是因为掉帧引起的,为什么会出现掉帧呢,这就需要我们分析下屏幕显示的原理。
CPU负责需要渲染的数据进行计算。
GPU负责渲染,把需要渲染的数据输出到framebuffer(帧缓冲区)
framebuffer再输出到Video Controller,最终于Monitor。
因为CPU的计算需要耗时,为了解决这个性能问题,引入了双缓冲的机制,一个前帧和一个后帧。
不断的从两个缓冲区交替读取数据,避免了显示空白的问题。
现实中有这么一种情况:
在读取某个帧缓冲区时 ,发现这个缓冲区比较忙,无法渲染,就会丢弃,读另一个缓冲区的渲染数据,这个时候如果刚好读取到渲染数据就会用这个数据去渲染,这个时候就出现了丢帧,界面上就会出现卡顿的现象。
60HZ/每秒这个频率在人类肉眼是感觉不到卡顿。
在两个VSync(垂直同步)信号之间可以计算显示完成,就可以显示正常。
2 卡顿的检测
我们在项目遇到卡顿的情况,该如何检测呢?
- YYKit的FPS可以检测,利用CADisplayLink(Class representing a timer bound to the display vsync)绑定在垂直同步信息号计时器。
通过count/时间间隔(刷新是频率)是否达到60fps。
垂直同步信息号是16.67ms一次。 - Runloop的卡顿检测
CFRunLoopObserverCreate创建一个observer的通知,通过回调监听事务的状态,
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
在回调中发送一个信号,如果在正常的时间间隔下,没有收到信号,就代表有其它事务阻塞。
// 创建信号
_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;
}
});
matrix是微信卡顿检测,基本原理也是利用Runloop的方案,做的比较全面些,Runloop的Timeout,CPU使用率等,也包括堆栈信息。
核心函数addRunLoopObserver和addMonitorThreadDoraemonKit是滴滴卡顿方案
3 预排版
预排版即预计算
我们做开发UI的时候,一般的思路:
- 请求网络,获取数据
- 布局UI的时候,计算行高和frame,渲染UI
优化如下:
- 请求网络
- 解析网络数据到Model
- 根据Model 计算出行高,frame位置,即计算出cellLayout保存起来
- 刷新数据
这样做的好处
- 计算cellLayout的时候,在子线程处理,不会阻塞主线程
- 保存计算好的cellLayout,防止重复计算,提高流畅度
4 预解码 & 预渲染
我们加载网络图片的时候,一般用的是SDWebImage,里面有这样一段代码
UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
NSCParameterAssert(imageData);
NSCParameterAssert(imageURL);
UIImage *image;
id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL];
} else {
cacheKey = imageURL.absoluteString;
}
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
NSValue *thumbnailSizeValue;
BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
if (shouldScaleDown) {
CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
CGFloat dimension = ceil(sqrt(thumbnailPixels));
thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
}
if (context[SDWebImageContextImageThumbnailPixelSize]) {
thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
}
SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
mutableCoderOptions[SDImageCoderWebImageContext] = context;
SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];
if (image) {
// Preload frames if supported
if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
[((id)image) preloadAllFrames];
}
} else {
// Check image class matching
if (options & SDWebImageMatchAnimatedImageClass) {
return nil;
}
}
}
}
if (!image) {
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
}
if (image) {
BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
}
return image;
}
开启了异步线程,放在串行队列,然后对imageData(NSData类型)的解码操作, 我们想一想为什么要这么做?
我们加载图片用的是UIImage这个类,这个加载其实并不是我们所谓的图片。
UIImage其实是一个模型,包含了Data Buffer和image Buffer,我们的图片是通过Data Buffer转换过来,再通过image Buffer缓冲区存储,再显示到UIImageView上。
图片加载流程
1. Data Buffer进行decode解码操作
2. image Buffer缓冲区存储
3. Frame Buffer(帧缓冲区)渲染
5 图片为什么需要预解码
当前我们不使用用SDWebImage加载图片时,它的解码操作是在主线程操作的。
这样的话,会对主线程造成很大的压力,造成主线程出现阻塞。
当然还有按需加载的优化方式。
6 异步渲染
6.1 UIView 与layer的关系
UIView更加偏向于与用户交互行为,Layer是来用渲染的
在真正渲染的过程中,是由Layer完成的,它是一个耗时的操作
iOS渲染层级
- UIKit
- Core Animation
- OpenGL ES/Metal CoreGraphics
- Graphics Hardware
第2层* OpenGL ES/Metal CoreGraphics*我们是可以操作的。
我们的一次渲染称为一次事务,它的环节有:
- layout 构建视图
- display 绘制
- prepare CoreAnimation的操作
- commit提交给reader server处理
我们在View绘制是在drawRect中操作,依赖于UIViewRendering, 它的流程如下
从这个堆栈中可以看出我们渲染流程是经过很多步骤的,是很耗时的过程。
这个时候引入Layer层,当我们layoutSublayers时,在display中会调起drawlayer:inContext方法,最终会在layerWillDraw绘制准备工作。
我们优化的方法,让这些耗时的操作放在子线程中绘制完成,最终在displayLayer回主线程进行显示,这就是异步渲染
6.2 异步渲染框架
-
Graver渲染流程
- (void)displayLayer:(CALayer *)layer
{
if (!layer) return;
NSAssert([layer isKindOfClass:[WMGAsyncDrawLayer class]], @"WMGAsyncDrawingView can only display WMGAsyncDrawLayer");
if (layer != self.layer) return;
[self _displayLayer:(WMGAsyncDrawLayer *)layer rect:self.bounds drawingStarted:^(BOOL drawInBackground) {
[self drawingWillStartAsynchronously:drawInBackground];
} drawingFinished:^(BOOL drawInBackground) {
[self drawingDidFinishAsynchronously:drawInBackground success:YES];
} drawingInterrupted:^(BOOL drawInBackground) {
[self drawingDidFinishAsynchronously:drawInBackground success:NO];
}];
}
//异步线程当中操作的~
- (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);
}
}
}
这里就是在displayLayer中开异步线程绘制,然后回到主线程。
Graver是可以交互的,因为它最终是继承于UIView。
总结
本篇文章给大家介绍了卡顿的原理,如何检测卡顿,预排版,Image的预解码以及异步渲染的知识,希望此文有助于大家优化我们的界面,使用界面更加流畅,给用户带来更好的体验。