项目开发进程中,跟着页面复杂度及代码书写问题,某些界面或许会呈现卡顿现在,这时咱们就需求对界面进行优化.

界面展现原理

通常来说,核算机中的显现进程是经过CPU、GPU和谐工作来将界面显现到屏幕上的

  1. CPU核算好显现的内容,提交到GPU
  2. GPU经过改换、合成、烘托完结后将烘托的成果放入帧缓存区(frameBuffer)
  3. 随后视频控制器(videoController)会依照笔直同步信号(VSync)逐行读取frameBuffer中的数据
  4. 经过或许的数模转化传递给显现器进行显现
    iOS - 界面优化
    最开始时,frameBuffer只有一个,这种情况下frameBuffer的读取和改写有很大的功率问题,为了解决这个问题,引入了双缓存区,也便是双缓冲机制.在这种情况下,GPU会预先烘托好一帧放入frameBuffer,让videoController读取,当下一帧烘托好后,GPU会直接将videoController的指针指向第二个frameBuffer.

双缓存区虽然解决了功率问题,可是会呈现新的问题:当videoController还未读取完结时,例如内容只显现了一半,GPU将新的一帧内容提交到frameBuffer,并将videoController的指针指向新的frameBuffer,videoController就会将新的一帧数据的下半段显现到屏幕上,造成屏幕撕裂

为了解决这个问题,选用了VSync,当敞开VSync后,GPU会等待显现器的VSync信号宣布后,才进行新一帧的烘托和frameBuffer的更新.

现在iOS设备中选用的便是双缓存区 + VSync

界面卡顿的原因

咱们知道了界面假如展现的,下面咱们来看看界面卡顿的原因

VSync信号到来后,体系图形服务会经过CADDisplayLink等机制告诉App进行 CPU -> GPU -> 烘托到frameBuffer -> 等待下一次VSync信号到来时显现到屏幕上.由于笔直同步到机制,假如在一个VSync时间内,CPU/GPU没有完结内容提交,则那一帧就会被丢弃,等待下一次时机再显现,显现屏会保存之前到内容不变,也便是俗称的掉帧

iOS - 界面优化
从图能够看出,当CPU/GPU阻塞了显现流程时,就会造成掉帧现象,在开发中,咱们需求进行卡顿检测来判别是否发生掉帧现象,现已发生掉帧现象的相应优化

卡顿监测

卡顿监测的办法一般有两种

  • FPS监测:为了保持流畅的UI交互,APP的改写频率应保持在60fps左右,其原因是由于iOS设备默许的改写频率≈60次/s,而一次改写(VSync信号宣布)的距离是 1000/60 ≈ 16.67ms,所以在信号宣布的距离内没准备好下一帧,就会产生卡顿
  • runloop监测: 经过子线程监测主线程的Runloop,判别两个状况KCFRunloopBeforeSourcesKCFRunloopAfterWaiting之间的耗时是否抵达设定的阀值
FPS监测

FPS的监控,参照YYKit中的YYFPSLabel,主要是经过CADisplayLink完成.借助link的时间差,来核算一次改写改写所需的时间,然后经过 改写次数 / 时间差 得到改写频次,并判别是否其规模,经过显现不同的文字色彩来表示卡顿严峻程度.主要代码如下:

@implementation YYFPSLabel {
  CADisplayLink *_link;
  NSUInteger _count;
  NSTimeInterval _lastTime;
}
- (instancetype)initWithFrame:(CGRect)frame {
  _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
  [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
  return self;
}
- (void)dealloc {
  [_link invalidate];
}
- (void)tick:(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;
  NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
}

假如只是简略的监测,运用FPS足够了,其原理仍然是监测主线程的卡顿导致runloop调用tick的次数削减,来判别是否掉帧。

runloop监测

监控主线程RunLoop原理,由于卡顿的是业务,而业务是交由主线程RunLoop处理的.

完成思路:检测主线程每次履行音讯循环的时间,当这个时间大于规则的阈值时,就记为发生了一次卡顿。这个也是微信卡顿三方matrix的原理

能够直接写一个进行RunLoop监控

@interface VTBlockMonitor (){
  CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation VTBlockMonitor
+ (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) {
  VTBlockMonitor *monitor = (__bridge VTBlockMonitor *)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(@"检测到超越两次接连卡顿 - %lu",(unsigned long)self->_timeoutCount);
        }
      }
      self->_timeoutCount = 0;
    }
  });
}
@end

