CATiledLayer是什么
CATiledLayer顾名思义,把绘制区域分成小块,采用类似贴瓷砖的方式进行绘制。
想象一张50000*50000大图,分解成一个个100*100小块,每次绘制,只需要绘制屏幕内的部分,从而达到省内存的目的。
它很适合与UIScrollView一起,实现超大图的手势放大缩小。网上关于CATiledLayer的demo有很多,但是都没说清其中的参数是什么意思。这里就不贴代码了,主要针对参数做一下解释。
CATiledLayer只暴露一个函数+三个变量,如图所示,下面对其一一进行解释。
什么是tileSize
tileSize就是分片大小。以像素为单位。
理解这一点,十分十分重要。
例如,你的手机是3倍屏,tileSize设为屏幕宽度(pt)的正方形,你会看到3x3个tile。因为每一个tile实际上是屏幕宽度的三分之一。
实际项目中,设置为屏幕宽度的正方形,感觉就没啥毛病。设置的太小,会导致tile太多,影响绘制效率。
什么是levelsOfDetail、levelsOfDetailBias
CATiledLayer不是单层瓷砖。
每一层瓷砖,代表一层清晰度水平。放大后,会使用下一层瓷砖显示。
比如,考虑tileSize设置为100*100,CATiledLayer所在UIView大小也是100*100,手机是一倍屏,展示一张10000*10000的图片。
第一层,只有一个瓷砖,把10000*10000的图采样成100*100显示。第二层,有2*2=4个瓷砖,10000*10000采样成200*200显示。以此类推,第三层4*4=16个瓷砖,400*400...
每一层,比上一层多2x2=4倍的瓷砖。显然,越往下放大,采样的图片大小越大,显示在屏幕上的内容越清晰。
有的朋友会说,你这放大以后,采样的像素也大了啊!放大到很大时,岂不是还是费很大内存,和使用普通的CALayer没区别啊???
非也。
你放大以后,屏幕中显示的瓷砖数也少了。CATiledLayer只会绘制屏幕中正在展示的瓷砖。你永远不可能把第三层16个瓷砖、第四层64个瓷砖完全显示在屏幕。这就是CATiledLayer既省内存又能显示清晰的根本原因!
levelsOfDetail就代表有几层瓷砖。levelsOfDetailBias代表有几层瓷砖是用来放大的。一般我们设置levelsOfDetailBias = levelsOfDetail - 1
在上面的例子中,我们设置levelsOfDetail = 5,levelsOfDetailBias = 4,则有1x1、2x2、4x4、8x8、16x16,五层瓷砖。若levelsOfDetailBias也等于5,则没有1x1,但多了32x32。
瓷砖以2次幂放大倍数为阈值进行切换。比如,UIScrollView放大倍率大于1了,则开始显示2x2层,大于2了,显示4x4层,大于4了,显示8x8层。
什么是fadeDuration
默认值是0.25,代表放大时,瓷砖渐变出现的时长,以秒为单位。
一般都希望设置为0,因为这个渐变效果会显得app运行缓慢。
CATiledLayer绘制流程推测
一般采用CATiledLayer的UIView这样定义
class CBTiledLayer: CATiledLayer{
override class func fadeDuration() -> CFTimeInterval {
0
}
}
class CBLineLayerView: UIView{
override class var layerClass: AnyClass {
CBTiledLayer.self
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
weak var tiledLayer: CATiledLayer?
override init(frame: CGRect) {
super.init(frame: frame)
tiledLayer = (self.layer as! CATiledLayer)
// 这里设置levelsOfDetail
}
override func draw(_ rect: CGRect) {
// 这里进行绘制操作
}
}
所有的绘制操作,都写在draw中。
经实测发现:
1.draw并不是画整个UIView,而是只画一个瓷砖。
2.并不是每次拖动、放大,都会触发draw。
3.draw的内容永远不能超过rect的范围。
4.绘制过的rect,后面可能还会重新触发draw。
5.这个函数永远在子线程中调用,哪怕设置了drawAsyncroniously=false。
推测,draw是一个准备瓷砖的接口。哪个位置的瓷砖,该显示什么内容,通过这个接口定义。它绘制出来的内容,并不直接显示在屏幕上,而是放在CoreAnimation的缓存里。当View需要用到某一片瓷砖的时候,便从缓存中拿出来绘制到屏幕上。当缓存满了,系统会销毁之前的瓷砖,以容纳新的瓷砖。
CATiledLayer的使用者,只需要定义每个瓷砖要显示什么内容。具体该显示哪个瓷砖,该绘制哪个瓷砖,该回收哪个瓷砖,由系统管理,不需要使用者关心。
永远不要使用drawAsyncroniously
因为CATiledLayer本身就是在后台线程准备瓷砖,设置drawAsyncroniously = true,会产生线程同步问题,从而发生闪屏。
使用CATiledLayer时,永远不要在后台线程创建任何CALayer
当CATiledLayer存在时,只要我们在后台线程创建其他CALayer(仅仅是创建,都不需要加入到View中),就会触发CATiledLayer的bug。
具体表现为,CATiledLayer无视缓存的存在,疯狂重绘屏幕区域的瓷砖,从而产生OOM。
看内存,CATiledLayer发生内存泄漏。哪怕CATiledLayer所在的VC已经pop掉了,CATiledLayer所在的UIView都被回收掉了,CATiledLayer及其缓存所占用的内存都无法被释放。根引用来自一个Stack,推测是某个线程的函数调用栈。
猜测,后台线程创建CALayer后,打乱了CoreAnimation本身的节奏,使得某些绘制CATiledLayer的线程被CoreAnimation忽视掉了。即使这些线程绘制好了瓷砖,CoreAnimation也不理不睬,晾着它们,既不使用其绘制的瓷砖,也不把线程结束掉,从而引发线程和瓷砖图片的泄漏。
强制重刷CATiledLayer
有时,CATiledLayer中的内容,是需要等待加载完成的。
当CATiledLayer所在的UIView对用户可见时,系统就会调用draw函数,但因为内容还没有准备好,会显示空白。
当图片准备好后,调用UIView的setNeedsDisplay,经实测,可能并不能完全刷新CATiledLayer的显示。这时就需要用到一个强制刷新CATiledLayer的trick。
layer.contents = nil
layer.setNeedsDisplay(layer.bounds)