一个自定义UI控件的优化历程

作者: Phelthas | 来源:发表于2017-03-25 18:55 被阅读86次

    之前实现了一个尺子,效果如图:


    考虑到以后其他地方可能还会用到,就做成了一个pod库,放到了github上,传送门
    后来使用过程中发现并修改了很多问题,来总结一下优化历程:

    最开始是用drawRect的方式实现的,就是在drawRect方法中,把每一条刻度都画出来,核心方法如下:

    - (void)drawRect:(CGRect)rect {
        CGContextRef context = UIGraphicsGetCurrentContext();
        [self.rulerLineColor set];
        CGFloat startPoint = self.rulerMargin;
        for (int i = 0; i <= (self.maxValue - self.minValue); i++) {
            CGContextSetLineWidth(context, 1);
            startPoint = i * self.rulerSpacing + self.rulerMargin;
            CGFloat endPoint = 0;
            if (i % 5 == 0) {
                endPoint = self.longLineDistance; 
            } else {
                endPoint = self.shortLineDistance;
            }
            CGContextMoveToPoint(context, startPoint, 0);
            CGContextAddLineToPoint(context, startPoint, endPoint);
            CGContextStrokePath(context);
        }
        CGContextMoveToPoint(context, self.rulerMargin, 0);
        CGContextAddLineToPoint(context, startPoint, 0);
        CGContextStrokePath(context);
    }
    

    (具体代码见tag 1.0.0)

    这个方法比较直观,所见即所得嘛,很容易想到,但很快就发现了一个大问题:
    太占内存了!!!
    如果只有100条刻度还好,占用内存也就几M,但实际场景是这个是用来选择身高的,精度要到0.1,所以刻度数量是原来的10倍。。。
    那内存就吃不消了,一个小控件要几十M,那来两三个内存不就炸了。。。
    至于为什么drawRect会这么站内存,可以参考客:
    内存恶鬼drawRect
    内存恶鬼drawRect(续:答疑篇)

    所以就迎来了第一次主要的优化:

    使用CALayer来绘图,弃用drawRect方式;

    思路跟原来没有什么大的变化,只是不再使用drawRect的方法,改为在initWithFrame方法中,直接添加layer,核心方法如下:

    - (void)setupUI {
        CGFloat startPoint = self.rulerMargin;
        for (int i = 0; i <= (self.maxValue - self.minValue) / self.accuracy; i++) {
            CALayer *subLayer = [CALayer layer];
            startPoint = i * self.rulerSpacing + self.rulerMargin;
            CGFloat endPoint = 0;
            if (i % 10 == 0) {
                endPoint = self.longLineDistance; 
            }
            else if (i % 5 == 0) {
                endPoint = (self.longLineDistance + self.shortLineDistance) / 2.0;
            }
            else {
                endPoint = self.shortLineDistance;
            }
            subLayer.frame = CGRectMake(startPoint, 0, 1, endPoint);
            subLayer.backgroundColor = self.rulerLineColor.CGColor;
            [self.layer addSublayer:subLayer];
        }
        CALayer *layer = [CALayer layer];
        layer.frame = CGRectMake(self.rulerMargin, 0, startPoint - self.rulerMargin, 1);
        layer.backgroundColor = self.rulerLineColor.CGColor;
        [self.layer addSublayer:layer];
    }
    

    (具体代码见tag 1.3.1)

    这个就是采用了上面提到的两篇博客提到的优化方式,效果也确实很明显,内存占用很小很小了;

    然而计划赶不上变化,产品经理提出了新的需求,要在一个界面上显示多个尺子,而且可以随时隐藏或者显示;
    所以就又碰到了一个大问题:
    绘制太慢,会卡住主线程!!!
    修改UI的操作只能在主线程操作,而要同时画出几个尺子,每个尺子都有成百上千个subLayer,这卡了真不能怪手机,只能怪设计不合理。。。(产品经理:怪我咯~)
    其实不是产品设计不合理,是UI控件设计不合理,尺子这种大面积重复的界面,肯定是可以复用的啦,怎么能直接画这么长呢!!!
    所以就迎来了第二次主要优化:

    使用UICollectionView的复用机制重构整个控件

    (具体代码见tag 2.1.0)
    这个思路就跟之前有比较大的不同了,要把一个完整的尺子拆成N个复用的cell;
    思前想后,我最后采用的方式是:不管刻度的间距是多少或者尺子的精度是多少,每个cell固定显示10个刻度;这样就可以比较容易的确定cell的大小和个数。
    还有几个细节优化的地方:

    1. cell里面固定有几个subLayer,是在初始化的时候就生成好的,在cellForRow方法中,只需要修改他们的frame,颜色等属性就可以;
    2. 原来的markView,是用一个View实现的,当初是为了能用autoLayout,现在直接用个CAShapeLayer实现,手动计算frame来确定位置

    最终效果如图:

    rulerView2.gif

    后续:

    理论上,在cellForRow中修改View的frame等属性也是比较消耗性能的操作,在cell初始化的时候,就把各种属性都设置对才是更理想的做法;
    目前还没想到有什么好的方式可以实现,因为cell类跟rulerView的实例联系不起来。。。
    同学们有什么好的思路,望不吝赐教~

    有什么问题,欢迎讨论~
    也可以直接去github(https://github.com/Phelthas/LXMRulerView ) 上提交issue或者pull request

    相关文章

      网友评论

        本文标题:一个自定义UI控件的优化历程

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