IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> iOS界面卡顿原理及优化 -> 正文阅读

[移动开发]iOS界面卡顿原理及优化

在日常开发中,我们最多遇到的就是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是否卡顿。

这里借鉴了YYKitYYFPSLabel运行代码如图:

创建一个工程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界面的高效渲染


?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-07-20 19:00:48  更:2022-07-20 19:01:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 3:50:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码