iOS底层原理-界面优化

界面优化无非就是解决卡顿问,优化界面流畅度,如下就经过先分析卡顿的缘由,而后再介绍具体的优化方案,来分析如何作界面优化微信

  • 界面渲染流程

    具体流程能够参考图片渲染初探这里就大概讲一下图片渲染的流程,大致上能够分为三个阶段就是 CPU处理阶段 GPU处理阶段和视频控制器显示阶段。
    1. CPU主要是计算出须要渲染的模型数据
    2. GPU主要是根据 CPU提供的渲染模型数据渲染图片而后存到帧缓冲区
    3. 视频控制器冲帧缓冲区中读取数据最后成像
    大体流程图解以下:
    16d81697920a87d3.png
    苹果为了解决图片撕裂的问题使用了 VSync + 双缓冲区的形式,就是显示器显示完成一帧的渲染的时候会向 发送一个垂直信号 VSync,收到这个这个垂直信号以后显示器开始读取另一个帧缓冲区中的数据而 App接到垂直信号以后开始新一帧的渲染。
  • 卡顿原理

    经过上文张的界面渲染流程知道,在图一帧渲染完成以后会发送一个垂直信号此时开始读取另一个帧缓冲区中的数据,加入此时 CPUGPU的工做尚未完成,也就是另一个帧缓冲区仍是加锁状态没有数据的时候,此时显示器显示的仍是上一帧的图像那么这种状况就会一直等待下一帧绘制完成而后视频控制器再读取另一个帧缓冲区中的数据而后成像,中间这个等待的过程就形成了掉帧,也就是会卡顿。
    卡顿图解以下:
    16d8169791d4b86e.png 这种状况随会形成卡顿
  • 卡顿检测

    1. FPS监控
      苹果的iPhone推荐的刷新率是60Hz,也就是每秒中刷新屏幕60次,也就是每秒中有60帧渲染完成,差很少每帧渲染的时间是1000/60 = 16.67毫秒整个界面会比较流畅,通常刷新率低于45Hz的就会出现明显的卡顿现象。这里能够经过YYFPSLabel来实现FPS的监控,该原理主要是依靠 CADisplayLink来实现的,经过CADisplayLink来监听每次屏幕刷新并获取屏幕刷新的时间,而后使用次数(也就是1)除以每次刷新的时间间隔获得FPS,具体源码以下:
      #import "YYFPSLabel.h"
        #import "YYKit.h"
      
        #define kSize CGSizeMake(55, 20)
      
        @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];
            }
      
            //YYWeakProxy 这里使用了虚拟类来解决强引用问题
            _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;
                NSLog(@"sdf");
                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
      复制代码
      FPS只用在开发阶段的辅助性的数值,由于他会频繁唤醒 runloop若是 runloop在闲置的状态被 CADisplayLink唤醒则会消耗性能。
    2. 经过RunLoop检测卡顿
      经过监听主线程 Runloop一次循环的时间来判断是否卡顿,这里须要配合使用 GCD的信号量来实现,设置初始化信号量为0,而后开一个子线程等待信号量的触发,也是就是在子线程的方法里面调用 dispatch_semaphore_wait方法设置等待时间是1秒,而后主线程的 RunloopObserver回调方法中发送信号也就是调用 dispatch_semaphore_signal方法,此时时间能够置为0了,若是是等待时间超时则看此时的 Runloop的状态是不是 kCFRunLoopBeforeSources或者是 kCFRunLoopAfterWaiting,若是在这两个状态下两秒则说明有卡顿,详细代码以下:(代码中也有相关的注释)
      #import "LGBlockMonitor.h"
      
        @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{
            // 建立信号c
            _semaphore = dispatch_semaphore_create(0);
            // 在子线程监控时长
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                while (YES)
                {
                    // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 全部的任务
                    // 没有接收到信号底层会先对信号量进行减减操做,此时信号量就变成负数
                    // 因此开始进入等到,等达到了等待时间尚未收到信号则进行加加操做复原信号量
                    // 执行进入等待的方法dispatch_semaphore_wait会返回非0的数
                    // 收到信号的时候此时信号量是1  底层是减减操做,此时恰好等于0 因此直接返回0
                    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)
                        {
                            //若是一直处于处理source0或者接受mach_port的状态则说明runloop的此次循环尚未完成
                            if (++self->_timeoutCount < 2){
                                NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                                continue;
                            }
                            // 若是超过两秒则说明卡顿了
                            // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
                            NSLog(@"检测到超过两次连续卡顿");
                        }
                    }
                    self->_timeoutCount = 0;
                }
            });
        }
      
      
      
        @end
      复制代码
    3. 微信matrix
      此方案也是借助 runloop实现的大致流程和方案三相同,不过微信加入了堆栈分析,可以定位到耗时的方法调用堆栈,因此须要准确的分析卡顿缘由能够借助微信matrix来分析卡顿。固然也能够在方案2中使用 PLCrashReporter这个开源的第三方库来获取堆栈信息
    4. 滴滴DoraemonKit
      实现方案大概就是在子线程中一直 ping主线程,在主线程卡顿的状况下,会出现断在的无响应的表现,进而检测卡顿
  • 优化方案

    上文中分析卡顿的缘由咱们知道主要就是在 CPUGPU阶段占用时间太长致使了掉帧卡顿,因此界面优化主要工做就是给 CPUGPU减负
    • 预排版
      预排版主要是对 CPU进行减负。
      假设如今又个 TableView其中须要根据每一个 cell的内容来定 cell的高度。咱们知道 TableView有重用机制,若是复用池中有数据,即将滑入屏内的 cell就会使用复用池内的 cell,作到节省资源,可是仍是要根据新数据的内容来计算 cell的高度,从新布局新 cell中内容的布局 ,这样反复滑动 TableView相同的 cell就会反复计算其 frame,这样也给 CPU带来了负担。若是在获得数据建立模型的时候就把 cell frame算出,TableView返回模型中的 frame这样的话一样的一条 cell就算来回反复滑动 TableView,计算 frame这个操做也就仅仅只会执行一次,因此也就作到了减负的功能,以下图:一个 cell的组成须要 modal找到数据,也须要 layout找到这个 cell如何布局: 未命名文件(41).png
    • 预解码 & 预渲染
      图片的渲染流程,在 CPU阶段拿到图片的顶点数据和纹理以后会进行解码生产位图,而后传递到 GPU进行渲染主要流程图以下 image.png 若是图片不少很大的状况下解码工做就会占用主线程 RunLoop致使其余工做没法执行好比滑动,这样就会形成卡顿现象,因此这里就能够将解码的工做放到异步线程中不占用主线程,可能有人会想只要将图片加载放到异步线程中在异步线程中生成一个 UIImage或者是 CGImage而后再主线程中设置给 UIImageView,此时能够写段代码使用 instrumentsTime Profiler查看一下堆栈信息 image.png 发现图片的编解码仍是在主线程。 针对这种问题常见的作法是在子线程中先将图片绘制到CGBitmapContext,而后从Bitmap 直接建立图片,例如SDWebImage三方框架中对图片编解码的处理。这就是Image的预解码,代码以下:
      dispatch_async(queue, ^{
           CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
           CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
      
           BOOL hasAlpha = NO;
           if (alphaInfo == kCGImageAlphaPremultipliedLast ||
               alphaInfo == kCGImageAlphaPremultipliedFirst ||
               alphaInfo == kCGImageAlphaLast ||
               alphaInfo == kCGImageAlphaFirst) {
               hasAlpha = YES;
           }
      
           CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
           bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
      
           size_t width = CGImageGetWidth(cgImage);
           size_t height = CGImageGetHeight(cgImage);
      
           CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
           CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
           cgImage = CGBitmapContextCreateImage(context);
      
           UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
           CGContextRelease(context);
           CGImageRelease(cgImage);
           completion(image);
       });
      复制代码
    • 按需加载
      顾名思义须要显示的加载出来,不须要显示的加载,例如 TableView中的图片滑动的时候不加载,在滑动中止的时候加载(可使用Runloop,图片绘制设置 defaultModal就行)
    • 异步渲染
      再说异步渲染以前先了解一下 UIViewCALayer的关系:
      1. UIView是基于 UIKit框架的,可以接受点击事件,处理用户的触摸事件,并管理子视图
      2. CALayer是基于 CoreAnimation,而CoreAnimation是基于QuartzCode的。因此CALayer只负责显示,不能处理用户的触摸事件
      3. UIView是直接继承 UIResponder的,CALayer是继承 NSObject
      4. UIVIew 的主要职责是负责接收并响应事件;而 CALayer 的主要职责是负责显示 UIUIView 依赖于 CALayer 得以显示
      总结:UIView主要负责时间处理,CALayer主要是视图显示 异步渲染的原理其实也就是在子线程将全部的视图绘制成一张位图,而后回到主线程赋值给 layercontents,例如 Graver框架的异步渲染流程以下:
      image.png 核心源码以下:
      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;
                    // ...
                }
                if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
                else finishBlock();
            }
      
            // 一些清理工做: release CGImageRef, Image context ending
        }
      
      复制代码
      最终效果图以下:
      image.png 也可使用 YYAsyncLayer
    • 其余
      1. 减小图层的层级
      2. 减小离屏渲染
      3. 图片显示的话图片的大小设置(不要太大)
      4. 少使用addViewcell动态添加view
      5. 尽可能避免使用透明view,由于使用透明view,会致使在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混合处理(当有两个图层的时候一个是半透明一个是不透明若是半透明的层级更高的话此时就会触发颜色混合,底层的混合并非仅仅的将两个图层叠加而是会将两股颜色混合计算出新的色值显示在屏幕中)
相关文章
相关标签/搜索