iOS 底层原理:界面优化

2022-01-13  本文已影响0人  大菠萝_DABLO

界面优化无非就是解决卡顿问,优化界面流畅度,以下就通过先分析卡顿的原因,然后再介绍具体的优化方案,来分析如何做界面优化

  1. CPU主要是计算出需要渲染的模型数据
  2. GPU主要是根据 CPU提供的渲染模型数据渲染图片然后存到帧缓冲区
  3. 视频控制器冲帧缓冲区中读取数据最后成像
  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唤醒则会消耗性能。

  1. 通过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
  1. 微信matrix

此方案也是借助 runloop实现的大体流程和方案三相同,不过微信加入了堆栈分析,能够定位到耗时的方法调用堆栈,所以需要准确的分析卡顿原因可以借助微信matrix来分析卡顿。当然也可以在方案2中使用 PLCrashReporter这个开源的第三方库来获取堆栈信息

  1. 滴滴DoraemonKit

实现方案大概就是在子线程中一直 ping主线程,在主线程卡顿的情况下,会出现断在的无响应的表现,进而检测卡顿

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);
   });
  1. UIView是基于 UIKit框架的,能够接受点击事件,处理用户的触摸事件,并管理子视图
  2. CALayer是基于 CoreAnimation,而CoreAnimation是基于QuartzCode的。所以CALayer只负责显示,不能处理用户的触摸事件
  3. UIView是直接继承 UIResponder的,CALayer是继承 NSObject
  4. UIVIew 的主要职责是负责接收并响应事件;而 CALayer 的主要职责是负责显示 UIUIView 依赖于 CALayer 得以显示

总结:UIView主要负责时间处理,CALayer主要是视图显示 异步渲染的原理其实也就是在子线程将所有的视图绘制成一张位图,然后回到主线程赋值给 layercontents,例如 Graver框架的异步渲染流程如下:

图片

核心源码如下:

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
}

最终效果图如下:


图片

也可以使用 YYAsyncLayer

  1. 减少图层的层级
  2. 减少离屏渲染
  3. 图片显示的话图片的大小设置(不要太大)
  4. 少使用addViewcell动态添加view
  5. 尽量避免使用透明view,因为使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混合处理(当有两个图层的时候一个是半透明一个是不透明如果半透明的层级更高的话此时就会触发颜色混合,底层的混合并不是仅仅的将两个图层叠加而是会将两股颜色混合计算出新的色值显示在屏幕中)
上一篇下一篇

猜你喜欢

热点阅读