Ekulelu's Blog

iOS线段绘制

1、简单粗暴的drawInRect (Android的OnDraw)

在iOS平台,drawInRect是重绘一个控件的方法,在这里可以拿到这个控件的一个上下文对象,可以往这个上下文对象里面画线或者其他一些简单图形。所以最开始做的简单方法就是在onTouch事件中收集到点的数据,然后在drawInRect方法里面绘制每一帧的时候,从第0个点重新绘制到最新的点。

1
2
3
4
5
6
7
8
9
10
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, 3); //线宽
CGContextSetAllowsAntialiasing(context, true); //抗锯齿
CGContextSetRGBStrokeColor(context, 70.0 / 255.0, 241.0 / 255.0, 241.0 / 255.0, 1.0); //线的颜色
CGContextMoveToPoint(context, 0, 0); //起点坐标
CGContextAddLineToPoint(context, self.frame.size.width, self.frame.size.height); //终点坐标
CGContextStrokePath(context);
}

因为iOS里面每次调用drawInRect的时候,都会清空上下文。这个弊端很大,当你的点有1万个的时候,每一帧都从第0个点开始绘制,会出现明显的延时。

2、使用Path对象

在OnTouch事件将取得的点添加进path对象里面,然后在drawInRect的时候,将这个path重绘。这个确实会节省大量的开销。但是有个问题:path对象只有一个width,写字板有个需求,在写的时候线的粗细需要跟着笔的压力进行变化。所以将全部点都添加到一个path是不可能的。最坏的情况就是每两个点都必须生成一个path对象。在那么多path的情况下对性能也有很大影响。

1
2
3
4
5
6
7
UIBezierPath *bPath = [[UIBezierPath alloc]init];
[bPath setLineCapStyle:kCGLineCapRound];
[bPath setLineJoinStyle:kCGLineJoinRound];
[bPath setLineWidth:self.lineWidth];
[self.lineColor setStroke];
[bPath moveToPoint:CGPointMake(500.0, 510.0)];
[bPath stroke];

3、使用CAShapeLayer

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:
1、渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。2、高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。3、不会被图层边界剪裁掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉。4、不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];
[path addLineToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(125, 225)];
[path moveToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(175, 225)];
[path moveToPoint:CGPointMake(100, 150)];
[path addLineToPoint:CGPointMake(200, 150)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;
//add it to our view
[self.containerView.layer addSublayer:shapeLayer];

CAShapeLayer的缺点是一个CAShapeLayer只能添加一个path,如果你需要绘制不同的形状,那么就得添加不同的layer。之前测试过一个demo,添加了几十个layer之后,内存仍没有可观测的变化。对于可变宽度的线,使用CAShapeLayer仍显得很占用内存。

4、drawInRect的改进方法

上面提到,drawInRect每次都会清空掉上一帧的图片,所以必须每次从头开始绘制。为了解决这个问题,可以采取这样的做法:每画一定数量的点之后,将当期的图片保存下来。后面就先绘制这张图片,然后再绘制这张图片之后的点。这样可以避免当点特别多的时候的卡顿。坏处就是当你的画布大小很大的时候,比如1080P,每次渲染的时候都需要绘制这张缓存的图片。在iOS系统测试来看,使用UIImage的drawInRect方法十分吃性能。

5、现在的做法

使用CGBitmapContextCreate(),先开辟一个画布上下文context,以后来的点都往这个画布上去画,且画过的点不需要再次绘制。当需要渲染到屏幕的时候,将这个画布context提取成UIImage对象。最后将这个image显示赋值到UIImageView,显示屏幕上。这样的话,开销就在于从context中提取image和绘制新到来的几个数据点。测试看来,能将CPU功耗降低一半。
这里遇到一个问题,创建context的时候,内存可以自己分配,或者传入NULL让系统自动生成,开始的时候忘记了内存是自己分配的,然后没进行释放,当你新建了许多页的时候,虽然你把context release掉了,但是内存还是泄露了,且程序不会崩溃,只会打log,然后绘制的时候出现一些莫名其妙的效果。我这边是闪屏。

6、更好的做法:

使用OpenGLES,测试了一些使用OpenGLES的demo,它们对CPU性能消耗要更低,而且可以做出原生API无法实现的效果。