【譯】你可能不知道的iOS性能優化建議(來自前Apple工程師)

今天在推特上看到一篇關於性能優化不錯的文章,是前蘋果開發人員寫的,翻譯了一下與你們分享,原地址iOS Performance tips you probably didn't know (from an ex-Apple engineer)html

racetrack.jpg

做爲開發人員,良好的性能對於使咱們的用戶感到驚喜和喜悅是無價的。iOS用戶具備很高的標準,若是你的應用程序反應很慢或在內存壓力下崩潰,他們將中止使用它,或者更糟糕的是,你的評論會很糟糕。ios

在過去的6年中,我在Apple從事Cocoa框架和第一方應用程序的開發工做。我從事Spotlight,iCloud,應用程序擴展程序的工做,最近從事過Files的工做。git

我注意到有一種很容易實現的目標,你能夠在20%的時間內得到80%的性能提高。github

這是一份性能提示清單,但願能給你帶來最大的收益:數據庫

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

uilabel-bordered.png

在內存使用方面,咱們傾向於將lables視爲輕量級的。最後,它們只是顯示文本。UILabel實際上存儲爲位圖,這很容易消耗兆字節的內存。swift

值得慶幸的是,UILabel的實現很聰明,而且只使用它須要的:性能優化

  • 若是label是單色的,UILabel將選擇kCAContentsFormatGray8Uint的calayercontents格式(每像素1字節),而非單色標籤(例如,要顯示"🥳是聚會時間了",或多色NSAttributedString)將須要使用kCAContentsFormatRGBA8Uint(每像素4字節)。

單色標籤最多消耗width * height * contentsScale ^ 2 *(每像素1字節)字節,而非單色標籤則消耗4倍的:width * height * contentsScale ^ 2 *(每像素4字節) 。bash

例如,在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的文本值極可能會有所不一樣,所以存儲它們很浪費。app

要釋放潛在的兆字節內存:

  • 若是將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工做的線程,所以主隊列會受到影響。

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

相關文章
相關標籤/搜索