網站首頁 編程語言 正文
1、卡頓原理
1.1、界面顯示原理
- CPU:Layout UI布局、文本計算、Display繪制、Prepare圖片解碼、Commit提交位圖給 GPU
- GPU:用于渲染,將結果放入 FrameBuffer
- FrameBuffer:幀緩沖
- Video Controller:根據Vsync(垂直同步)信號,逐行讀取 FrameBuffer 中的數據,經過數模轉換傳遞給 Monitor
- Monitor:顯示器,用于顯示;對于顯示模塊來說,會按照手機刷新率以固定的頻率:1 / 刷新率 向 FrameBuffer 索要數據,這個索要數據的命令就是 垂直同步信號Vsync(低刷60幀為16.67毫秒,高刷120幀為 8.33毫秒,下邊舉例主要以低刷16.67毫秒為主)
1.2、界面撕裂
顯示端每16.67ms從 FrameBuffer(幀緩存區)讀取一幀數據,如果遇到耗時操作交付不了,那么當前畫面就還是舊一幀的畫面,但顯示過程中,下一幀數據準備完畢,導致部分顯示的又是新數據,這樣就會造成屏幕撕裂
1.3、界面卡頓
為了解決界面撕裂,蘋果使用雙緩沖機制 + 垂直同步信號,使用 2個FrameBuffer 存儲 GPU 處理結果,顯示端交替從這2個FrameBuffer中讀取數據,一個被讀取時另一個去緩存;但解決界面撕裂的問題也帶來了新的問題:掉幀
如果遇到畫面帶馬賽克等情況,導致GPU渲染能力跟不上,會有2種掉幀情況;
如圖,FrameBuffer2 未渲染完第2幀,下一個16.67ms去 FrameBuffer1 中拿第3幀:
- 掉幀情況1:第3幀渲染完畢,接下來需要第4幀,第2幀被丟棄
- 掉幀情況2:第3幀未渲染完,再一個16.67ms去 FrameBuffer2 拿到第2幀,但第1幀多停留了16.67*2毫秒
小結
固定的時間間隔會收到垂直同步信號(Vsync),如果 CPU 和 GPU 還沒有將下一幀數據放到對應的幀 FrameBuffer緩沖區,就會出現 掉幀
2、卡頓檢測
2.1、CADisplayLink
系統在每次發送 VSync 時,就會觸發CADisplayLink,通過統計每秒發送 VSync 的數量來查看 App 的 FPS 是否穩定
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, assign) NSTimeInterval lastTime; // 每隔1秒記錄一次時間
@property (nonatomic, assign) NSUInteger count; // 記錄VSync1秒內發送的數量
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkAction:)];
[_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)linkAction: (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;
NSLog(@"?? FPS : %f ", fps);
}
@end
2.2、RunLoop檢測
RunLoop 的退出和進入實質都是Observer的通知,我們可以監聽Runloop的狀態,并在相關回調里發送信號,如果在設定的時間內能夠收到信號說明是流暢的;如果在設定的時間內沒有收到信號,說明發生了卡頓。
#import "LZBlockMonitor.h"
@interface LZBlockMonitor (){
CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LZBlockMonitor
+ (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];
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
//NSIntegerMax : 優先級最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LZBlockMonitor *monitor = (__bridge LZBlockMonitor *)info;
monitor->activity = activity;
// 發送信號
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (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(@"檢測到超過兩次連續卡頓");
}
}
self->_timeoutCount = 0;
}
});
}
@end
- 主線程監聽 kCFRunLoopBeforeSources(即將處理事件)和kCFRunLoopAfterWaiting(即將休眠),子線程監控時長,若連續兩次 1秒 內沒有收到信號,說明發生了卡頓
2.3、微信matrix
- 微信的matrix也是借助 runloop 實現,大體流程與上面 Runloop 方式相同,它使用退火算法優化捕獲卡頓的效率,防止連續捕獲相同的卡頓,并且通過保存最近的20個主線程堆棧信息,獲取最近最耗時堆棧
2.4、滴滴DoraemonKit
- DoraemonKit的卡頓檢測方案不使用 RunLoop,它也是while循環中根據一定的狀態判斷,通過主線程中不斷發送信號semaphore,循環中等待信號的時間為5秒,等待超時則說明主線程卡頓,并進行相關上報
3、優化方法
平時簡單的方案有:
- 避免使用 透明UIView
- 盡量使用PNG圖片
- 避免離屏渲染(圓角使用貝塞爾曲線等)
3.1、預排版
- 就是常規的在Model層請求數據后提前將cell高度算好
3.2、預編碼 / 解碼
UIImage 是一個Model,二進制流數據 存儲在DataBuffer中,經過decode解碼,加載到imageBuffer中,最終進入FrameBuffer才能被渲染
- 當使用 UIImage 或CGImageSource的方法創建圖片時,圖片的數據不會立即解碼,而是在設置UIImageView.image時解碼
- 將圖片設置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的數據才進行解碼
- 如果任由系統處理,這一步則無法避免,并且會發生在主線程中。如果想避免這個機制,在子線程先將圖片繪制到CGBitmapContext,然后從Bitmap中創建圖片
3.3、按需加載
如果目標行與當前行相差超過指定行數,只加載目標滾動范圍的前后指定3行
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
[needLoadArr removeAllObjects];
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if (labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
if (velocity.y<0) {
NSIndexPath *indexPath = [temp lastObject];
if (indexPath.row+3<datas.count) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
}
} else {
NSIndexPath *indexPath = [temp firstObject];
if (indexPath.row>3) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
在滑動結束時進行 Cell 的渲染
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
scrollToToping = YES;
return YES;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
scrollToToping = NO;
[self loadContent];
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
scrollToToping = NO;
[self loadContent];
}
//用戶觸摸時第一時間加載內容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!scrollToToping) {
[needLoadArr removeAllObjects];
[self loadContent];
}
return [super hitTest:point withEvent:event];
}
- (void)loadContent{
if (scrollToToping) {
return;
}
if (self.indexPathsForVisibleRows.count<=0) {
return;
}
if (self.visibleCells && self.visibleCells.count>0) {
for (id temp in [self.visibleCells copy]) {
VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
[cell draw];
}
}
}
- 這種方式會導致滑動時有空白內容,因此要做好占位內容
3.4、異步渲染
- 異步渲染 就是在子線程把需要繪制的圖形提前處理好,然后將處理好的圖像數據直接返給主線程使用
- 異步渲染操作的是layer層,將多層堆疊的控件們通過UIGraphics畫成一張位圖,然后展示在layer.content上
3.4.1、CALayer
- CALayer基于CoreAnimation進而基于QuartzCode,只負責顯示,且顯示的是位圖,不能處理用戶的觸摸事件
- 不需要與用戶交互時,使用 UIView 和 CALayer 都可以,甚至 CALayer 更簡潔高效
3.4.2、異步渲染實現
- 異步渲染的框架推薦:Graver、YYAsyncLayer
- CALayer 在調用display方法后回去調用繪制相關的方法,繪制會執行drawRect:方法
簡單例子
繼承 CALayer
#import "LZLayer.h"
@implementation LZLayer
//前面斷點調用寫下的代碼
- (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
繼承 UIView
// - (CGContextRef)createContext 和 - (void)closeContext要在.h中聲明
#import "LZView.h"
#import "LZLayer.h"
@implementation LZView
- (void)drawRect:(CGRect)rect {
// Drawing code, 繪制的操作, BackingStore(額外的存儲區域產于的) -- GPU
}
//子視圖的布局
- (void)layoutSubviews{
[super layoutSubviews];
}
+ (Class)layerClass{
return [LZLayer 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];
// 畫個不規則圖形
CGContextMoveToPoint(ctx, self.bounds.size.width / 2- 20, 20);
CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 20, 20);
CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 40, 80);
CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 40, 100);
CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 20, 20);
CGContextSetFillColorWithColor(ctx, UIColor.magentaColor.CGColor);
CGContextSetStrokeColorWithColor(ctx, UIColor.magentaColor.CGColor); // 描邊
CGContextDrawPath(ctx, kCGPathFillStroke);
// 畫個紅色方塊
[[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);
// 文字
[@"LZ" drawInRect:CGRectMake(self.bounds.size.width / 2 - 40, 100, 80, 24) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:20],NSForegroundColorAttributeName: UIColor.blueColor}];
// 圖片
[[UIImage imageWithContentsOfFile:@"/Volumes/Disk_D/test code/Test/Test/yasuo.png"] drawInRect:CGRectMake(10, self.bounds.size.height/2, self.bounds.size.width - 20, self.bounds.size.height/2 -10)];
}
//layer.contents = (位圖)
- (void)displayLayer:(CALayer *)layer{
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
layer.contents = (__bridge id)(image.CGImage);
});
}
- (void)closeContext{
UIGraphicsEndImageContext();
}
原文鏈接:https://juejin.cn/post/7113799052122128392
相關推薦
- 2023-01-23 Python列表對象中元素的刪除操作方法_python
- 2022-07-08 Python如何通過地址獲取變量_python
- 2022-08-19 Linux系統文件目錄介紹
- 2022-01-19 解決element-ui 表格分頁序號不遞增問題。
- 2022-09-29 Python組合數據類型詳解_python
- 2022-03-28 c語言for、while和do-while循環之間的區別_C 語言
- 2022-06-15 C語言詳解實現字符菱形的方法_C 語言
- 2022-09-27 Kotlin淺析延遲初始化與密封類的實現方法_Android
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支