在日常开发中,咱们最多遇到的便是UI制作,内容展现等需求的开发,APP的UI展现是否流通,也是用户最直接的感触。今天就针对UI界面卡顿原理进行剖析,怎样优化进行讨论。

一. 卡顿原理

核算机正常的烘托流通:

iOS界面卡顿原理及优化
经过CPU核算GPU生成FrameBuffer在进行Video Controller 显现到显现器上(monitor) 优化功能后:
iOS界面卡顿原理及优化
在原理基础上增加了一个buffer缓冲区,显现刷帧率60fps/120fps 来回在两个缓冲区取帧。

卡顿原因:如果GPU在某一帧生成中生成不及时,显现取帧时就会在buffer 1 和 buffer 2 来回跑,等待这一帧的生成。这时候就会产生界面UI卡顿。如果这帧没有生成,下一帧生成了就会直接越过这一帧显现下一帧(这便是掉帧状况)。

核心问题

  • 已经知道卡顿的原因
  • 怎样监测卡顿呢
  • 有哪些办法可以监测卡顿呢

1、卡顿检测

用什么来监测卡顿呢,咱们这儿用到RunLoop。咱们知道RunLoop是主运转循环,可以保存使命的生命周期。(60FPS=16.67ms=1/60)

咱们大致了解一下RunLoop:

iOS界面卡顿原理及优化
首要思路便是,添加监测使命到RunLoop中来监测Vsync(垂直同步信号)来判别UI是否卡顿。

这儿学习了YYKitYYFPSLabel运转代码如图:

iOS界面卡顿原理及优化
创立一个工程NYMainThreadBlock:来感触监测卡顿的核心思维;
iOS界面卡顿原理及优化
注册自定义observer使命到runloop而且发送信号量->NYBlockMonitor中,NYBlockMonitor有一个子线程无限循环->等待判别自定义observer的休眠,唤醒的信号量,用来评判整个体系runloop的作业状况,由于UI的烘托作业也在runloop的体系使命中,其他优先级高的使命占用很多runloop的运转时刻,咱们自定义的observer使命就会产生等待休眠,这样咱们就可以判别出UI是否卡顿。(如上图所示)

核心代码:

// NYBlockMonitor.m
// NYMainThreadBlock
//
// Created by ning on 2022/7/12.
//
#import "NYBlockMonitor.h"
@interface NYBlockMonitor(){
  CFRunLoopActivity acticity; //状况集
}
@property (nonatomic,strong) dispatch_semaphore_t semaphore;
@property (nonatomic,assign) NSUInteger timeoutCount;
@end
@implementation NYBlockMonitor
+ (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 CallBock(CFRunLoopObserverRef observer,CFRunLoopActivity activity,void *info)
{
  NYBlockMonitor *monitor = (__bridge NYBlockMonitor *)info;
  monitor->acticity = activity;
  //发送信号
  dispatch_semaphore_t semaphore = monitor->_semaphore;
  dispatch_semaphore_signal(semaphore); //信号+1
}
- (void)registerObserver
{
  CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
  //NSIntegerMax :优先级最小
  CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                              kCFRunLoopAllActivities,
                              YES,
                              NSIntegerMax,
                              &CallBock,
                              &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) {
        // 行将处理 Source , 刚从休眠中唤醒 进入判别
        if (self->acticity == kCFRunLoopBeforeSources || self->acticity == kCFRunLoopAfterWaiting) {
          if (++self->_timeoutCount < 2) {
            NSLog(@"timeoutCount=%lu",(unsigned long)self->_timeoutCount);
            continue;
          }
          // 一秒左右的衡量标准 很大可能性接连来 防止大规模打印!
          NSLog(@"检测到超越两次接连卡顿 - %ld",(unsigned long)self->_timeoutCount);
        }
      }
      self->_timeoutCount = 0;
    }
  });
}
@end

运转效果:

iOS界面卡顿原理及优化

二. 界面优化

1、预排版

