iOS 中的界面优化

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。

  • 界面优化是一个老生常谈的问题,这篇文章主要介绍界面渲染流程,界面卡顿的检测解决卡顿的方法。

1. 界面渲染流程

通常我们知道视图显示是通过GPU进行图像渲染加载出来的

image.png

  1. CPU计算出要显示的内容,交由GPU处理。
  2. GPU渲染完成后把渲染的结果交由Frame Buffer(帧缓存区)处理。
  3. 帧缓存区会把把数据交给视频控制器进行读取
  4. 经过数模转换后显示在屏幕上

1.1 双缓存区设计

为了提高渲染效率,开发者设计出了双缓存区,2个FrameBuffer切换读取,当GPU渲染好放入帧缓存区,交给视屏控制器读取,之后读取显示过程中GPU渲染好的结果放在另一个帧缓存区,这样视频控制器来回切换读取,大大提高了效率

image.png

但是随之而来的问题是,当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象

image.png

为了解决这个问题引入了VSync垂直同步信号机制。开启后GPU会等待屏幕的VSync发出信号后进行新一帧的渲染和缓存。这样避免了加载不完全的问题,但是也会消耗更多的内存

1.2 垂直同步信号机制:V-sync

目前主流的移动设备是什么情况呢?从网上查到的资料可以知道,iOS 设备会始终使用双缓存,并开启垂直同步。 但是随之而来会有新的问题,界面的卡顿。我们知道每一帧的显示是经过CPU计算好要显示的内容后,交由GPU进行渲染,之后放入缓存区,视频控制器读取显示。如果某一帧CPU计算时间过长,或者GPU渲染过长。加起来的时间超过了垂直同步信号的间隔时间,这个时候视频控制器就不会读取没有处理好的数据,我们感觉到卡顿。其实就是掉帧,如下图所示:

image.png

通过图中可知我们需要对CPU的计算还是CGPU的渲染进行优化,减少用户卡顿。

2. 卡顿检测

卡顿的检测通常有2种方式:
1.FPS检测:我们可以使用YYKit中的YYFPSLabel,也可以仿照它自己自定义一个。

image.png

主要是通过CADisplayLink来实现,通过link的时间差计算一秒刷新的次数,根据刷新的次数显示刷新频次。对于一般的检测FPS已经够用了。

  1. 主线程卡顿监控

我们通过RunLoop来监控,因为卡顿的是事务,而事务是交由主线程RunLoop处理的。

@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;

        }

    });

}

@end
复制代码

通过runloop监测主线程每次执行任务循环的时间,如果这个时间超过我们定义的时间说明发生了超时,出行了卡顿。首先给mainRunloop添加观察者,通过CFRunLoopAddObserver,每次结束任务循环都会调用回调函数。之后再子线程监控时长,这里采用信号量计算间隔。当间隔超过1秒的话,并且两种状态kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting,则说明发生了卡顿

image.png 我们把线程睡眠2秒,监听超过1秒所以报卡顿了。

image.png

一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!,说明3秒了卡顿2次,减去默认1秒。

image.png

不卡顿的情况。 也可以直接使用三方库

  • Swift的卡顿检测第三方ANREye,其主要思路是:创建子线程进行循环监测,每次检测时设置标记置为true,然后派发任务到主线程,标记置为false,接着子线程睡眠超过阈值时,判断标记是否为false,如果没有,说明主线程发生了卡顿
  • OC可以使用 微信matrix滴滴DoraemonKit

3. 界面优化

3.1 CPU方面优化

  1. 预排版:对于我们常用的比如ableview,我们可以在拿到数据的时候就计算好它的布局情况,避免渲染的时候进行计算。比如cell的行高等。
  2. 减少动态的添加view,比如cell添加view。
  3. 对于一些没有交互的显示是图可以用CALayer代替UIView,用轻量级的对象
  4. 减少Autolayout的使用,对于简单的布局Autolayout可以大量节约我们时间,对于复杂页面,嵌套布局较多会造成CPU运算消耗会呈指数级上升如果你不想手动调整 frame 等属性,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
  5. 按需加载,我们可以使用懒加载复用机制。不要一次性创建所有的subview,当需要时才创建,当完成任务,进行切换的时候可以放入一个可重用的队列,这样下次滚动或者显示的时候,避免不必要的内存分配。
  6. 当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
  7. 对于一些处理较慢的objects,比如NSDateFormatterNSCalendar。比如请求的数据中显示日期,想要避免使用这个对象的瓶颈就需要复用他们,可以通过添加属性到类中,或者创建静态变量来实现。
  8. 尽量避免使用透明view,因为使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来
  9. 请求或者耗时操作异步子线程处理,不要阻塞主线程
  10. 正确的设置背景图片,全屏背景图的话添加一个UIImageview作为子View。某个小小的view的背景图,使用UIColorcolorWithPatternImage来做,它会更快的渲染不会浪费很多内存。
  11. 尽量使用PNG图片,不使用JPGE图片;优化图片大小,尽量避免动态缩放,在运行中缩放图片很耗费资源,特别UIImageview嵌套UIScrollView中。如果是从服务器中下载的,可以在下载前调整到合适大小。也可以在下载完成后用background thread进行缩放一次,之后UIImageview使用缩放后的图片。
  12. 图片在使用UIImage或者CGImageSource创建时,图片不会立即解码,而是在设置的时候进行解码,我们可以在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap 直接创建图片,例如SDWebImage三方框架中对图片编解码的处理。这就是Image的预解码
  13. 当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的绘制子线程中进行
  14. 图片是否进行缓存,imageNamed会加载成功后缓存到内存中,而对于imageWithContentOfFile则不会,实用于加载一张大图并且使用一次。

3.2 GPU方面

对于GPU方面主要是优化它渲染进行优化

  1. 尽量减少在短时间内大量图片的显示,尽可能将多张图片合为一张显示,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况

  2. 尽量避免离屏渲染,它会开辟新的缓存区,同时整个过程会多次切换上下文,显示从当前的屏幕切换到离屏,离屏渲染结束后把离屏缓冲区的结果显示到屏幕上,又要将上下文环境从离屏切换到当前屏幕。这样会造成大量内存浪费,更多的开销。

  3. 离屏渲染通常有:光栅化 layer.shouldRasterize = YES;遮罩层mask,阴影shadow, 圆角cornerRadius+clipsToBounds,毛玻璃效果等。

  4. 异步渲染,例如可以将cell中的所有控件、视图合成一张图片进行显示,可以参考Graver三方框架。

Guess you like

Origin juejin.im/post/7064938326062530591
ios