您可能不知道的iOS性能技巧(来自前Apple工程师)
原地址:iOS Performance tips you probably didn't know (from an ex-Apple engineer)
如果您想了解有关Cocoa开发和引导软件业务的最新文章,请在Twitter上关注我或注册邮件列表*。
作为开发人员,良好的性能对于使我们的用户感到惊喜和喜悦是无价的。iOS用户具有很高的标准,如果您的应用程序呆滞或在内存压力下崩溃,他们将停止使用该应用程序,或者更糟糕的是,您的评论将很糟糕。
在过去的6年中,我在Apple从事Cocoa框架和第一方应用程序的开发工作。我曾经从事Spotlight,iCloud,应用程序扩展的工作,最近从事过Files的工作。
我注意到有一种低垂的结果,您可以在20%的时间内获得80%的性能提升。
这是一份性能提示清单,希望能给您带来最大的收益:
1)UILabel
成本超出您的想象
UILabel
一个UILabel
在野外
我们很容易认为标签在内存使用方面是轻量级的。最后,它们只显示文本。UILabels
实际上存储为位图,这很容易消耗兆字节的内存。
值得庆幸的是,该UILabel
实现很聪明,并且仅消耗其所需的资源:
- 如果您的标签是单色的,
UILabel
将选择CALayerContentsFormat
的kCAContentsFormatGray8Uint
(每像素1个字节),而非单色的标签(例如,显示“参加聚会的时间”或彩色NSAttributedString
)则需要使用kCAContentsFormatRGBA8Uint
(每像素4个字节)。
单色标签最多消耗width * height * contentsScale^2 * (1 byte per pixel)
字节,而非单色标签最多消耗4倍:width * height * contentsScale^2 * (4 bytes per pixel)
。
例如,在iPhone 11 Pro Max上,大小414 * 100
点标签最多可消耗:
-
414 * 100 * 3^2 * 1 = **372.6kB**
(单色) -
414 * 100 * 3^2 * 4 = **~1.49MB**
(非单色)
编辑:
在与UIKit工程师在Twitter上讨论之后,我要提请注意。
确保始终先进行测量,如果性能问题确实是由标签引起的内存压力,请仅考虑以下更改。
从UIKit的@Inferis中:
就目前的情况而言:假设将来对UILabel的更新可以优化其(重新)使用后备存储的方式,那么您的优化现在会使事情(可能很多)变得更糟。
当这些单元格进入重用队列时,一种常见的反模式是使UITableView/UICollectionView
单元格标签填充文本内容。一旦单元被回收,标签的文本值很可能会有所不同,因此存储它们是浪费的。
要释放潜在的兆字节内存:
-
text
如果将标签设置为隐藏并且仅偶尔显示,则取消标签。 - 如果标签
text
显示在UITableView/UICollectionView
单元格中,则取消标签:
tableView(_:didEndDisplaying:forRowAt:)
collectionView(_:didEndDisplaying:forItemAt:)
2)始终从串行队列开始,并且仅将并发队列用作最后的选择
常见的反模式是将不会影响UI的块从主队列分配到全局并发队列之一。
例如:
func textDidChange(_ notification: Notification) {
let text = myTextView.text
myLabel.text = text
DispatchQueue.global(qos: .utility).async {
self.processText(text)
}
}
如果我们暂停我们的申请:
螺纹爆炸CDGCD为我们提交的每个块创建了一个线程
当您将dispatch_async
一个块放入并发队列时,GCD会尝试在其线程池中找到一个空闲线程来运行该块。如果找不到空闲线程,则必须为工作项创建一个新线程。快速将块分配到并发队列可能导致快速创建新线程。
请记住:
通常,您应该始终从数量有限的串行队列开始,每个串行队列代表应用程序的子组件(数据库队列,文本处理队列等)。对于具有自己的串行分派队列的较小对象,请使用来定位子组件队列之一dispatch_set_target_queue
。
仅当遇到瓶颈可以通过其他并发解决时,才使用您自己创建的并发队列(不使用dispatch_get_global_queue
),并考虑使用dispatch_apply
。
关于的注释dispatch_get_global_queue
:
您从中获得的并发队列dispatch_get_global_queue
不利于将QoS信息转发到系统,因此应避免。
一个报价由libdispatch”皮埃尔Habouzit:
dispatch_get_global_queue()
实际上,这是调度API提供的最糟糕的事情之一,因为尽管在运行时做出了所有最大的努力,但是在运行时没有足够的有关您的操作/参与者/…的信息来了解您的意图并对其进行优化。 。
有关libdispatch效率提示的更详细概述,请查看此出色的汇编。
3)可能没有看起来那么糟糕
因此,您尝试了尽可能多地优化内存使用率,但是即使那样,使用您的应用程序一段时间后,内存使用率仍然很高。
不用担心,某些系统组件只有在收到内存警告时才会释放内存。
例如,在低内存情况下,UICollectionView
对-didReceiveMemoryWarning
(从iOS 13开始)做出反应,从内存中清除其重用队列。
模拟内存警告:
- 在iOS模拟器中,使用
Simulate Memory Warning
菜单项。 - 在测试设备上,调用私有API(请勿与此一起提交到App Store):
[[UIApplication sharedApplication] performSelector:@selector(_performMemoryWarning)];
4)避免dispatch_semaphore_t
用于等待异步工作
这是一个常见的反模式:
let sem = DispatchSemaphore(value: 0)
makeAsyncCall {
sem.signal()
}
sem.wait()
问题在于,优先级信息不会传播到将由其makeAsyncCall
完成工作的其他线程/进程,并且可能导致优先级倒置:
- 假设
makeAsyncCall
从主队列进行调用会将工作负载分派到QoS的数据库队列中QOS_CLASS_UTILITY
。 -
QOS_CLASS_USER_INITIATED
由于来自主队列的makeAsyncCall
调用dispatch_async
,DB队列的QoS将得到提高。 - 用信号量阻塞主队列意味着它被困在等待正在运行的工作
QOS_CLASS_USER_INITIATED
(低于主队列的工作QOS_CLASS_USER_INTERACTIVE
),因此优先级反转。
附注XPC
:
如果您已经使用过XPC
(在macOS上,或者您正在使用NSFileProviderService
),并且想要进行同步调用,请避免使用信号量,而是使用以下命令将消息发送到同步代理:
- [NSXPCConnection synchronousRemoteObjectProxyWithErrorHandler:].
5)不要使用UIView
标签
这是一种不好的做法,并表明有代码异味。这也不利于性能。
我最近使用过代码,一旦点击一个视图,便会根据其标签值更改其子视图的颜色。
UIKit使用来实现标签objc_get/setAssociatedObject()
,这意味着每次设置或获取标签时,您都在进行字典查找,这可能会在热循环中显示在Instruments中:
[图片上传失败...(image-58e2b0-1606097151280)]
<figcaption class="image-caption" style="box-sizing: inherit; font-style: normal; display: inherit; text-align: center; font-size: 14.4px; color: rgb(86, 86, 86);">-[UIView tag]
处理触摸事件时要花费宝贵的毫秒。</figcaption>
编辑:充其量是微优化。我的收获是:1)令人惊讶的-[UIView tag]
是基于关联的对象,而2)仅在性能敏感的代码中大量使用它才有任何影响。
离别的想法
希望您今天阅读这些提示后能学到新的知识。与往常一样,请确保在进行性能调整之前先进行测量。
有问题吗?有更多性能提示要分享吗?在评论中让我知道!
插头
您可以在这里查看我整洁的Mac实用程序。
编辑
- 感谢Paul Hudson在
UICollectionView
/中使用时,纠正了使标签内容无效的位置UITableView
。
网友评论