1、卡顿原理

1.1、界面显现原理

iOS底层探索-界面优化

  • CPULayout UI布局、文本计算、Display制作、Prepare图片解码、Commit提交位图给 GPU

  • GPU:用于烘托,将结果放入 FrameBuffer

  • FrameBuffer:帧缓冲

  • Video Controller:依据Vsync(笔直同步)信号,逐行读取 FrameBuffer 中的数据,经过数模转换传递给 Monitor

  • Monitor:显现器,用于显现;关于显现模块来说,会按照手机刷新率以固定的频率:1 / 刷新率FrameBuffer 索要数据,这个索要数据的指令便是 笔直同步信号Vsync(低刷60帧为16.67毫秒,高刷120帧为 8.33毫秒,下边举例主要以低刷16.67毫秒为主)

1.2、界面撕裂

显现端每16.67ms从 FrameBuffer(帧缓存区)读取一帧数据,假如遇到耗时操作交给不了,那么当时画面就还是旧一帧的画面,但显现过程中,下一帧数据准备结束,导致部分显现的又是新数据,这样就会形成屏幕撕裂

1.3、界面卡顿

  • 为了解决界面撕裂,苹果运用双缓冲机制 + 笔直同步信号,运用 2个FrameBuffer 存储 GPU 处理结果,显现端替换从这2个FrameBuffer中读取数据,一个被读取时另一个去缓存;但解决界面撕裂的问题也带来了新的问题:掉帧
    iOS底层探索-界面优化
  • 假如遇到画面带马赛克等状况,导致GPU烘托能力跟不上,会有2种掉帧状况;如图,FrameBuffer2 未烘托完第2帧,下一个16.67ms去 FrameBuffer1 中拿第3帧:
    • 掉帧状况1:第3帧烘托结束,接下来需要第4帧,第2帧被丢掉
    • 掉帧状况2:第3帧未烘托完,再一个16.67ms去 FrameBuffer2 拿到第2帧,但第1帧多停留了16.67*2毫秒

小结

  • 固定的时刻距离会收到笔直同步信号(Vsync),假如 CPUGPU 还没有将下一帧数据放到对应的帧 FrameBuffer缓冲区,就会呈现 掉帧
    iOS底层探索-界面优化

2、卡顿检测

2.1、CADisplayLink

体系在每次发送 VSync 时,就会触发CADisplayLink,经过计算每秒发送 VSync 的数量来检查 AppFPS 是否稳定

#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, assign) NSTimeInterval lastTime; // 每隔1秒记录一次时刻
@property (nonatomic, assign) NSUInteger count;     // 记录VSync1秒内发送的数量
@end
@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkAction:)];
  [_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)linkAction: (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;
  NSLog(@" FPS : %f ", fps);
}
@end

2.2、RunLoop检测

RunLoop 的退出和进入本质都是Observer的告诉,咱们可以监听Runloop的状况,并在相关回调里发送信号,假如在设定的时刻内能够收到信号阐明是流通的;假如在设定的时刻内没有收到信号,阐明发生了卡顿。

#import "LZBlockMonitor.h"
@interface LZBlockMonitor (){
  CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LZBlockMonitor
+ (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];
}
- (void)registerObserver{
  CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
  //NSIntegerMax : 优先级最小
  CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                              kCFRunLoopAllActivities,
                              YES,
                              NSIntegerMax,
                              &CallBack,
                              &context);
  CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
  LZBlockMonitor *monitor = (__bridge LZBlockMonitor *)info;
  monitor->activity = activity;
  // 发送信号
  dispatch_semaphore_t semaphore = monitor->_semaphore;
  dispatch_semaphore_signal(semaphore);
}
- (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
  • 主线程监听 kCFRunLoopBeforeSources(行将处理事情)和kCFRunLoopAfterWaiting(行将休眠),子线程监控时长,若接连两次 1秒 内没有收到信号,阐明发生了卡顿

2.3、微信matrix

  • 微信的matrix也是借助 runloop 完成,大体流程与上面 Runloop 办法相同,它运用退火算法优化捕获卡顿的功率,防止接连捕获相同的卡顿,而且经过保存最近的20个主线程仓库信息,获取最近最耗时仓库

2.4、滴滴DoraemonKit

  • DoraemonKit的卡顿检测计划不运用 RunLoop,它也是while循环中依据一定的状况判别,经过主线程中不断发送信号semaphore,循环中等候信号的时刻为5秒,等候超时则阐明主线程卡顿,并进行相关上报

3、优化办法

平时简单的计划有:

  • 防止运用 透明UIView
  • 尽量运用PNG图片
  • 防止离屏烘托(圆角运用贝塞尔曲线等)

3.1、预排版

  • 便是惯例的在Model层恳求数据后提早将cell高度算好

3.2、预编码 / 解码

  • UIImage 是一个Model二进制流数据 存储在DataBuffer中,经过decode解码,加载到imageBuffer中,最终进入FrameBuffer才能被烘托

    iOS底层探索-界面优化

  • 当运用 UIImageCGImageSource的办法创立图片时,图片的数据不会立即解码,而是在设置UIImageView.image时解码

  • 将图片设置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU烘托前,CGImage中的数据才进行解码

  • 假如任由体系处理,这一步则无法防止,而且会发生在主线程中。假如想防止这个机制,在子线程先将图片制作到CGBitmapContext,然后从Bitmap中创立图片

3.3、按需加载

  • 假如方针行与当时行相差超过指定行数,只加载方针滚动规模的前后指定3行
    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
        [needLoadArr removeAllObjects];
    }
    - (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];
        }
    }
    
  • 在滑动结束时进行 Cell 的烘托
    - (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];
            }
        }
    }
    
  • 这种办法会导致滑动时有空白内容,因而要做好占位内容

