需要实现的效果图
![](https://img.haomeiwen.com/i5290537/1dffef93fa26652d.gif)
绘制动态波形的总体思路
在我们绘制波形的控件中,定义一个可变数组receiveDataArray
,不断接收外面心电数据,同时定义一个定时器,以把数据不断的显示在网格中,绘制成心电波形;
_currentLocationX = 0; // 当前波形点的X位置,
_receiveDataArray = [NSMutableArray array]; // 接收心电数据的数组
_pointSpace = self.frame.size.height / 500.0; // 两波形点之间的距离
_totalWidth = self.frame.size.width; // 要绘制的总宽度
_pointCount = (NSInteger)_totalWidth / _pointSpace; // 一屏幕总的点数
// 开启一个定时器在 drawWaves 方法 绘制波形
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawWaves)];
if (@available(iOS 10.0, *)) {
displayLink.preferredFramesPerSecond = 30; // 每秒三十次
} else {
displayLink.frameInterval = 2; // 每秒三十次,iOS10以下的方法
}
// 添加到NSRunLoopCommonModes模式中,避免UI交互,影响绘制
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
普通方式——通过setNeedsDisplay来全屏绘制
每秒三十次,每次通过setNeedsDisplay
方法把屏幕上的点重新绘制一遍,这里要注意给动的那个点,留出一部分空白区域,每次绘制的点应该要比实际数小20个左右,在定时器方法中,调用 setNeedsDisplay方法开始绘制
- (void)drawWaves
{
// 只要多于10个就开始画
if (self.receiveDataArray.count >= 10) {
// 把接收到的数据赋值给另外一个数组
self.midArray = [NSMutableArray arrayWithArray:self.receiveDataArray];
// 清空数组
[self.receiveDataArray removeAllObjects];
[self setNeedsDisplay]; // 调用drawRect方法开始绘制
}
在drawRect方法中,我们开始绘制波形
- (void)drawRect:(CGRect)rect
{
// 控件高度
CGFloat height = self.frame.size.height;
// self.midArray 每次新增加的数据数组
if (self.midArray.count == 0) {
return;
}
// 把新增加的数据转为要新绘制的点
for (int i = 0; i < self.midArray.count; i++) {
// 如果当前位置大于要绘制的宽度,从开始位置继续画
if (_currentLocationX > _totalWidth) {
_currentLocationX = 0;
}
// 获取数组中的数据
CGFloat tempY = [self.midArray[i] floatValue];
// 把数据转化为波形的y值,不需要关心怎么转的,数值随意填,看这像就行了
CGFloat y = height/2 + height *(tempY - 2040)/1784;
// 波形点,添加到数组中
CGPoint startPoint = CGPointMake(_currentLocationX,y);
NSValue *value = [NSValue valueWithCGPoint:startPoint];
[self.pointArray addObject:value];
// 当前X位置增加单位距离
_currentLocationX += _pointSpace;
}
// 留出二十个点的空隙,显示那个空白的移动点
if (self.pointArray.count > _pointCount - 20) {
[self.pointArray removeObjectsInRange:NSMakeRange(0, self.pointArray.count - _pointCount + 20)];
}
CGContextRef ctx = UIGraphicsGetCurrentContext(); // 获取上下文
CGPoint point = [self.pointArray[0] CGPointValue];
CGContextMoveToPoint(ctx, point.x, point.y); // 移动到第一个点
CGContextSetLineWidth(ctx, 1); // 设置宽度
for (NSInteger i = 1; i < [self.pointArray count];i++) {
if (self.pointArray[i] == nil) {
break;
}
CGPoint previousPoint = [self.pointArray[i - 1] CGPointValue]; // 前面的点
CGPoint nextPoint = [self.pointArray[i] CGPointValue]; // 后面的点
// 如果后面的点小于前面点的x值,表明已经回到开始值,不要增加线,把点移到原点;
if (nextPoint.x < previousPoint.x) {
// 移动到起点
CGContextMoveToPoint(ctx, nextPoint.x, nextPoint.y);
} else {
// 继续增加线
CGContextAddLineToPoint(ctx, nextPoint.x, nextPoint.y);
}
}
[[UIColor blackColor] setStroke]; // 设置颜色
CGContextStrokePath(ctx); // 画波形
}
资源占用,见图
![](https://img.haomeiwen.com/i5290537/fe4834e3bf123830.png)
这种全屏幕绘制,占用的cpu很高,到达了18%,性能很差;
高效方式1——通过setNeedsDisplayInRect 方法绘制
通过setNeedsDisplay
方法,全屏绘制效率很低,观察波形,我们可以看到除了新来的那一部分数据在变,其它部分是没有变的,有没有办法让这部分数据不重新绘制,只绘制新来的那一部分数据,查找API,发现有setNeedsDisplayInRect
这个方法,跟setNeedsDisplay
这个方法一样,调用它也会触发drawRect
这个方法,但它只绘制我们指定的那一部分屏幕区域;
注意我们给出的数据一定要超过我们指定的区域,避免把相交区域的波形的裁剪细了,不对称
在定时器方法中
- (void)drawWaves
{
// 只要多于10个就开始画
if (self.receiveDataArray.count >= 10) {
// 把接收到的数据赋值给另外一个数组
self.midArray = [NSMutableArray arrayWithArray:self.receiveDataArray];
// 清空数组
[self.receiveDataArray removeAllObjects];
[self shape];
// [self setNeedsDisplay];
#if 0
NSInteger midcount = [self.midArray count];
// 每次多画十个,也就是10个的空隙,_currentLocationX 现在的位置,_pointSpace 两点之间的距离
CGFloat totalNum = _currentLocationX + _pointSpace * (midcount + 10);
if (totalNum > _totalWidth) {
// 大于屏幕的宽度时,两部分要画, 最后面和最前面,往开始位置前面,不要纠结于5是什么,根据实际情况调节就是了
// 后半部分
[self setNeedsDisplayInRect:CGRectMake(_currentLocationX - _pointSpace * 5, 0, _totalWidth - _currentLocationX + _pointSpace * 5, self.frame.size.height)];
// 前半部分
[self setNeedsDisplayInRect:CGRectMake(0, 0, totalNum - _totalWidth, self.frame.size.height)];
} else {
// 小于屏幕宽度
[self setNeedsDisplayInRect:CGRectMake(_currentLocationX - _pointSpace * 5 , 0, _pointSpace *(midcount + 15), self.frame.size.height)];
}
}
}
在drawRect
方法中都只绘制的最新的四十个,因为我接收的数据每次大约是10个,我绘制的比它多个30个左右进行了,避免相交部分被裁切;
- (void)drawRect {
同上........
// 这部分改为下面那部分
if (self.pointArray.count > _pointCount - 10) {
[self.pointArray removeObjectsInRange:NSMakeRange(0, self.pointArray.count - _pointCount + 10)];
}
// 每次最多画40个点,大于 setNeedsDisplayInRect 方法里 设置的范围就行了
if (self.pointArray.count > 40) {
[self.pointArray removeObjectsInRange:NSMakeRange(0, self.pointArray.count - 40)];
}
}
资源占用
![](https://img.haomeiwen.com/i5290537/4d21f463b9d9a5e1.png)
相比较于setNeedsDisplay
CPU从18%
降低到了2%
,内存降低了4M左右,性能优化很明显
高效方式2——通过CAShapeLayer方式绘制
CAShapeLayer创建一个CAShapeLayer对象,每次设置它的path来绘制波形,在初始化方法中设置
CAShapeLayer *shape = [[CAShapeLayer alloc] init];
shape.lineCap = kCALineCapRound;
[self.layer addSublayer:shape];
shape.strokeColor = [UIColor blackColor].CGColor;
shape.fillColor = [UIColor clearColor].CGColor;
shape.lineWidth = 1.0f;
self.shapLayer = shape;
self.path = [UIBezierPath bezierPath]; // 创建
在定时器方法中,每次设置CAShapeLayer
的路径就行了
- (void)setupShapePath:(CGRect)rect
{
// 控件高度
CGFloat height = self.frame.size.height;
// self.midArray 每次新增加的数据数组
if (self.midArray.count == 0) {
return;
}
// 把新增加的数据转为要新绘制的点
for (int i = 0; i < self.midArray.count; i++) {
// 如果当前位置大于要绘制的宽度,从开始位置继续画
if (_currentLocationX > _totalWidth) {
_currentLocationX = 0;
}
// 获取数组中的数据
CGFloat tempY = [self.midArray[i] floatValue];
// 把数据转化为波形的y值,不需要关心怎么转的,数值随意填,看这像就行了
CGFloat y = height/2 + height *(tempY - 2040)/1784;
// 波形点,添加到数组中
CGPoint startPoint = CGPointMake(_currentLocationX,y);
NSValue *value = [NSValue valueWithCGPoint:startPoint];
[self.pointArray addObject:value];
// 当前X位置增加单位距离
_currentLocationX += _pointSpace;
}
// 留出十个点的空隙,显示那个空白的移动点
if (self.pointArray.count > _pointCount - 30) {
[self.pointArray removeObjectsInRange:NSMakeRange(0, self.pointArray.count - _pointCount + 30)];
}
// 移除所有的点
[self.path removeAllPoints];
CGPoint point = [self.pointArray[0] CGPointValue];
[self.path moveToPoint:point];
for (NSInteger i = 1; i < [self.pointArray count];i++) {
if (self.pointArray[i] == nil) {
break;
}
CGPoint previousPoint = [self.pointArray[i - 1] CGPointValue]; // 前面的点
CGPoint nextPoint = [self.pointArray[i] CGPointValue]; // 后面的点
// 如果后面的点小于前面点的x值,表明已经回到开始值,不要增加线,把点移到原点;
if (nextPoint.x < previousPoint.x) {
// 移动到起点
[self.path moveToPoint:nextPoint];
} else {
// 继续增加线
[self.path addLineToPoint:nextPoint];
}
}
self.shapLayer.path = self.path.CGPath; // 设置shapeLayer的路径
}
占用资源
![](https://img.haomeiwen.com/i5290537/26ee71d17d2aff12.png)
CPU跟setNeedsDisplayInRect
差不多,多了2%左右,但相较于setNeedsDisplay
好了很多,惊讶的是,占用内存居然比setNeedsDisplayInRect
还少,要知到这里是把全屏幕的数据加进去的,而setNeedsDisplayInRect
只算了部分数据;;
多讲一下,关于CAShapeLayer,
苹果弄出的一个很很很牛逼的控件,能实现很多神奇的效果,占用资源也非常少,如果你不是很懂,推荐去看一下;
总结:
setNeedsDisplay
方法效率很差,占用CPU和内存都很高,不推荐使用,我们做其它方面的项目时,用这方法也应特别小心,至于setNeedsDisplayInRect
和CAShapeLayer
两种方法,性能都不错,相对来说,setNeedsDisplayInRect
性能稍微好点,但判断绘制的区域很麻烦,综合考虑,推荐使用CAShapeLayer
方法;
网友评论