也能够直接运用三方库

  • Swift的卡顿检测第三方ANREye,其主要思路是:创立子线程进行循环监测,每次检测时设置符号置为true,然后派发使命到主线程,符号置为false,接着子线程睡眠超越阈值时,判别符号是否为false,假如没有,阐明主线程发生了卡顿

  • OC能够运用微信matrix、滴滴DoraemonKit

界面优化

CPU层面的优化
  1. 尽量用轻量级的目标替代重量级的目标,能够对功能有所优化,例如 不需求相应触摸事件的控件,用CALayer替代UIView

  2. 尽量削减对UIViewCALayer的特点修改

    • CALayer内部并没有特点,当调用特点办法时,其内部是经过运行时resolveInstanceMethod为目标暂时增加一个办法,并将对应特点值保存在内部的一个Dictionary中,同时还会告诉delegate、创立动画等,十分耗时

    • UIView相关的显现特点,例如frame、bounds、transform等,实际上都是从CALayer映射来的,对其进行调整时,耗费的资源比一般特点要大

  3. 当有很多目标开释时,也是十分耗时的,尽量挪到后台线程去开释

  4. 尽量提前核算视图布局,即预排版,例如cell的行高

  5. Autolayout在简略页面情况下们能够很好的提升开发功率,但是对于复杂视图而言,会产生严峻的功能问题,跟着视图数量的增加,Autolayout带来的CPU耗费是呈指数上升的,所以尽量运用代码布局.

  6. 文本处理的优化:当一个界面有很多文本时,其行高的核算、制作也是十分耗时的

    • 假如对文本没有特殊要求,能够运用UILabel内部的完成办法,且需求放到子线程中进行,防止阻塞主线程
      • 核算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
      • 文本制作:[NSAttributedString drawWithRect:options:context:]
    • 自定义文本控件,运用TextKit 或最底层的 CoreText 对文本异步制作.而且CoreText 目标创立好后,能直接获取文本的宽高等信息,防止了多次核算(调整和制作都需求核算一次),CoreText直接运用了CoreGraphics占用内存小,功率高
  7. 图片处理(解码 + 制作)

    • 当运用UIImageCGImageSource 的办法创立图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU烘托前,CGImage中的数据才进行解码)。这一步是无可防止的,且是发生在主线程中的。想要绕开这个机制,常见的做法是在子线程中先将图片制作到CGBitmapContext,然后从Bitmap 直接创立图片,例如SDWebImage三方框架中对图片编解码的处理。这便是Image的预解码
    • 当运用CG最初的办法制作图画到画布中,然后从画布中创立图片时,能够将图画的制作子线程中进行
  8. 图片优化

    • 尽量运用PNG图片,不运用JPGE图片
    • 经过子线程预解码,主线程烘托,即经过Bitmap创立图片,在子线程赋值image
    • 优化图片大小,尽量防止动态缩放
    • 尽量将多张图合为一张进行显现
  9. 尽量防止运用通明view,由于运用通明view,会导致在GPU中核算像素时,会将通明view下层图层的像素也核算进来,即色彩混合处理,或许触发离屏烘托,默许的圆角,暗影,裁切等操作也会触发离屏烘托

  10. 按需加载,例如在TableView中滑动时不加载图片,运用默许占位图,而是在滑动中止时加载,[self performSelector: withObject: afterDelay: inModes:NSDefaultRunLoopMode];

  11. 少运用addViewcell动态增加view

GPU层面优化

相对于CPU而言,GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并烘托,输出到屏幕上.

  1. 尽量削减在短时间内很多图片的显现,尽或许将多张图片合为一张显现,主要是由于当有很多图片进行显现时,无论是CPU的核算还是GPU的烘托,都是十分耗时的,很或许呈现掉帧的情况
  2. 尽量防止图片的尺度超越40964096,由于当图片超越这个尺度时,会先由CPU进行预处理,然后再提交给GPU处理,导致额定CPU资源耗费
  3. 尽量削减视图数量和层次,主要是由于视图过多且堆叠时,GPU会将其混合,混合的进程也是十分耗时的
  4. 尽量防止离屏烘托,layer.background,圆角,暗影,裁切等
  5. 异步烘托,例如能够将cell中的所有控件. 视图合成一张图片进行显现,重写drawLayer: inContext:和displayLayer:进行异步制作.能够参考Graver三方框架