CPU負責計算 -- > GPU負責渲染 --> frameBuffer -- > video Controller --> Monitor
frameBuffer是存儲幀緩存的,蘋果是每隔60fps進行刷,有時無法同時刷到就會產(chǎn)生丟幀
在YYKit框架里有檢測FPS的,其主要是使用了CADisplayLink這個,點擊進去是其綁定了一個vsync信號,其實際為60pfs,16.67ms,其實其是被綁定在link上面然后加入到一個runloop里。這里用到了YYWeakProxy是為了弱引用,添加中間變量,起到消息轉發(fā),防止循環(huán)引用。
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
UIFont *_font;
UIFont *_subFont;
NSTimeInterval _llll;
}
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
//消息快速轉發(fā)
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
//消息慢速轉發(fā)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
/** Class representing a timer bound to the display vsync. **/
API_AVAILABLE(ios(3.1), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macOS)
@interface CADisplayLink : NSObject
{
@private
void *_impl;
}
在代碼中 CGFloat progress = fps / 60.0,這里fps當小于60,說明count++執(zhí)行次數(shù)少了,那么就說明mian(主線程)被卡頓了。
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;
CGFloat progress = fps / 60.0;
UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
[text setColor:color range:NSMakeRange(0, text.length - 3)];
[text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
text.font = _font;
[text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
Runloop檢測卡頓
線程和runloop是一一對應的


下面通過kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting這兩種狀態(tài)可以檢測。首先就是獲取當前線程的runloop狀態(tài),因為這個是主線程,應該是一直都會運行的,如果沒運行的話在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting狀態(tài)就說明線程阻塞了。
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
NSLog(@"檢測到超過兩次連續(xù)卡頓 - %lu",(unsigned long)self->_timeoutCount);
}
圖片加載流程
1)預排版
就是先弄個framelayout進行先預排版
2)預編碼/解碼
圖片加載流程
Data Buffer --->decoee-->Image Buffer --->Frame Buffer
一般采集到data后,通過開啟子線程進行預解碼操作(也就是把Data變成imageREF)
下采樣可以優(yōu)化內(nèi)存:
- (UIImage *)downsampleImageAt:(NSURL *)imageURL to:(CGSize)pointSize scale:(CGFloat)scale {
// 利用圖像文件地址創(chuàng)建 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;
}
未使用下采樣加載一個內(nèi)存30M圖片,消耗內(nèi)存20.3MB如下

使用下采樣加載一個內(nèi)存30M圖片,消耗內(nèi)存13.6MB如下

3.按需加載
就是在滾動的時候不加載,停止后才進行加載
4.異步渲染
UIView是視圖的顯示和layer是用來渲染圖層的,用drawRect操作,第三方框架Graver. 繪制到一張位圖上面顯示
- (void)drawRect:(CGRect)rect {
// Drawing code, 繪制的操作, BackingStore(額外的存儲區(qū)域產(chǎn)于的) -- GPU
}
我們再drawRect這里打個斷點,然后進行bt指令調(diào)試,我們看到了commit_transaction這個方法操作。那commit_transaction做了什么呢?

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001001e99f8 LGViewRenderExplore`-[LGView drawRect:](self=0x000000014ad08490, _cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 200, height = 200))) at LGView.m:20:1
frame #1: 0x00000001852e9748 UIKitCore`-[UIView(CALayerDelegate) drawLayer:inContext:] + 552
frame #2: 0x00000001001e9c48 LGViewRenderExplore`-[LGView drawLayer:inContext:](self=0x000000014ad08490, _cmd="drawLayer:inContext:", layer=0x0000600003065780, ctx=0x0000600000550480) at LGView.m:50:5
frame #3: 0x00000001001e9964 LGViewRenderExplore`-[LGLayer display](self=0x0000600003065780, _cmd="display") at LGLayer.m:33:5
frame #4: 0x000000018851e770 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 400
frame #5: 0x0000000188457538 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double, double*) + 448
frame #6: 0x0000000188483564 QuartzCore`CA::Transaction::commit() + 696
frame #7: 0x0000000184d991e8 UIKitCore`__34-[UIApplication _firstCommitBlock]_block_invoke_2 + 40
frame #8: 0x0000000180360580 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 20
frame #9: 0x000000018035f854 CoreFoundation`__CFRunLoopDoBlocks + 408
frame #10: 0x000000018035a018 CoreFoundation`__CFRunLoopRun + 764
frame #11: 0x0000000180359804 CoreFoundation`CFRunLoopRunSpecific + 572
frame #12: 0x000000018c23660c GraphicsServices`GSEventRunModal + 160
frame #13: 0x0000000184d7bd2c UIKitCore`-[UIApplication _run] + 992
frame #14: 0x0000000184d808c8 UIKitCore`UIApplicationMain + 112
frame #15: 0x00000001001e94b8 LGViewRenderExplore`main(argc=1, argv=0x000000016fc19c28) at main.m:17:12
frame #16: 0x0000000100409cd8 dyld_sim`start_sim + 20
frame #17: 0x00000001004990f4 dyld`start + 520

這種開異步渲染的話,會開啟多個線程,會占用cpu物理空間,但是他的優(yōu)化也是很明顯的。
為什么UI的渲染要在主線程,首先主線程比較安全,開發(fā)無法修改,在蘋果底層是通過懶加載去操作的,主線程也是運行最快的,如果渲染放在異步那就會紊亂,一個這個界面中控件顯示,另外一個顯示其他的。