CPU负责计算 -- > GPU负责渲染 --> frameBuffer -- > video Controller --> Monitor
frameBuffer是存储帧缓存的,苹果是每隔60fps进行刷,有时无法同时刷到就会产生丢帧
在YYKit框架里有检测FPS的,其主要是使用了CADisplayLink这个,点击进去是其绑定了一个vsync信号,其实际为60pfs,16.67ms,其实其是被绑定在link上面然后加入到一个runloop里。这里用到了YYWeakProxy是为了弱引用,添加中间变量,起到消息转发,防止循环引用。
@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];
//消息快速转发
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
//消息慢速转发
- (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++执行次数少了,那么就说明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这两种状态可以检测。首先就是获取当前线程的runloop状态,因为这个是主线程,应该是一直都会运行的,如果没运行的话在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting状态就说明线程阻塞了。
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
NSLog(@"检测到超过两次连续卡顿 - %lu",(unsigned long)self->_timeoutCount);
}
图片加载流程
1)预排版
就是先弄个framelayout进行先预排版
2)预编码/解码
图片加载流程
Data Buffer --->decoee-->Image Buffer --->Frame Buffer
一般采集到data后,通过开启子线程进行预解码操作(也就是把Data变成imageREF)
下采样可以优化内存:
- (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;
}
未使用下采样加载一个内存30M图片,消耗内存20.3MB如下
使用下采样加载一个内存30M图片,消耗内存13.6MB如下
3.按需加载
就是在滚动的时候不加载,停止后才进行加载
4.异步渲染
UIView是视图的显示和layer是用来渲染图层的,用drawRect操作,第三方框架Graver. 绘制到一张位图上面显示
- (void)drawRect:(CGRect)rect {
// Drawing code, 绘制的操作, BackingStore(额外的存储区域产于的) -- GPU
}
我们再drawRect这里打个断点,然后进行bt指令调试,我们看到了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物理空间,但是他的优化也是很明显的。
为什么UI的渲染要在主线程,首先主线程比较安全,开发无法修改,在苹果底层是通过懒加载去操作的,主线程也是运行最快的,如果渲染放在异步那就会紊乱,一个这个界面中控件显示,另外一个显示其他的。