iOS開(kāi)發(fā)底層探索界面優(yōu)化示例詳解
1、卡頓原理
1.1、界面顯示原理
- CPU:Layout UI布局、文本計(jì)算、Display繪制、Prepare圖片解碼、Commit提交位圖給 GPU
- GPU:用于渲染,將結(jié)果放入 FrameBuffer
- FrameBuffer:幀緩沖
- Video Controller:根據(jù)Vsync(垂直同步)信號(hào),逐行讀取 FrameBuffer 中的數(shù)據(jù),經(jīng)過(guò)數(shù)模轉(zhuǎn)換傳遞給 Monitor
- Monitor:顯示器,用于顯示;對(duì)于顯示模塊來(lái)說(shuō),會(huì)按照手機(jī)刷新率以固定的頻率:1 / 刷新率 向 FrameBuffer 索要數(shù)據(jù),這個(gè)索要數(shù)據(jù)的命令就是 垂直同步信號(hào)Vsync(低刷60幀為16.67毫秒,高刷120幀為 8.33毫秒,下邊舉例主要以低刷16.67毫秒為主)
1.2、界面撕裂
顯示端每16.67ms從 FrameBuffer(幀緩存區(qū))讀取一幀數(shù)據(jù),如果遇到耗時(shí)操作交付不了,那么當(dāng)前畫面就還是舊一幀的畫面,但顯示過(guò)程中,下一幀數(shù)據(jù)準(zhǔn)備完畢,導(dǎo)致部分顯示的又是新數(shù)據(jù),這樣就會(huì)造成屏幕撕裂
1.3、界面卡頓
為了解決界面撕裂,蘋果使用雙緩沖機(jī)制 + 垂直同步信號(hào),使用 2個(gè)FrameBuffer 存儲(chǔ) GPU 處理結(jié)果,顯示端交替從這2個(gè)FrameBuffer中讀取數(shù)據(jù),一個(gè)被讀取時(shí)另一個(gè)去緩存;但解決界面撕裂的問(wèn)題也帶來(lái)了新的問(wèn)題:掉幀
如果遇到畫面帶馬賽克等情況,導(dǎo)致GPU渲染能力跟不上,會(huì)有2種掉幀情況;
如圖,F(xiàn)rameBuffer2 未渲染完第2幀,下一個(gè)16.67ms去 FrameBuffer1 中拿第3幀:
- 掉幀情況1:第3幀渲染完畢,接下來(lái)需要第4幀,第2幀被丟棄
- 掉幀情況2:第3幀未渲染完,再一個(gè)16.67ms去 FrameBuffer2 拿到第2幀,但第1幀多停留了16.67*2毫秒
小結(jié)
固定的時(shí)間間隔會(huì)收到垂直同步信號(hào)(Vsync),如果 CPU 和 GPU 還沒(méi)有將下一幀數(shù)據(jù)放到對(duì)應(yīng)的幀 FrameBuffer緩沖區(qū),就會(huì)出現(xiàn) 掉幀
2、卡頓檢測(cè)
2.1、CADisplayLink
系統(tǒng)在每次發(fā)送 VSync 時(shí),就會(huì)觸發(fā)CADisplayLink,通過(guò)統(tǒng)計(jì)每秒發(fā)送 VSync 的數(shù)量來(lái)查看 App 的 FPS 是否穩(wěn)定
#import "ViewController.h" @interface ViewController () @property (nonatomic, strong) CADisplayLink *link; @property (nonatomic, assign) NSTimeInterval lastTime; // 每隔1秒記錄一次時(shí)間 @property (nonatomic, assign) NSUInteger count; // 記錄VSync1秒內(nèi)發(fā)送的數(shù)量 @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檢測(cè)
RunLoop 的退出和進(jìn)入實(shí)質(zhì)都是Observer的通知,我們可以監(jiān)聽(tīng)Runloop的狀態(tài),并在相關(guān)回調(diào)里發(fā)送信號(hào),如果在設(shè)定的時(shí)間內(nèi)能夠收到信號(hào)說(shuō)明是流暢的;如果在設(shè)定的時(shí)間內(nèi)沒(méi)有收到信號(hào),說(shuō)明發(fā)生了卡頓。
#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 : 優(yōu)先級(jí)最小 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; // 發(fā)送信號(hào) dispatch_semaphore_t semaphore = monitor->_semaphore; dispatch_semaphore_signal(semaphore); } - (void)startMonitor{ // 創(chuàng)建信號(hào) _semaphore = dispatch_semaphore_create(0); // 在子線程監(jiān)控時(shí)長(zhǎng) dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (YES) { // 超時(shí)時(shí)間是 1 秒,沒(méi)有等到信號(hào)量,st 就不等于 0, RunLoop 所有的任務(wù) 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; } // 一秒左右的衡量尺度 很大可能性連續(xù)來(lái) 避免大規(guī)模打印! NSLog(@"檢測(cè)到超過(guò)兩次連續(xù)卡頓"); } } self->_timeoutCount = 0; } }); } @end
- 主線程監(jiān)聽(tīng) kCFRunLoopBeforeSources(即將處理事件)和kCFRunLoopAfterWaiting(即將休眠),子線程監(jiān)控時(shí)長(zhǎng),若連續(xù)兩次 1秒 內(nèi)沒(méi)有收到信號(hào),說(shuō)明發(fā)生了卡頓
2.3、微信matrix
- 微信的matrix也是借助 runloop 實(shí)現(xiàn),大體流程與上面 Runloop 方式相同,它使用退火算法優(yōu)化捕獲卡頓的效率,防止連續(xù)捕獲相同的卡頓,并且通過(guò)保存最近的20個(gè)主線程堆棧信息,獲取最近最耗時(shí)堆棧
2.4、滴滴DoraemonKit
- DoraemonKit的卡頓檢測(cè)方案不使用 RunLoop,它也是while循環(huán)中根據(jù)一定的狀態(tài)判斷,通過(guò)主線程中不斷發(fā)送信號(hào)semaphore,循環(huán)中等待信號(hào)的時(shí)間為5秒,等待超時(shí)則說(shuō)明主線程卡頓,并進(jìn)行相關(guān)上報(bào)
3、優(yōu)化方法
平時(shí)簡(jiǎn)單的方案有:
- 避免使用 透明UIView
- 盡量使用PNG圖片
- 避免離屏渲染(圓角使用貝塞爾曲線等)
3.1、預(yù)排版
- 就是常規(guī)的在Model層請(qǐng)求數(shù)據(jù)后提前將cell高度算好
3.2、預(yù)編碼 / 解碼
UIImage 是一個(gè)Model,二進(jìn)制流數(shù)據(jù) 存儲(chǔ)在DataBuffer中,經(jīng)過(guò)decode解碼,加載到imageBuffer中,最終進(jìn)入FrameBuffer才能被渲染
- 當(dāng)使用 UIImage 或CGImageSource的方法創(chuàng)建圖片時(shí),圖片的數(shù)據(jù)不會(huì)立即解碼,而是在設(shè)置UIImageView.image時(shí)解碼
- 將圖片設(shè)置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的數(shù)據(jù)才進(jìn)行解碼
- 如果任由系統(tǒng)處理,這一步則無(wú)法避免,并且會(huì)發(fā)生在主線程中。如果想避免這個(gè)機(jī)制,在子線程先將圖片繪制到CGBitmapContext,然后從Bitmap中創(chuàng)建圖片
3.3、按需加載
如果目標(biāo)行與當(dāng)前行相差超過(guò)指定行數(shù),只加載目標(biāo)滾動(dòng)范圍的前后指定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]; } }
在滑動(dòng)結(jié)束時(shí)進(jìn)行 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]; } //用戶觸摸時(shí)第一時(shí)間加載內(nèi)容 - (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]; } } }
- 這種方式會(huì)導(dǎo)致滑動(dòng)時(shí)有空白內(nèi)容,因此要做好占位內(nèi)容
3.4、異步渲染
- 異步渲染 就是在子線程把需要繪制的圖形提前處理好,然后將處理好的圖像數(shù)據(jù)直接返給主線程使用
- 異步渲染操作的是layer層,將多層堆疊的控件們通過(guò)UIGraphics畫成一張位圖,然后展示在layer.content上
3.4.1、CALayer
- CALayer基于CoreAnimation進(jìn)而基于QuartzCode,只負(fù)責(zé)顯示,且顯示的是位圖,不能處理用戶的觸摸事件
- 不需要與用戶交互時(shí),使用 UIView 和 CALayer 都可以,甚至 CALayer 更簡(jiǎn)潔高效
3.4.2、異步渲染實(shí)現(xiàn)
- 異步渲染的框架推薦:Graver、YYAsyncLayer
- CALayer 在調(diào)用display方法后回去調(diào)用繪制相關(guān)的方法,繪制會(huì)執(zhí)行drawRect:方法
簡(jiǎn)單例子
繼承 CALayer
#import "LZLayer.h" @implementation LZLayer //前面斷點(diǎn)調(diào)用寫下的代碼 - (void)layoutSublayers{ if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) { //UIView [self.delegate layoutSublayersOfLayer:self]; }else{ [super layoutSublayers]; } } //繪制流程的發(fā)起函數(shù) - (void)display{ // Graver 實(shí)現(xiàn)思路 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(額外的存儲(chǔ)區(qū)域產(chǎn)于的) -- 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{ //繪制的準(zhǔn)備工作,do nontihing } //繪制的操作 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{ [super drawLayer:layer inContext:ctx]; // 畫個(gè)不規(guī)則圖形 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); // 畫個(gè)紅色方塊 [[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(); }
控件們被繪制成了一張圖
此外,雖然將控件畫到一張位圖上,但是還有問(wèn)題,就是控件的交互事件,內(nèi)容較多建議鉆研一下graver的源碼
以上就是iOS開(kāi)發(fā)底層探索界面優(yōu)化示例詳解的詳細(xì)內(nèi)容,更多關(guān)于iOS開(kāi)發(fā)界面優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IOS點(diǎn)擊按鈕隱藏狀態(tài)欄詳解及實(shí)例代碼
這篇文章主要介紹了IOS點(diǎn)擊按鈕隱藏狀態(tài)欄詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02詳解Swift 利用Opration和OprationQueue來(lái)下載網(wǎng)絡(luò)圖片
這篇文章主要介紹了詳解Swift 利用Opration和OprationQueue來(lái)下載網(wǎng)絡(luò)圖片的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09ios實(shí)現(xiàn)簡(jiǎn)易隊(duì)列
這篇文章主要為大家詳細(xì)介紹了ios實(shí)現(xiàn)簡(jiǎn)易隊(duì)列,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02淺談iOS開(kāi)發(fā)如何適配暗黑模式(Dark Mode)
這篇文章主要介紹了淺談iOS開(kāi)發(fā)如何適配暗黑模式(Dark Mode),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09ios 實(shí)現(xiàn)倒計(jì)時(shí)的兩種方式
這篇文章主要介紹了ios實(shí)現(xiàn)倒計(jì)時(shí)的兩種方式,第一種方式使用NSTimer來(lái)實(shí)現(xiàn),第二種方式使用GCD來(lái)實(shí)現(xiàn)。具體內(nèi)容詳情大家參考下本文2017-01-01iOS實(shí)現(xiàn)翻頁(yè)效果動(dòng)畫實(shí)例代碼
本篇文章主要介紹了iOS實(shí)現(xiàn)翻頁(yè)效果動(dòng)畫實(shí)例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05