简单-UIKit性能分析优化

本文是基于UIKit性能调优实战讲解 - 这篇文章的总结详细请看原文

实例讲解可以参考:小心别让圆角成了你列表的帧数杀手 - CocoaChina_让移动开发更简单

fps表示frames per second,也就是每秒钟显示多少帧画面。对于静止不变的内容,我们不需要考虑它的刷新率,但在执行动画或滑动时,fps的值直接反映出滑动的流畅程度

调试优化:

1.图层混合:

首先理解像素:像素就是屏幕上的每一个点由RGB三种颜色构成有时候会带有alpha,如果某一个区域覆盖了多个layer,最后的显示效果就会收到影响,显色混合会消耗一定的GPU,当把最上层的颜色透明度设置为100%时GPU就会忽略下面所有的layer节省不必要的运算

第一个调试选项"Color Blended Layers"正是用于检测哪里发生了图层混合,并用红色标记出来。因此我们需要尽可能减少看到的红色区域。一旦发现应该想法设法消除它

解决方案:a.将控件的opaque = true 原理是希望避免图层混合,但是作用不大,因为UIView的这个属性默认就是true只要不是人为的设置为透明都不会出现图层混合,但对于UIImageView他本身和图片都不能为透明的,图片自身的性质也可能会对结果产生影响,比opaque属性更重要的是backgroundColor属性,如果不设置这个属性,控件依然被认为是透明的,所以我们做的是将他设置为白色

PS:如果label文字有中文,依然会出现图层混合,这是因为此时label多了一个sublayer,如果有好的解决办法欢迎告诉我。

2.光栅化光栅化会导致离屏渲染

光栅化是将一个layer预先渲染成位图(bitmap),然后加入缓存中。如果对于阴影效果这样比较消耗资源的静态内容进行缓存,可以得到一定幅度的性能提升。demo中的这一行代码表示将label的layer光栅化:

label.layer.shouldRasterize = true

Instrument中,第二个调试选项是“Color Hits Green and Misses Red”,它表示如果命中缓存则显示为绿色,否则显示为红色,显然绿色越多越好,红色越少越好。

光栅化的缓存机制是一把双刃剑,先写入缓存再读取有可能消耗较多的时间。因此光栅化仅适用于较复杂的、静态的效果。通过Instrument的调试发现,这里使用光栅化经常出现未命中缓存的情况,如果没有特殊需要则可以关闭光栅化

3.颜色格式:

像素在内存中的布局和它在磁盘中的存储方式并不相同。考虑一种简单的情况:每个像素有R、G、B和alpha四个值,每个值占用1字节,因此每个像素占用4字节的内存空间。一张1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600个像素,因此占用了超过8Mb的内存。但是一张同样分辨率的PNG格式或JPEG格式的图片一般情况下不会有这么大。这是因为JPEG将像素数据进行了一种非常复杂且可逆的转化。

比如应用中有一些从网络下载的图片,而GPU恰好不支持这个格式,这就需要CPU预先进行格式转化

第三个选项“Color Copied Images”就用来检测这种实时的格式转化,如果有则会将图片标记为蓝色。

4.图片大小

第五个选项“Color Misaligned Images”。它表示如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色

第三个优化是调整所有图片的像素大小以避免不必要的缩放。

5.离屏渲染

正常的渲染通道:OpenGL提交一个命令到Command Buffer,随后GPU开始渲染,渲染结果放到Render Buffer中,这是正常的渲染流程

有一些复杂的效果无法直接渲染出结果,它需要分步渲染最后再组合起来,比如添加一个蒙版(mask):GPU分别得到了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把两者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,因此相对于普通渲染更占用资源。

第六个选项“Color Offscreen-Rendered Yellow”会把需要离屏渲染的地方标记为黄色,大部分情况下我们需要尽可能避免黄色的出现。离屏渲染可能会自动触发,也可以手动触发。以下情况可能会导致触发离屏渲染:

重写drawRect方法

有mask或者是阴影(layer.masksToBounds, layer.shadow*),模糊效果也是一种mask

layer.shouldRasterize = true

前两者会自动触发离屏渲染,第三种方法是手动开启离屏渲染。

第四个优化,在设置阴影效果的四行代码下面添加一行:

imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath

这行代码制定了阴影路径,如果没有手动指定,Core Animation会去自动计算,这就会触发离屏渲染。如果人为指定了阴影路径,就可以免去计算,从而避免产生离屏渲染。

设置cornerRadius本身并不会导致离屏渲染,但很多时候它还需要配合layer.masksToBounds = true使用。根据之前的总结,设置masksToBounds会导致离屏渲染。解决方案是尽可能在滑动时避免设置圆角,如果必须设置圆角,可以使用光栅化技术将圆角缓存起来:

// 设置圆角    ps:用后用后无效果

label.layer.masksToBounds = true

label.layer.cornerRadius = 8

label.layer.shouldRasterize = true

label.layer.rasterizationScale = layer.contentsScale

6.快速路径:

之前将离屏渲染和渲染路径时的示意图么,离屏渲染的最后一步是把此前的多个路径组合起来。如果这个组合过程能由CPU完成,就会大量减少GPU的工作。这种技术在绘制地图中可能用到。

第七个选项“Color Compositing Fast-Path Blue”用于标记由硬件绘制的路径,蓝色越多越好。

7.变化区域

