在我们的社交 APP 上,⽤户的动态由精美的照⽚ 、视频和⽂字组成。对于每张照⽚和视频, 我 们都会展示出完整的标题和五个最新评论。由于⽤户喜欢使⽤标题来讲述照⽚背后的故事,因此它们通常很⻓、很复杂,并且可能包含超链接和表情符号。渲染如此复杂的⽂本带来了⼀些问题,它在滚动时造成性能下降。
即使在iPhone12这样的新设备上,复杂标题的初始⽂本绘制需要⻓达50毫秒,⽽⽂本展示需要⻓达30毫秒,渲染速度很慢。⽂本问题还是简单问题, 有时我们需要加载更加复杂的图⽚甚⾄视频。所有这些步骤都发⽣在 UI 线程上, 导致app在⽤户滚动时丢帧。
基础知识:
在开始之前, 最好先了解本⽂的基本概念。
主线程不应该⽤于繁重的操作,⽽主要⽤于:
1.接受⽤户输⼊/交互;
2.显示结果并更新UI。
当主线程必须处理太多操作时,最常⻅的后果是出现丢帧现象,当我们不能保证60 fps(每16.67毫秒⼀帧)时就会发⽣这种现象。
精准地识别并调试丢帧问题
有时我们很容易发现掉帧问题,因为掉帧最常⻅的表现形式是⽆响应/卡顿。
我们可以使⽤友盟+U-APM检查在iPhone12这样的新款设备上是否会发⽣卡顿。
显⽽易⻅在 iPhone12 上也发⽣了卡顿, 由此推断我们的代码存在优化空间, ⽽并⾮⽤户的设备 配置问题。接下来, 我们需要更准确的⽅法来跟踪卡顿问题。我们尝试了使⽤CADisplayLink 和TimeProfiler。
使⽤CADisplayLink类:
class DroppingFramesHelper: NSObject {
private var firstTime: TimeInterval =0.0
private var lastTime: TimeInterval = 0.0
func activate() {
let link = CADisplayLink(target: self, selector: #selector(updat
link.add(to: .main, forMode: .commonModes)
}
@objc private func update(link: CADisplayLink) {
if lastTime ==0{
firstTime = link.timestamp
lastTime = link.timestamp
}
let currentTime = link.timestamp
let elapsedTime = floor((currentTime - lastTime) * 10_000)/10
let totalElapsedTime = currentTime - firstTime
if elapsedTime > 16.7 {
print("[DFH] Frame was dropped with elpased time of \(elapse
}
lastTime = link.timestamp
}
}
然后, 在 AppDelegate 的⽅法中访问它的⼀个实例:
didFinishLaunchingWithOptions:DroppingFramesHelper().activate()
现在,如果测试程序出现丢帧的情况,我们可以在控制台上监控它们:
采取的措施
现在通过控制台和友盟+U-APM知道了掉帧的情况存在,我们能做些什么呢?可以采取⼀些下⾯这些措施:
(1)减少视图和透明视图的数量
(2)最⼩化“连续调⽤函数”中的负载
(3)解码JPEG图像
(4)离屏渲染
我们将会⼀⼀进⾏讨论。
1.减少试图和透明试图的数量
为了提⾼应⽤程序的性能,⾸先要做的事是:
• 减少视图的数量
• 降低透明度
解决的⽅法很简单:
label.layer.opacity = 1.0 label.backgroundColor = .white |
为了更加容易地观察到重叠的透明度,我们可以使⽤⼀个⾮常⽅便的⼯具: 调试->视图调试->渲染->颜⾊混合层。
这个⼯具让我们可以轻松地发现重叠的视图, 如下图所示:
在我们不需要时,这⾥使⽤标签将背景颜⾊设置为不清晰。
2.最小化“连续调用函数”中的负载
显⽽易⻅,像cellForItemAtindexPath或scrollViewDidScroll这样被连续调⽤的函数必须运算得⾮常快。
所以我们尽可能使⽤最“单纯”的视图/单元格,并使⽤⾮常轻巧快速的运算⽅法。(例如, 不涉及布局约束、对象分配的配置)
3.解码JPEG图像
当我们处理丢帧问题时,常⻅的“可优化点”是图像解码。
通常, 这个操作是在主线程上是由 imageViews 完成的, 但在图像⾮常⼤的时候会导致我们的 应⽤程序变慢。
为了缓解这个问题,⼀种解决⽅案是将解码⼯作移⾄后台队列。这样,操作不会像UIImageView采⽤的正常解码那样⾼效,但mainThread将是空闲的。
在后台解码图像:
extension UIImage { class func decodedImage(_ image: UIImage) -> UIImage? { guard let newImage = image.cgImage else { return nil } |
// To optimize this, you cansomecachecontrol.
let colorspace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: nil,
width: newImage.width,
height: newImage.height,
bitsPerComponent: 8,
bytesPerRow: newImage.width * 4,
space: colorspace,
bitmapInfo: CGImageAlphaInfo.noneSkipFir
context?.draw(newImage, in: CGRect(x: 0, y: 0, width: newImage.w
let drawnImage = context?.makeImage()
if let drawnImage = drawnImage {
return UIImage(cgImage: drawnImage)
}
return nil
}
}
可以添加⼀些进⼀步的缓存控制以提⾼效率:
import UIKit class AsyncImageView: UIView { private var _image:UIImage? var image: UIImage? { get { return _image } set { _image = newValue layer.contents = nil guard let image = newValue else { return } DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.main.sync { } let decodedImage = UIImage.decodedImage(image) DispatchQueue.main.async { self.layer.contents = decodedImage?.cgImage } } } } |
可以使⽤“AsyncImageView”,在后台线程⽽不是主线程中解码图像。
为什么使用DispatchQueue.main.sync{}?
在debug的前期我们尝试不使⽤sync,但是程序发⽣了崩溃⾏为。为了找出原因,我们使⽤了友盟+U-APM的异常检测进⾏测试。
可以从图中看出,代码导致了OOM内存异常报警,这是由于内存警告是在主线程上处理的, ⽽我们正在后台处理图像,所以如果我们使⽤太多内存, 就会出现意外⾏为并带来极⼤⻛险(例如图中发⽣的崩溃)
4.离屏渲染
当我们处理UI元素的特定属性时,可能会遇到⼀些离屏渲染问题,因为我们需要在呈现它们之前准备渲染这些元素。这意味着⼤量使⽤CPU和GPU。
如何发现这种问题?
我们使⽤了⼯具: Debug->ViewDebugging->Rendering->ColorOffscreen-RenderedYellow。和前⽂第⼆点的例⼦相似, 使⽤此⼯具, 我们可以发现以⻩⾊或红⾊突出显示的元素。
以下代码内容:
imageView.layer.cornerRadius = avatarImageHeight / 2.0 |
我们使⽤ UIBezierPath代替,它可以简单地解决特定的离屏渲染问题:
extension UIImage { class func circularImage(from image:UIImage,size:CGSize) let scale = UIScreen.main.scale let circleRect = CGRect(x: 0, y: 0, width: size.width *
UIGraphicsBeginImageContextWithOptions(circleRect.size, |
-> UIImage? {
scale, height:
false, scale) |
let circlePath = UIBezierPath(roundedRect:circleRect,cornerRadius:c circlePath.addClip()
image.draw(in: circleRect)
if let roundImage =UIGraphicsGetImageFromCurrentImageContext(){ return roundImage }
return nil } } |
简而言之,以下是通过调试得出的几点经验:
1.避免CornerRadius属性:
2.避免使⽤ShouldRasterize;
3.使⽤ .rounded()值,因为更容易计算。
4.Shadows也会导致离屏渲染。
其他经验:
读者还可以尝试⼀下以下的优化建议:
1.⽂本测量(boudingRectWithSize),但是debug过程可能⾮常繁重。除⾮⾮常需要,否则请尽量避免使⽤它们。
2.检查结构布局, 尤其是使⽤⾃动布局并且必须⽀持旧设备时。
3. 尝试将⼯作放⼊后台队列, 但请注意内存警告。
作者:郑文韬