惯例MVC模式中,有可能在view层中核算frame的巨细,及相关UI的size。这在 UI显现熏染中会损耗功能。怎样解决这一问题?便是把view的巨细及排版归类到model中在子线程中就把view的排版核算好了,这样可以减少UI view 的烘托损耗。

上一段小代码:

@implementation NYTimeLineCellLayout
- (instancetype)initWithModel:(LGTimeLineModel *)timeLineModel
{
  if (!timeLineModel) return nil;
  self = [super init];
  if (self) {
    _timeLineModel = timeLineModel;
    [self layout];
  }
  return self;
}
- (void)setTimeLineModel:(LGTimeLineModel *)timeLineModel
{
  _timeLineModel = timeLineModel;
  [self layout];
}
- (void)layout
{
  CGFloat sWidth = [UIScreen mainScreen].bounds.size.width;
  self.iconRect = CGRectMake(10, 10, 45, 45);
  CGFloat nameWidth = [self calcWidthWithTitle:_timeLineModel.name font:titleFont];
  CGFloat nameHeight = [self calcLabelHeight:_timeLineModel.name fontSize:titleFont width:nameWidth];
  self.nameRect = CGRectMake(CGRectGetMaxX(self.iconRect) + nameLeftSpaceToHeadIcon, 17, nameWidth, nameHeight);
  CGFloat msgWidth = sWidth - 10 - 16;
  CGFloat msgHeight = 0;
  //文本信息高度核算
 //**********************省掉代码***********************//
  self.height = CGRectGetMaxY(self.seperatorViewRect);
}
#pragma mark **-- Caculate Method**
- (CGFloat)calcWidthWithTitle:(NSString *)title font:(CGFloat)font {
  NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
  CGRect rect = [title boundingRectWithSize:CGSizeMake(MAXFLOAT,MAXFLOAT) options:options attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:font]} context:nil];
  CGFloat realWidth = ceilf(rect.size.width);
  return realWidth;
}
- (CGFloat)calcLabelHeight:(NSString *)str fontSize:(CGFloat)fontSize width:(CGFloat)width {
  NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
  CGRect rect = [str boundingRectWithSize:CGSizeMake(width,MAXFLOAT) options:options attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize]} context:nil];
  CGFloat realHeight = ceilf(rect.size.height);
  return realHeight;
}
- (int)caculateAttributeLabelHeightWithString:(NSAttributedString *)string width:(int)width {
  int total_height = 0;
  CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);  //string 为要核算高度的NSAttributedString
  CGRect drawingRect = CGRectMake(0, 0, width, 100000); //这儿的高要设置足够大
  CGMutablePathRef path = CGPathCreateMutable();
  CGPathAddRect(path, NULL, drawingRect);
  CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0), path, NULL);
  CGPathRelease(path);
  CFRelease(framesetter);
  NSArray *linesArray = (NSArray *) CTFrameGetLines(textFrame);
  CGPoint origins[[linesArray count]];
  CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
  int line_y = (int) origins[[linesArray count] -1].y; //最终一行line的原点y坐标
  CGFloat ascent;
  CGFloat descent;
  CGFloat leading;
  CTLineRef line = (__bridge CTLineRef) [linesArray objectAtIndex:[linesArray count]-1];
  CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  total_height = 100000 - line_y + (int) descent +1;  //+1为了纠正descent转换成int小数点后舍去的值
  CFRelease(textFrame);
  return total_height;
}
@end
//TableViewCell 添加配置NYTimeLineCellLayout 办法
- (void)configureLayout:(NYTimeLineCellLayout *)layout{
//**********************省掉代码***********************//
}

这样就到达预排版的意图了(挺简单的手段,咱们都能想到吧)。

2、预编码解码

现在项目开发中,还有一种状况会对UI熏染功能造成耗费。便是图片的加载,为什么图片会对体系造成担负呢,要怎样减少图片加载带来的过多耗费呢?

