您可能不知道的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
在野外
我們很容易認爲標籤在內存使用方面是輕量級的。最後,它們只顯示文本。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
。