iOS-如何优化界面

前言

在我们的项目,我们有时候会遇到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和addMonitorThread

  • DoraemonKit是滴滴卡顿方案

3 预排版

预排版即预计算
我们做开发UI的时候,一般的思路:

  1. 请求网络,获取数据
  2. 布局UI的时候,计算行高和frame,渲染UI

优化如下:

  1. 请求网络
  2. 解析网络数据到Model
  3. 根据Model 计算出行高,frame位置,即计算出cellLayout保存起来
  4. 刷新数据

这样做的好处

  • 计算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<SDWebImageCacheKeyFilter> 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<SDAnimatedImage>)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渲染层级

  1. UIKit
  2. Core Animation
  3. OpenGL ES/Metal CoreGraphics
  4. Graphics Hardware

第2层* OpenGL ES/Metal CoreGraphics*我们是可以操作的。

我们的一次渲染称为一次事务,它的环节有:

  1. layout 构建视图
  2. display 绘制
  3. prepare CoreAnimation的操作
  4. commit提交给reader server处理

我们在View绘制是在drawRect中操作,依赖于UIViewRendering, 它的流程如下


1

从这个堆栈中可以看出我们渲染流程是经过很多步骤的,是很耗时的过程。

这个时候引入Layer层,当我们layoutSublayers时,在display中会调起drawlayer:inContext方法,最终会在layerWillDraw绘制准备工作。

我们优化的方法,让这些耗时的操作放在子线程中绘制完成,最终在displayLayer回主线程进行显示,这就是异步渲染

6.2 异步渲染框架

  • Graver渲染流程


    2
- (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的预解码以及异步渲染的知识,希望此文有助于大家优化我们的界面,使用界面更加流畅,给用户带来更好的体验。

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

推荐阅读更多精彩内容