iOS 绘图优化
在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core Animation和OpenGL,Core Graphics要慢了不少。
软件绘图不仅效率低,还会消耗可观的内存。 CALayer 只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给 contents 属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为 contents 属性,那么他们将会共用同一块内存,而不是复制内存块。
但是一旦你实现了 CALayerDelegate 协议中的 -drawLayer:inContext: 方法或者 UIView 中的 -drawRect: 方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽图层高4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 204815264字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。
1.用Core Graphics实现一个简单的绘图应用
#import "DrawingCoreGraphicsView.h"
@interface DrawingCoreGraphicsView()
@property (nonatomic, strong) UIBezierPath *path;
@end
@implementation DrawingCoreGraphicsView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
[self initPath];
}
return self;
}
- (void)awakeFromNib{
[super awakeFromNib];
[self initPath];
}
- (void)initPath{
self.path = [[UIBezierPath alloc] init];
self.path.lineJoinStyle = kCGLineJoinRound;
self.path.lineCapStyle = kCGLineCapRound;
self.path.lineWidth = 5;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];
//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];
//add a new line segment to our path
[self.path addLineToPoint:point];
//redraw the view
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
//draw path
[[UIColor clearColor] setFill];
[[UIColor redColor] setStroke];
[self.path stroke];
}
@end

这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径( UIBezierPath ),随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。
2.用 CAShapeLayer 重新实现绘图应用
#import "DrawingShapeLayerView.h"
@interface DrawingShapeLayerView()
@property (nonatomic, strong) UIBezierPath *path;
@end
@implementation DrawingShapeLayerView
+ (Class)layerClass
{
//this makes our view create a CAShapeLayer
//instead of a CALayer for its backing layer
return [CAShapeLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.path = [[UIBezierPath alloc] init];
//configure the layer
CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.lineWidth = 5;
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];
//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];
//add a new line segment to our path
[self.path addLineToPoint:point];
//update the layer with a copy of the path
((CAShapeLayer *)self.layer).path = self.path.CGPath;
}
@end

这种比CoreGraphics稍微好点,但绘制的点多了,掉帧一样很厉害。
为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。
3.下面是使用图片来绘制一个黑板檫的功能,用-setNeedsDisplayInRect: 来减少不必要的绘制
#define BRUSH_SIZE 32
@interface DrawingBlackBoardView()
@property (nonatomic, strong) NSMutableArray *strokes;
@end
@implementation DrawingBlackBoardView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.strokes = [NSMutableArray array];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];
//add brush stroke
[self addBrushStrokeAtPoint:point];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self];
//add brush stroke
[self addBrushStrokeAtPoint:point];
}
- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];
[self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}
- (CGRect)brushRectForPoint:(CGPoint)point
{
return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}
-(void)drawRect:(CGRect)rect
{
for (NSValue *value in self.strokes) {
CGPoint point = [value CGPointValue];
CGRect brushRect = [self brushRectForPoint:point];
if (CGRectIntersectsRect(rect, brushRect)) {
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
}
@end

可以看到,即使到了最后面,帧率还能接近最大值。这种只绘制重绘区域,极大的优化了重绘功能。
总结:
1.第三种方式以极大的优化了绘制的性能,我们还可以使用异步绘制来优化,性能会更棒,这个就留读者自行思考了。
2.有任何问题欢迎留言评论。