刷新视图时,我们应该把需要重绘的区域尽可能缩小。对于未发生变化的内容则不应该重绘,第八个选项“Flash updated Regions”用于标记发生重绘的区域。一个典型的例子是系统的时钟应用,绝大多数时候只有显示秒针的区域需要重绘:

总结

如果你一步一步做到了这里,我想一定会有不少收益。不过,学而不思则罔,思而不学则殆。动手实践后还是应该总结提炼,优化滑动性能主要涉及三个方面:

避免图层混合

确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明

如无特殊需要,不要设置低于1的alpha值

确保UIImage没有alpha通道

避免临时转换

确保图片大小和frame一致,不要在滑动时缩放图片

确保图片颜色格式被GPU支持,避免劳烦CPU转换

慎用离屏渲染

绝大多数时候离屏渲染会影响性能

重写drawRect方法,设置圆角、阴影、模糊效果,光栅化都会导致离屏渲染

设置阴影效果是加上阴影路径

滑动时若需要圆角效果,开启光栅化

实战

本文的demo可以在我的Github上下载,然后一步一步自己体验优化过程。但demo毕竟是刻意搭建的一个环境,我会在我自己的仿写的app上不断进行实战优化,欢迎共同学习交流。

代码:

UIView圆角:

#import "UIView+AddCorner.h"

@implementation UIView (AddCorner)

-(double)roundbyuint:(double) num anUint:(double )unit{

double remain = modf(num, &unit);

if (remain > unit / 2.0) {

return [self ceilbyuint:num andUint:unit];

} else {

return [self floorbyuint:num andUint:unit];

}

}

-(double)ceilbyuint:(double)num andUint:(double)unit{

return num - modf(num, &unit) + unit;

//将浮点数num分解成整数部分和小数部分

}

-(double)floorbyuint:(double)num andUint:(double)unit{

return num - modf(num, &unit) ;

}

-(double)piexl:(double)num{

double unit;

switch ((int)[UIScreen mainScreen].scale) {

case 1: unit = 1.0/1.0; break;

case 2: unit = 1.0/2.0; break;

case 3: unit = 1.0/3.0; break;

default: unit = 0.0;

break;

}

return [self roundbyuint:num anUint:unit];

}

-(void)kt_viewAddCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBorderColor:(UIColor*)borderColor andBackgroundColor:(UIColor*)backgroundColor{

UIImageView * imageView = [[UIImageView alloc]initWithImage:[self ViewAddCorner:radius andBorderWidth:borderWidth andBorderColor:borderColor andBackgroundColor:backgroundColor]] ;

[self insertSubview:imageView atIndex:0];

}

-(UIImage *)ViewAddCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBorderColor:(UIColor*)borderColor andBackgroundColor:(UIColor*)backgroundColor {

CGSize sizeToFit = CGSizeMake((double)CGRectGetWidth(self.frame), (double)CGRectGetHeight(self.frame));

CGFloat halfBorderWidth = (CGFloat)borderWidth / 2.0;

UIGraphicsBeginImageContextWithOptions(sizeToFit, false, [UIScreen mainScreen].scale);

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetLineWidth(context, borderWidth);

CGContextSetStrokeColorWithColor(context, borderColor.CGColor);

CGContextSetFillColorWithColor(context, backgroundColor.CGColor);

CGFloat width = sizeToFit.width, height = sizeToFit.height;

CGContextMoveToPoint(context, width - halfBorderWidth, radius + halfBorderWidth);  // 开始坐标右边开始

CGContextAddArcToPoint(context, width - halfBorderWidth, height - halfBorderWidth, width - radius - halfBorderWidth, height - halfBorderWidth, radius);  // 右下角角度

CGContextAddArcToPoint(context, halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - radius - halfBorderWidth, radius); // 左下角角度

CGContextAddArcToPoint(context, halfBorderWidth, halfBorderWidth, width - halfBorderWidth, halfBorderWidth, radius); // 左上角

CGContextAddArcToPoint(context, width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, radius + halfBorderWidth, radius) ;// 右上角

CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);

UIImage  *output = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

return output;

}

@end

UIImageView圆角:

#import "UIImageView+AddCorner.h"

@implementation UIImageView (AddCorner)

-(void)kt_ImageViewAddCornerWithRadius:(CGFloat)radius{

// self.image = self.image?:[self.image imageAddCornerWithRadius:radius andSize:self.bounds.size];

//注意第三个选项的设置

UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);

//在绘制之前先裁剪出一个圆形

[[UIBezierPath bezierPathWithRoundedRect:self.bounds

cornerRadius:radius] addClip];

//图片在设置的圆形里面进行绘制

[self.image drawInRect:self.bounds];

//获取图片

self.image = UIGraphicsGetImageFromCurrentImageContext();

//结束绘制

UIGraphicsEndImageContext();

}

@end

UIImage圆角:

#import "UIImage+ImageRoundedCorner.h"

@implementation UIImage (ImageRoundedCorner)

- (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{

CGRect rect = CGRectMake(0, 0, size.width, size.height);

UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);

CGContextRef ctx = UIGraphicsGetCurrentContext();

UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];

CGContextAddPath(ctx,path.CGPath);

CGContextClip(ctx);

[self drawInRect:rect];

CGContextDrawPath(ctx, kCGPathFillStroke);

UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

return newImage;

}

@end

你可能感兴趣的:(简单-UIKit性能分析优化)