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的结果
NoneMaskedView的结果
从上面的结果图里可以看到,每次添加UIView后,都会发生一次集中的CPU高使用率事件。我们可以得到CPU高使用率的这段时间里主线程所有方法的执行时间,上面说了虽然这个时间并不都是绘制UIView的时间,但考虑到我们的测试场景变化很小,这个时间的差别是可以近似地认为是绘制UIView时间的差别的。
把1000个MaskedView添加到父View上10次的总耗时
把1000个NoneMaskedView添加到父View上10次的总耗时
结果还是符合我们的预期的,使用了viewToBeMasked.layer.mask的情况下耗时少了37%!
总结下来,本文把对layer遮罩和裁剪CGContext画布的对比转化成了对进程执行时间的对比,这个结果并不严谨,但还是有参考价值的。
对我的思路有什么意见,欢迎拍砖。