iOS改变控件形状常用方法性能分析

在iOS开发中有时要控制控件的形状,比如显示为圆形。熟悉iOS绘制的朋友第一反应大都是用Core Graphics/Quartz 2D的提供的方式裁剪CGContext画布等,这当然没错,但在不复杂的应用场景,可能会出现杀鸡用牛刀的情况,甚至Core Graphics性能还不如其他简单的方法。
之前在做显示圆形图片的需求时看到一个方法:

let maskPath = UIBezierPath(roundedRect: viewToBeMasked.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSizeMake(viewToBeMasked.bounds.width / 2, viewToBeMasked.bounds.height / 2))
let maskLayer = CAShapeLayer()
maskLayer.frame = viewToBeMasked.bounds
maskLayer.path = maskPath.CGPath
viewToBeMasked.layer.mask = maskLayer

实际上就是用一个CAShapeLayer来遮罩(mask)想要改变形状的控件viewToBeMasked,CAShapeLayer的path属性是控制形状的关键,据说性能比其他的方法都要好。乍看之下这个说法还是靠谱的,viewToBeMasked.layer.mask的mask直接控制了viewToBeMasked的可显示的区域,比起裁剪画布这些耗时的操作简单。但只凭感觉不能作为结论,毕竟实践出真知啊!虽然没有遇到性能问题,但生命不息折腾不止,我决定对比一下这种方法跟裁剪画布有多大差别。

要对比起来也很简单,就是对一个UIView做两种操作,一个是赋值给viewToBeMasked.layer.mask,一个是在drawRect方法里裁剪CGContext画布,然后对比这两种情况下耗费的时间。具体就是下面的MaskedView和NoneMaskedView:

import UIKit
//使用viewToBeMasked.layer.mask的UIView
class MaskedView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .clearColor()
    }

    init(frame: CGRect, mask: Bool) {
        super.init(frame: frame)
        self.backgroundColor = .clearColor()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        let viewToBeMasked = self
        let maskPath = UIBezierPath(roundedRect: viewToBeMasked.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSizeMake(viewToBeMasked.bounds.width / 2, viewToBeMasked.bounds.height / 2))
        let maskLayer = CAShapeLayer()
        maskLayer.frame = viewToBeMasked.bounds
        maskLayer.path = maskPath.CGPath
        viewToBeMasked.layer.mask = maskLayer

        super.layoutSubviews()
    }

    override func drawRect(rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
        CGContextFillRect(context, rect)
    }

}

//裁剪CGContext画布的UIView
class NoneMaskedView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .clearColor()
    }

    init(frame: CGRect, mask: Bool) {
        super.init(frame: frame)
        self.backgroundColor = .clearColor()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func drawRect(rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        CGContextBeginPath(context)
        CGContextAddEllipseInRect(context, rect)
        CGContextClosePath(context)
        CGContextClip(context)
        CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
        CGContextFillRect(context, rect)
    }

}

平常的场景是不容易看出来差别,为了对比考虑了极端的情况,把1000个测试用的View添加到父View上,记录下绘制这些UIView耗费的时间进行对比。

for _ in 1...1000 {
    let noneMaskedView = NoneMaskedView(frame: CGRectMake(self.view.center.x - 100 / 2, self.view.center.y - 100 / 2, 100, 100))
    superView.addSubview(noneMaskedView)
//    let maskedView = MaskedView(frame: CGRectMake(self.view.center.x - 100 / 2, self.view.center.y - 100 / 2, 100, 100), mask: true)
//    superView.addSubview(maskedView)
}

到了这里,才发现并不容易得到绘制UIView耗费的时间,翻了CocoaTouch的文档没有找到好的办法。。。
没办法了,只能转向Xcode测试工具Instruments。爬了一下Instruments的文档,发现了一个叫Time Profiler Instrument的东西,可按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计时间间隔之间的堆栈状态,来推算某个方法执行的时间,并获得一个近似值。
实际使用Time Profiler的时候,没发现具体的某个方法的执行时间方便进行比较,不过既然Time Profiler给出了线程的堆栈状态,那我们根据主线程的堆栈状态,可以得出从开始添加UIView到绘制完成时所有方法的执行时间。虽然这个时间并不都是绘制UIView的时间,但考虑到我们的测试场景变化很小,用来比较我们的NoneMaskedView和MaskedView足够了。这样说是不是迷迷糊糊的?哈哈,没关系,下面对照着结果看就明白了!
测试用到的机器是iPhone 5s,对NoneMaskedView和MaskedView分别进行10次测试。(Instruments的使用不是本文重点,有兴趣可以参考一下这篇博客 IOS性能调优系列:使用Time Profiler发现性能瓶颈)

下面是Time Profiler给出的线程堆栈变化时的CPU使用率柱状图

MaskedView的结果

ios15小组件怎么改格式 ios怎么改变小组件形状_控件

NoneMaskedView的结果

ios15小组件怎么改格式 ios怎么改变小组件形状_ios15小组件怎么改格式_02

从上面的结果图里可以看到,每次添加UIView后,都会发生一次集中的CPU高使用率事件。我们可以得到CPU高使用率的这段时间里主线程所有方法的执行时间,上面说了虽然这个时间并不都是绘制UIView的时间,但考虑到我们的测试场景变化很小,这个时间的差别是可以近似地认为是绘制UIView时间的差别的。

把1000个MaskedView添加到父View上10次的总耗时

ios15小组件怎么改格式 ios怎么改变小组件形状_性能_03

把1000个NoneMaskedView添加到父View上10次的总耗时

ios15小组件怎么改格式 ios怎么改变小组件形状_控件_04

结果还是符合我们的预期的,使用了viewToBeMasked.layer.mask的情况下耗时少了37%!
总结下来,本文把对layer遮罩和裁剪CGContext画布的对比转化成了对进程执行时间的对比,这个结果并不严谨,但还是有参考价值的。
对我的思路有什么意见,欢迎拍砖。