今天在推特上看到一篇關於性能優化不錯的文章,是前蘋果開發人員寫的,翻譯了一下與大家分享
作為開發人員,良好的性能對於使我們的用戶感到驚喜和喜悅是無價的。iOS用戶具有很高的標准,如果你的應用程序反應很慢或在內存壓力下崩潰,他們將停止使用它,或者更糟糕的是,你的評論會很糟糕。
在過去的6年中,我在Apple從事Cocoa框架和第一方應用程序的開發工作。我從事Spotlight,iCloud,應用程序擴展程序的工作,最近從事過Files的工作。
我注意到有一種很容易實現的目標,你可以在20%的時間內獲得80%的性能提升。
這是一份性能提示清單,希望能給你帶來最大的收益:
1. UILabel的成本超出你的想象
在內存使用方面,我們傾向於將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:
🙀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中:
-[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工作的線程,因此主隊列會受到影響。
2
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的時間稍長一點。” 請注意,我只是想起這一點,因為我只是在這個領域中工作,試圖弄清楚如何避免內存不足的情況而不進行此優化。再次感謝你的帖子。
3
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
推薦👇:
如果你想一起進階,不妨添加一下交流群1012951431
面試題資料或者相關學習資料都在群文件中 進群即可下載!