在日常开发中,咱们最多遇到的便是UI制作,内容展现等需求的开发,APP的UI展现是否流通,也是用户最直接的感触。今天就针对UI界面卡顿原理进行剖析,怎样优化进行讨论。
一. 卡顿原理
核算机正常的烘托流通:
经过CPU核算GPU生成FrameBuffer
在进行Video Controller
显现到显现器上(monitor)
优化功能后:
在原理基础上增加了一个buffer缓冲区,显现刷帧率60fps/120fps 来回在两个缓冲区取帧。
卡顿原因
:如果GPU在某一帧生成中生成不及时,显现取帧时就会在buffer 1 和 buffer 2 来回跑,等待这一帧的生成。这时候就会产生界面UI卡顿。如果这帧没有生成,下一帧生成了就会直接越过这一帧显现下一帧(这便是掉帧
状况)。
核心问题:
- 已经知道卡顿的原因
- 怎样监测卡顿呢
- 有哪些办法可以监测卡顿呢
1、卡顿检测
用什么来监测卡顿呢,咱们这儿用到RunLoop。咱们知道RunLoop是主运转循环
,可以保存使命的生命周期。(60FPS=16.67ms=1/60)
咱们大致了解一下RunLoop: 首要思路便是,添加监测使命到RunLoop中来监测Vsync(垂直同步信号)来判别UI是否卡顿。
这儿学习了YYKit
的YYFPSLabel
运转代码如图:
创立一个工程NYMainThreadBlock:来感触监测卡顿的核心思维;
注册自定义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
运转效果:
二. 界面优化
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;
运转项目,检查占用内存状况。 可实践图片巨细是31.4MB 如果改为如下代码(苹果官方文档的下采样方式):
// 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;
}
运转效果: 经过下采样解码减少体系对图片加载的耗费。
3、异步烘托
异步烘托是什么意思呢,异步烘托做了什么呢?咱们经过一个事例来研究了解一下: 运转项目发现,怎样只要一个图层。正常敞开咱们在view上创立多种控件,组成了某个界面然后每个控件都有自己的图层。而咱们的事例只要一层,这是为什么呢?咱们渐渐解开谜底。
运转项目检查仓库信息:
咱们看到有用到图层的地方都会有CA::Transaction::commit()
这样的代码。Transaction作了什么呢?
iOS中UIKit能显现内容首要依靠的结构如图: 从Core Animation到GPU烘托过程:
-
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
运转效果:
制作的次序:
layoutSublayersOfLayer
->createContext
-> layerWillDraw
-> drawLayer
-> displayLayer
-> closeContext
也可研究一下
美团开源Graver结构:用“雕琢”诠释iOS端UI界面的高效烘托