3.4、异步烘托

  • 异步烘托 便是在子线程把需要制作的图形提早处理好,然后将处理好的图像数据直接返给主线程运用
  • 异步烘托操作的是layer层,将多层堆叠的控件们经过UIGraphics画成一张位图,然后展现在layer.content
3.4.1、CALayer
  • CALayer根据CoreAnimation从而根据QuartzCode,只负责显现,且显现的是位图,不能处理用户的接触事情
  • 不需要与用户交互时,运用 UIViewCALayer 都可以,乃至 CALayer 更简练高效
3.4.2、异步烘托完成
  • 异步烘托的结构引荐:GraverYYAsyncLayer
  • CALayer 在调用display办法后回去调用制作相关的办法,制作会履行drawRect:办法
简单比如
  • 承继 CALayer

    #import "LZLayer.h"
    @implementation LZLayer
    //前面断点调用写下的代码
    - (void)layoutSublayers{
        if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {
            //UIView
            [self.delegate layoutSublayersOfLayer:self];
        }else{
            [super layoutSublayers];
        }
    }
    //制作流程的发起函数
    - (void)display{
        // Graver 完成思路
        CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
        [self.delegate layerWillDraw:self];
        [self drawInContext:context];
        [self.delegate displayLayer:self];
        [self.delegate performSelector:@selector(closeContext)];
    }
    @end
    
  • 承继 UIView

    // - (CGContextRef)createContext 和 - (void)closeContext要在.h中声明
    #import "LZView.h"
    #import "LZLayer.h"
    @implementation LZView
    - (void)drawRect:(CGRect)rect {
        // Drawing code, 制作的操作, BackingStore(额外的存储区域产于的) -- GPU
    }
    //子视图的布局
    - (void)layoutSubviews{
        [super layoutSubviews];
    }
    + (Class)layerClass{
        return [LZLayer 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;
    }
    - (void)layerWillDraw:(CALayer *)layer{
        //制作的准备工作,do nontihing
    }
    //制作的操作
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
        [super drawLayer:layer inContext:ctx];
        // 画个不规则图形
        CGContextMoveToPoint(ctx, self.bounds.size.width / 2- 20, 20);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 20, 20);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 40, 80);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 40, 100);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 20, 20);
        CGContextSetFillColorWithColor(ctx, UIColor.magentaColor.CGColor);
        CGContextSetStrokeColorWithColor(ctx, UIColor.magentaColor.CGColor); // 描边
        CGContextDrawPath(ctx, kCGPathFillStroke);
        // 画个赤色方块
        [[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);
        // 文字
        [@"LZ" drawInRect:CGRectMake(self.bounds.size.width / 2 - 40, 100, 80, 24) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:20],NSForegroundColorAttributeName: UIColor.blueColor}];
        // 图片
        [[UIImage imageWithContentsOfFile:@"/Volumes/Disk_D/test code/Test/Test/yasuo.png"] drawInRect:CGRectMake(10, self.bounds.size.height/2, self.bounds.size.width - 20, self.bounds.size.height/2 -10)];
    }
    //layer.contents = (位图)
    - (void)displayLayer:(CALayer *)layer{
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        dispatch_async(dispatch_get_main_queue(), {
            layer.contents = (__bridge id)(image.CGImage);
        });
    }
    - (void)closeContext{
        UIGraphicsEndImageContext();
    }
    
  • 控件们被制作成了一张图

    iOS底层探索-界面优化

  • 此外,虽然将控件画到一张位图上,可是还有问题,便是控件的交互事情,内容较多建议研究一下graver的源码