13

【译】你可能不知道的iOS性能优化建议(来自前Apple工程师)

 4 years ago
source link: https://juejin.im/post/5e4cfa4f6fb9a07cce74dba7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2020年02月19日 阅读 3746

【译】你可能不知道的iOS性能优化建议(来自前Apple工程师)

今天在推特上看到一篇关于性能优化不错的文章,是前苹果开发人员写的,翻译了一下与大家分享,原地址iOS Performance tips you probably didn't know (from an ex-Apple engineer)

racetrack.jpg

作为开发人员,良好的性能对于使我们的用户感到惊喜和喜悦是无价的。iOS用户具有很高的标准,如果你的应用程序反应很慢或在内存压力下崩溃,他们将停止使用它,或者更糟糕的是,你的评论会很糟糕。

在过去的6年中,我在Apple从事Cocoa框架和第一方应用程序的开发工作。我从事Spotlight,iCloud,应用程序扩展程序的工作,最近从事过Files的工作。

我注意到有一种很容易实现的目标,你可以在20%的时间内获得80%的性能提升。

这是一份性能提示清单,希望能给你带来最大的收益:

1. UILabel的成本超出你的想象

uilabel-bordered.png

在内存使用方面,我们倾向于将lables视为轻量级的。最后,它们只是显示文本。UILabel实际上存储为位图,这很容易消耗兆字节的内存。

值得庆幸的是,UILabel的实现很聪明,并且只使用它需要的:

  • 如果label是单色的,UILabel将选择kCAContentsFormatGray8Uint的calayercontents格式(每像素1字节),而非单色标签(例如,要显示"🥳是聚会时间了",或多色NSAttributedString)将需要使用kCAContentsFormatRGBA8Uint(每像素4字节)。

单色标签最多消耗width * height * contentsScale ^ 2 *(每像素1字节)字节,而非单色标签则消耗4倍的:width * height * contentsScale ^ 2 *(每像素4字节) 。

例如,在iPhone 11 Pro Max上,大小为414 * 100 points的lable最多可消耗:

  • 414 * 100 * 3 ^ 2 * 1 = 372.6kB(单色)
  • 414 * 100 * 3 ^ 2 * 4 =〜1.49MB(非单色)

当这些cells进入重用队列时,一种常见的反模式是使UITableView / UICollectionView cell labels填充文本内容。一旦cells被回收,label的文本值很可能会有所不同,因此存储它们很浪费。

要释放潜在的兆字节内存:

  • 如果将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 = text
    DispatchQueue.global(qos: .utility).async {
        self.processText(text)
    }
}

复制代码

如果我们暂停application:

thread-explosion.jpg
🙀GCD为我们提交的每个块创建了一个线程

当你dispatch_async一个块到并发队列时,GCD将尝试在其线程池中找到一个空闲线程来运行该块。 如果找不到空闲线程,则必须为工作项创建一个新线程。将块快速分配到并发队列可能导致快速创建新线程。

记住这些:

  • 创建线程不是免费的。如果你要提交的工作量很小(<1毫秒),那么在切换执行上下文,CPU周期和内存弄脏方面,创建新线程会很浪费。
  • GCD会很乐意继续为你创建线程,可能导致线程爆炸。

通常,你应该始终从数量有限的串行队列开始,每个串行队列代表应用程序的子组件(数据库队列,文本处理队列等)。对于具有自己的串行调度队列的较小对象,请使用dispatch_set_target_queue定位子组件队列之一。

仅当遇到额外的并发可以解决的瓶颈时,才使用自己创建的并发队列(不使用dispatch_get_global_queue),并考虑使用dispatch_apply。

关于dispatch_get_global_queue的注释

从dispatch_get_global_queue获得的并发队列不利于将QoS信息转发到系统,因此应避免。

有关libdispatch效率更多详细建议,请查看这个出色的收集

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会将工作负载分派到QoS QOS_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中:

tag_time_profiler.jpg

-[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工作的线程,因此主队列会受到影响。

Tyler:非常有趣,谢谢你。重新填充cell-我的理解是,collection/table view进入重用池会在大于可见区域的边界上触发-这是一种防止重用池抖动的优化。如果我们 clear/load cell可见性,那么我们是否不进行这种优化? 我了解你的建议是解决内存问题,但这对提高性能有什么作用? 不幸的是,似乎没有一种方法可以知道单元何时真正回到重用池中。

Rony Fadel:cells不在视图中时(通常在滚动时)进入重用队列。它与内存有关(性能的一部分,至少是我们在Apple上的分类方式),但与滚动性能无关。

Tyler:我认为你描述的是在didDisappear时返回重用池的内容与iOS10之前的行为一致。 他们从iOS 10记录中的UICollectionView的新增功能中描述了添加的滚动性能优化- “...现在该cell将要退出CollectionView的可见范围。因此,我们将向其发送期望的didEndDisplayingCell。Peter在谈论iOS 9时,此时该cell进入了重用队列,我们将完成此操作。要再次在此特定cell中显示数据,我们必须经历生命周期的开始 并调用cellForItemAtIndexPath。但是在iOS 10中,我们将保留该cell的时间稍长一点。” 请注意,我只是想起这一点,因为我只是在这个领域中工作,试图弄清楚如何避免内存不足的情况而不进行此优化。再次感谢你的帖子。

John Siracusa:当你要等待超时的异步非主线程用户启动的工作时,你建议使用什么而不是DispatchSemaphore?

Yaron Inger:你可以使用dispatch group 和 dispatch_group_wait。

Rafael Cerioli:Dispatch groups 和 semaphores一样,没有方法将async转变成sync。

J Matusevich:Dispatch group 是答案。

NieR: Autoconf:Dispatch group 和 semaphore 性能一样. The API 很棒但行为没有区别。

Bob Godwin:DispatchWorkItem👍🏽它们处理了我必须使用semafores的那些情况。 只是该API尚未为开发人员所广泛了解 dispatchworkitem

pkamb:DispatchGroup! Waiting for multiple blocks to finish


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK