欢迎使用CSDN-markdown编辑器

iOS绘图性能优化

2016年07月03日

达内教育 纪老师

GitHub

CSDN博客

新浪微博: 小新-iOS讲师

Linus Torvalds: Talk is cheap. Show me the code.

绘图程序

这是一个常见的绘图程序


普通绘制方式

Core Graphics做一个简单的绘图应用这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径UIBezierPath,随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。我们需要一个更好的方法。

#import "MyViewController.h"

@interface MyView : UIView
@property (nonatomic, strong) UIBezierPath *path;
@end

@implementation MyView
- (UIBezierPath *)path{
    if (!_path) {
        _path = [UIBezierPath bezierPath];
        _path.lineWidth = 2;
    }
    return _path;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point = [touches.anyObject locationInView:self];
    //移动路径起始位置
    [self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point = [touches.anyObject locationInView:self];
    //添加线
    [self.path addLineToPoint:point];
    //刷新界面
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    //重绘
    [[UIColor clearColor] setFill];
    [[UIColor redColor] setStroke];
    [self.path stroke];
}

@end

@interface MyViewController ()

@end

@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MyView *v = [[MyView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:v];
}

@end

使用CAShapeLayer进行优化改写

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:

  • 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
  • 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
  • 不会被图层边界剪裁掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉。
  • 不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。

CAShapeLayer可以绘制多边形,直线和曲线。CATextLayer可以绘制文本。CAGradientLayer用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。 如果稍微将之前的代码变动一下,用CAShapeLayer替代Core Graphics,性能就会得到提高.虽然随着路径复杂性的增加,绘制性能依然会下降,但是只有当非常非常浮躁的绘制时才会感到明显的帧率差异。用CAShapeLayer重新实现绘图应用.

#import "ViewController.h"

@interface MyView1 : UIView
@property (nonatomic) UIBezierPath *path;
@property (nonatomic, readonly) CAShapeLayer *sharpLayer;
@end

@implementation MyView1

- (UIBezierPath *)path{
    if (!_path) {
        _path = [UIBezierPath bezierPath];
        self.sharpLayer.lineWidth = 2;
        self.sharpLayer.strokeColor = [UIColor redColor].CGColor;
        //默认封闭路径,需要把填充色去掉
        self.sharpLayer.fillColor = [UIColor clearColor].CGColor;
    }
    return _path;
}

+ (Class)layerClass{
    return [CAShapeLayer class];
}

- (CAShapeLayer *)sharpLayer{
    return (CAShapeLayer *)self.layer;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    [self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    [self.path addLineToPoint:point];
    //设置绘图的路径, 不需要重写drawRect:方法进行重绘
    self.sharpLayer.path = self.path.CGPath;
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MyView1 *v = [[MyView1 alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:v];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

脏矩形-另一种优化方式

有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。 我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。 我们的『黑板』应用的最初实现,我们更改第一个版本的绘图,用一个画刷位置的数组代替UIBezierPath.

#import "MViewController.h"
//图片的宽度
#define BRUSH_SIZE 20

@interface DrawingView: UIView
@property (nonatomic, strong) NSMutableArray<NSValue *> *strokes;
@end

@implementation DrawingView

- (NSMutableArray<NSValue *> *) strokes{
    if (!_strokes) {
        self.strokes = [NSMutableArray array];
    }
    return _strokes;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self];
    [self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self];
    [self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point{
    [self.strokes addObject:[NSValue valueWithCGPoint:point]];
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect{
    for (NSValue *value in self.strokes) {
        CGPoint point = [value CGPointValue];
        CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
        [[UIImage imageNamed:@"Line"] drawInRect:brushRect];
    }
}
@end


@interface MViewController ()

@end

@implementation MViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    DrawingView *v = [[DrawingView alloc] initWithFrame:self.view.bounds];
    v.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:v];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

上方的写法, 每次添加一个新的视图. 都需要重绘整个屏幕.
为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。 当一个视图被改动过了,可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。

#import "MViewController.h"
//图片的宽度
#define BRUSH_SIZE 20

@interface DrawingView: UIView
@property (nonatomic, strong) NSMutableArray<NSValue *> *strokes;
@end

@implementation DrawingView

- (NSMutableArray<NSValue *> *) strokes{
    if (!_strokes) {
        self.strokes = [NSMutableArray array];
    }
    return _strokes;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self];
    [self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self];
    [self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point{
    [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:@"Line"] drawInRect:brushRect];
        }
    }
}
@end

@interface MViewController ()

@end

@implementation MViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    DrawingView *v = [[DrawingView alloc] initWithFrame:self.view.bounds];
    v.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:v];
}

@end
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值