UIImage *image = [UIImage imageWithContentsOfFile:@"/xxxxx.png"];
self.kcImageView.image = image;

运转项目,检查占用内存状况。

iOS界面卡顿原理及优化
可实践图片巨细是31.4MB
iOS界面卡顿原理及优化
如果改为如下代码(苹果官方文档的下采样方式):

// Objective-C: 大图缩小为显现尺度的图
- (UIImage *)downsampleImageAt:(NSURL *)imageURL to:(CGSize)pointSize scale:(CGFloat)scale {
  // 利用图画文件地址创立 image source
  NSDictionary *imageSourceOptions = @{(__bridge NSString *)kCGImageSourceShouldCache: @NO // 原始图画不要解码
  };
  CGImageSourceRef imageSource =
  CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, (__bridge CFDictionaryRef)imageSourceOptions);
  // 下采样
  CGFloat maxDimensionInPixels = MAX(pointSize.width, pointSize.height) * scale;
  NSDictionary *downsampleOptions =
  @{
   (__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
   (__bridge NSString *)kCGImageSourceShouldCacheImmediately: @YES, // 缩小图画的同时进行解码
   (__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform: @YES,
   (__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize: @(maxDimensionInPixels)
   };
  CGImageRef downsampledImage =
  CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
  UIImage *image = [[UIImage alloc] initWithCGImage:downsampledImage];
  CGImageRelease(downsampledImage);
  CFRelease(imageSource);
  return image;
}

运转效果:

iOS界面卡顿原理及优化
经过下采样解码减少体系对图片加载的耗费。

3、异步烘托

异步烘托是什么意思呢,异步烘托做了什么呢?咱们经过一个事例来研究了解一下:

iOS界面卡顿原理及优化
运转项目发现,怎样只要一个图层。正常敞开咱们在view上创立多种控件,组成了某个界面然后每个控件都有自己的图层。而咱们的事例只要一层,这是为什么呢?咱们渐渐解开谜底。

运转项目检查仓库信息:

iOS界面卡顿原理及优化
咱们看到有用到图层的地方都会有CA::Transaction::commit()这样的代码。Transaction作了什么呢?

iOS中UIKit能显现内容首要依靠的结构如图:

iOS界面卡顿原理及优化
从Core Animation到GPU烘托过程:
iOS界面卡顿原理及优化

  • Application 中布局 UIKit 视图控件直接的关联Core Animation 图层
  • Core Animation 图层相关的数据提交到 iOS Render Server,即 OpenGL ES & Core Graphics
  • Render Server 将与 GPU通信把数据经过处理之后传递给 GPU
  • GPU 调用 iOS 当前设备烘托相关的图形设备 Display

Commit Transaction做了什么?

  • Layout构建视图frame,遍历的操作[UIView layerSubview],[CALayer layoutSubLayers]
  • Display制作视图,display – drawReact(),displayLyaer:(位图的制作)
  • Prepare,额定的 Core Animation 作业,比如解码
  • Commit,打包图层并将它们发送到 Render Server

代码:

@implementation NYView
- (void)drawRect:(CGRect)rect {
  // Drawing code, 制作的操作, BackingStore(额定的存储区域产于的) -- GPU
}
+ (Class)layerClass{
  return [NYLayer 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];
  [[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);
}
//layer.contents = (位图)
- (void)displayLayer:(CALayer *)layer{
  UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  dispatch_async(dispatch_get_main_queue(), {
    layer.contents = (__bridge id)(image.CGImage);
  });
}
- (void)closeContext{
  UIGraphicsEndImageContext();
}
@end
@implementation NYLayer
//前面断点调用写下的代码
- (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

运转效果:

iOS界面卡顿原理及优化
制作的次序: layoutSublayersOfLayer ->createContext-> layerWillDraw-> drawLayer-> displayLayer-> closeContext
iOS界面卡顿原理及优化
也可研究一下 美团开源Graver结构:用“雕琢”诠释iOS端UI界面的高效烘托