美文网首页
iOS 两种方式高效的绘制动态波形

iOS 两种方式高效的绘制动态波形

作者: alenpaulkevin | 来源:发表于2018-03-13 19:01 被阅读883次

    需要实现的效果图


    waves.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);  // 画波形
     }
    

    资源占用,见图


    setNeedsDisplay.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)];
         }
     }
    

    资源占用

    setNeedsDisplayInRect.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的路径
     }
    

    占用资源


    CAShapeLayer.png

    CPU跟setNeedsDisplayInRect差不多,多了2%左右,但相较于setNeedsDisplay好了很多,惊讶的是,占用内存居然比setNeedsDisplayInRect还少,要知到这里是把全屏幕的数据加进去的,而setNeedsDisplayInRect只算了部分数据;

    多讲一下,关于CAShapeLayer,苹果弄出的一个很很很牛逼的控件,能实现很多神奇的效果,占用资源也非常少,如果你不是很懂,推荐去看一下;

    总结:

    setNeedsDisplay方法效率很差,占用CPU和内存都很高,不推荐使用,我们做其它方面的项目时,用这方法也应特别小心,至于setNeedsDisplayInRectCAShapeLayer两种方法,性能都不错,相对来说,setNeedsDisplayInRect性能稍微好点,但判断绘制的区域很麻烦,综合考虑,推荐使用CAShapeLayer方法;

    相关文章

      网友评论

          本文标题:iOS 两种方式高效的绘制动态波形

          本文链接:https://www.haomeiwen.com/subject/ntezfftx.html