界面优化无非就是解决卡顿问,优化界面流畅度,如下就经过先分析卡顿的缘由,而后再介绍具体的优化方案,来分析如何作界面优化微信
CPU
处理阶段 GPU
处理阶段和视频控制器显示阶段。
CPU
主要是计算出须要渲染的模型数据GPU
主要是根据 CPU
提供的渲染模型数据渲染图片而后存到帧缓冲区VSync
+ 双缓冲区的形式,就是显示器显示完成一帧的渲染的时候会向 发送一个垂直信号 VSync
,收到这个这个垂直信号以后显示器开始读取另一个帧缓冲区中的数据而 App
接到垂直信号以后开始新一帧的渲染。CPU
和 GPU
的工做尚未完成,也就是另一个帧缓冲区仍是加锁状态没有数据的时候,此时显示器显示的仍是上一帧的图像那么这种状况就会一直等待下一帧绘制完成而后视频控制器再读取另一个帧缓冲区中的数据而后成像,中间这个等待的过程就形成了掉帧,也就是会卡顿。60Hz
,也就是每秒中刷新屏幕60次,也就是每秒中有60帧渲染完成,差很少每帧渲染的时间是1000/60 = 16.67
毫秒整个界面会比较流畅,通常刷新率低于45Hz
的就会出现明显的卡顿现象。这里能够经过YYFPSLabel
来实现FPS
的监控,该原理主要是依靠 CADisplayLink
来实现的,经过CADisplayLink
来监听每次屏幕刷新并获取屏幕刷新的时间,而后使用次数(也就是1)除以每次刷新的时间间隔获得FPS
,具体源码以下: #import "YYFPSLabel.h"
#import "YYKit.h"
#define kSize CGSizeMake(55, 20)
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
UIFont *_font;
UIFont *_subFont;
NSTimeInterval _llll;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (frame.size.width == 0 && frame.size.height == 0) {
frame.size = kSize;
}
self = [super initWithFrame:frame];
self.layer.cornerRadius = 5;
self.clipsToBounds = YES;
self.textAlignment = NSTextAlignmentCenter;
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
_font = [UIFont fontWithName:@"Menlo" size:14];
if (_font) {
_subFont = [UIFont fontWithName:@"Menlo" size:4];
} else {
_font = [UIFont fontWithName:@"Courier" size:14];
_subFont = [UIFont fontWithName:@"Courier" size:4];
}
//YYWeakProxy 这里使用了虚拟类来解决强引用问题
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
return self;
}
- (void)dealloc {
[_link invalidate];
}
- (CGSize)sizeThatFits:(CGSize)size {
return kSize;
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
NSLog(@"sdf");
return;
}
//次数
_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)];
self.attributedText = text;
}
@end
复制代码
FPS
只用在开发阶段的辅助性的数值,由于他会频繁唤醒 runloop
若是 runloop
在闲置的状态被 CADisplayLink
唤醒则会消耗性能。Runloop
一次循环的时间来判断是否卡顿,这里须要配合使用 GCD
的信号量来实现,设置初始化信号量为0,而后开一个子线程等待信号量的触发,也是就是在子线程的方法里面调用 dispatch_semaphore_wait
方法设置等待时间是1秒,而后主线程的 Runloop
的 Observer
回调方法中发送信号也就是调用 dispatch_semaphore_signal
方法,此时时间能够置为0了,若是是等待时间超时则看此时的 Runloop
的状态是不是 kCFRunLoopBeforeSources
或者是 kCFRunLoopAfterWaiting
,若是在这两个状态下两秒则说明有卡顿,详细代码以下:(代码中也有相关的注释) #import "LGBlockMonitor.h"
@interface LGBlockMonitor (){
CFRunLoopActivity activity;
}
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;
@end
@implementation LGBlockMonitor
+ (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 CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// 建立信号c
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 全部的任务
// 没有接收到信号底层会先对信号量进行减减操做,此时信号量就变成负数
// 因此开始进入等到,等达到了等待时间尚未收到信号则进行加加操做复原信号量
// 执行进入等待的方法dispatch_semaphore_wait会返回非0的数
// 收到信号的时候此时信号量是1 底层是减减操做,此时恰好等于0 因此直接返回0
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)
{
//若是一直处于处理source0或者接受mach_port的状态则说明runloop的此次循环尚未完成
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 若是超过两秒则说明卡顿了
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}
@end
复制代码
runloop
实现的大致流程和方案三相同,不过微信加入了堆栈分析,可以定位到耗时的方法调用堆栈,因此须要准确的分析卡顿缘由能够借助微信matrix来分析卡顿。固然也能够在方案2中使用 PLCrashReporter
这个开源的第三方库来获取堆栈信息ping
主线程,在主线程卡顿的状况下,会出现断在的无响应的表现,进而检测卡顿CPU
和 GPU
阶段占用时间太长致使了掉帧卡顿,因此界面优化主要工做就是给 CPU
和 GPU
减负
CPU
进行减负。TableView
其中须要根据每一个 cell
的内容来定 cell
的高度。咱们知道 TableView
有重用机制,若是复用池中有数据,即将滑入屏内的 cell
就会使用复用池内的 cell
,作到节省资源,可是仍是要根据新数据的内容来计算 cell
的高度,从新布局新 cell
中内容的布局 ,这样反复滑动 TableView
相同的 cell
就会反复计算其 frame
,这样也给 CPU
带来了负担。若是在获得数据建立模型的时候就把 cell
frame
算出,TableView
返回模型中的 frame
这样的话一样的一条 cell
就算来回反复滑动 TableView
,计算 frame
这个操做也就仅仅只会执行一次,因此也就作到了减负的功能,以下图:一个 cell
的组成须要 modal
找到数据,也须要 layout
找到这个 cell
如何布局: CPU
阶段拿到图片的顶点数据和纹理以后会进行解码生产位图,而后传递到 GPU
进行渲染主要流程图以下 RunLoop
致使其余工做没法执行好比滑动,这样就会形成卡顿现象,因此这里就能够将解码的工做放到异步线程中不占用主线程,可能有人会想只要将图片加载放到异步线程中在异步线程中生成一个 UIImage
或者是 CGImage
而后再主线程中设置给 UIImageView
,此时能够写段代码使用 instruments
的 Time Profiler
查看一下堆栈信息 CGBitmapContext
,而后从Bitmap
直接建立图片,例如SDWebImage
三方框架中对图片编解码的处理。这就是Image
的预解码,代码以下: dispatch_async(queue, ^{
CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
cgImage = CGBitmapContextCreateImage(context);
UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
CGContextRelease(context);
CGImageRelease(cgImage);
completion(image);
});
复制代码
TableView
中的图片滑动的时候不加载,在滑动中止的时候加载(可使用Runloop
,图片绘制设置 defaultModal
就行)UIView
和 CALayer
的关系:
UIView
是基于 UIKit
框架的,可以接受点击事件,处理用户的触摸事件,并管理子视图CALayer
是基于 CoreAnimation
,而CoreAnimation
是基于QuartzCode
的。因此CALayer
只负责显示,不能处理用户的触摸事件UIView
是直接继承 UIResponder
的,CALayer
是继承 NSObject
的UIVIew
的主要职责是负责接收并响应事件;而 CALayer
的主要职责是负责显示 UI
。UIView
依赖于 CALayer
得以显示UIView
主要负责时间处理,CALayer
主要是视图显示 异步渲染的原理其实也就是在子线程将全部的视图绘制成一张位图,而后回到主线程赋值给 layer
的 contents
,例如 Graver
框架的异步渲染流程以下:if (drawingFinished && targetDrawingCount == layer.drawingCount)
{
CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
{
// 让 UIImage 进行内存管理
// 最终生成的位图
UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
void (^finishBlock)(void) = ^{
// 因为block可能在下一runloop执行,再进行一次检查
if (targetDrawingCount != layer.drawingCount)
{
failedBlock();
return;
}
//主线程中赋值完成显示
layer.contents = (id)image.CGImage;
// ...
}
if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
else finishBlock();
}
// 一些清理工做: release CGImageRef, Image context ending
}
复制代码
最终效果图以下:YYAsyncLayer
和addView
给cell
动态添加view
view
,由于使用透明view
,会致使在GPU
中计算像素时,会将透明view
下层图层的像素也计算进来,即颜色混合处理(当有两个图层的时候一个是半透明一个是不透明若是半透明的层级更高的话此时就会触发颜色混合,底层的混合并非仅仅的将两个图层叠加而是会将两股颜色混合计算出新的色值显示在屏幕中)