作者丨凉介
来源丨掘金
链接:
https://juejin.im/post/5e4cfa4f6fb9a07cce74dba7
今天在推特上看到一篇关于性能优化不错的文章,是前苹果开发人员写的,翻译了一下与大家分享 原地址: iOS Performance tips you probably didn't know (from an ex-Apple engineer) https://www.fadel.io/blog/posts/ios-performance-tips-you-probably-didnt-know/ 作为开发人员,良好的性能对于使我们的用户感到惊喜和喜悦是无价的。iOS用户具有很高的标准,如果你的应用程序反应很慢或在内存压力下崩溃,他们将停止使用它,或者更糟糕的是,你的评论会很糟糕。 在过去的6年中,我在Apple从事Cocoa框架和第一方应用程序的开发工作。我从事Spotlight,iCloud,应用程序扩展程序的工作,最近从事过Files的工作。 我注意到有一种很容易实现的目标,你可以在20%的时间内获得80%的性能提升。 这是一份性能提示清单,希望能给你带来最大的收益:1. UILabel的成本超出你的想象
在内存使用方面,我们倾向于将lables视为轻量级的。最后,它们只是显示文本。UILabel实际上存储为位图,这很容易消耗兆字节的内存。 值得庆幸的是,UILabel的实现很聪明,并且只使用它需要的:- 如果label是单色的,UILabel将选择kCAContentsFormatGray8Uint的calayercontents格式(每像素1字节),而非单色标签(例如,要显示"?是聚会时间了",或多色NSAttributedString)将需要使用kCAContentsFormatRGBA8Uint(每像素4字节)。
- 414 * 100 * 3 ^ 2 * 1 = 372.6kB(单色)
- 414 * 100 * 3 ^ 2 * 4 =〜1.49MB(非单色)
- 如果将label的文本设置为隐藏,则将label的文本设置为nil,仅偶尔显示它们。
- 如果label的文本显示在UITableView / UICollectionView cell中,则将label的文本设置为nil,在:
tableView(_:didEndDisplaying:forRowAt:)collectionView(_:didEndDisplaying:forItemAt:)
2. 始终从串行队列开始,仅将并发队列作为最后的选择
例如: 常见的反模式是将不会影响UI的块从主队列分配到一个全局并发队列中。func textDidChange(_ notification: Notification) {let text = myTextView.text myLabel.text = textDispatchQueue.global(qos: .utility).async {self.processText(text) }}
如果我们暂停application:
?GCD为我们提交的每个块创建了一个线程
当你
dispatch_async
一个块到并发队列时,GCD将尝试在其线程池中找到一个空闲线程来运行该块。
如果找不到空闲线程,则必须为工作项创建一个新线程。
将块快速分配到并发队列可能导致快速创建新线程。
记住这些:
- 创建线程不是免费的。如果你要提交的工作量很小(<1毫秒),那么在切换执行上下文,CPU周期和内存弄脏方面,创建新线程会很浪费。
- GCD会很乐意继续为你创建线程,可能导致线程爆炸。
从dispatch_get_global_queue获得的并发队列不利于将QoS信息转发到系统,因此应避免。有关libdispatch效率更多详细建议,请查看下面这个 出色的收集 。 https://gist.github.com/tclementdev/6af616354912b0347cdf6db159c37057
3. 它可能没有看起来那么糟糕
因此,你尝试过尽可能优化内存使用率,但是即使如此,使用应用程序一段时间后,内存使用率仍然很高。 不用担心,某些系统组件只有在收到内存警告时才会释放内存。 例如,UICollectionView对-didReceiveMemoryWarning(从iOS 13开始)作出反应,在内存不足的情况下从内存中清除其重用队列。 模拟内存警告:- 在iOS模拟器中,使用"模拟内存警告"菜单项。
- 在测试设备上,调用私有API(请勿与此一起提交到App Store):
[[UIApplication sharedApplication] performSelector:@selector(_performMemoryWarning)];
4. 避免使用dispatch_semaphore_t等待异步工作
这是一个常见的反模式:let sem = DispatchSemaphore(value: 0)makeAsyncCall { sem.signal()}sem.wait()
问题在于,优先级信息不会传播到将由
makeAsyncCall
发起的工作将完成的其他线程/进程,并且可能导致优先级倒置:
- 假设从主队列调用
makeAsyncCall
会将工作负载分派到QoSQOS_CLASS_UTILITY
的数据库队列中。 - 由于
makeAsyncCall
从主队列调用了dispatch_async
,数据库队列的QoS将提高到QOS_CLASS_USER_INITIATED
。 - 用信号量阻塞主队列意味着它被困在等待
QOS_CLASS_USER_INITIATED
下运行的工作(低于主队列的QOS_CLASS_USER_INTERACTIVE
),因此优先级反转。
XPC
的附带说明:
如果你已经使用XPC(在macOS上,或者您正在使用
NSFileProviderService
),并且想要进行同步调用,请避免使用信号量,而是使用以下方式将消息发送到同步代理:
-[NSXPCConnection synchronousRemoteObjectProxyWithErrorHandler:].
5. 不要使用UIView tags
这是一种不好的做法,并表明有代码异味。这也不利于性能。 我最近写过这样的代码,一旦点击一个视图,便会根据其标签值更改其子视图的颜色。 UIKit使用objc_get / setAssociatedObject()
实现标签,这意味着每次你设置或获取标签时,你都在进行字典查找,该字典将显示在Instruments中:
-[UIView tag]在处理触摸事件时会消耗宝贵的毫秒数。
文章和推特下有意思的讨论
文章和推特下有意思的讨论,我这里摘取一些,可能也有帮助 ####1 Steven Fisher: 我仍然没有找到替代4的好方法。 我减少了对该模式的使用,以至于它仅在我的测试工具中使用,但仍然困扰着我。 Xaxxus: PromiseKit,是你的答案。 Rony Fadel: 向API提供者索要同步API,使用同步API是你最好的选择,它将确保QoS传播。 Daniel Pourhadi: 如果说API提供者是Apple,又要等AVAsset属性填充怎么办? 在后台线程线程(相对于主线程)中的信号量有害吗? Rony Fadel: 后台线程上的信号量有什么好处? 如果你真的认为使用同步API有好处,请提交错误报告。 这是有害的,因为每次你阻塞等待后台工作的信号时,系统都会丢失QoS传播信息。 然后想象一下,主队列在该后台队列上执行dispatch_sync
。
boost不会一直传播到执行AVAsset工作的线程,因此主队列会受到影响。