微博 Demo 性能優化技巧 我為了演示 YYKit 的功能,實現了微博和 Twitter 的 Demo,併為它們做了不少性能優化,下麵就是優化時用到的一些技巧。 預排版 當獲取到 API JSON 數據後,我會把每條 Cell 需要的數據都在後臺線程計算並封裝為一個佈局對象 CellLayout。 ...
微博 Demo 性能優化技巧
我為了演示 YYKit 的功能,實現了微博和 Twitter 的 Demo,併為它們做了不少性能優化,下麵就是優化時用到的一些技巧。
預排版
當獲取到 API JSON 數據後,我會把每條 Cell 需要的數據都在後臺線程計算並封裝為一個佈局對象 CellLayout。CellLayout 包含所有文本的 CoreText 排版結果、Cell 內部每個控制項的高度、Cell 的整體高度。每個 CellLayout 的記憶體占用並不多,所以當生成後,可以全部緩存到記憶體,以供稍後使用。這樣,TableView 在請求各個高度函數時,不會消耗任何多餘計算量;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算佈局了。
對於通常的 TableView 來說,提前在後臺計算好佈局結果是非常重要的一個性能優化點。為了達到最高性能,你可能需要犧牲一些開發速度,不要用 Autolayout 等技術,少用 UILabel 等文本控制項。但如果你對性能的要求並不那麼高,可以嘗試用 TableView 的預估高度的功能,並把每個 Cell 高度緩存下來。這裡有個來自百度知道團隊的開源項目可以很方便的幫你實現這一點:FDTemplateLayoutCell。
預渲染
微博的頭像在某次改版中換成了圓形,所以我也跟進了一下。當頭像下載下來後,我會在後臺線程將頭像預先渲染為圓形並單獨保存到一個 ImageCache 中去。
一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:1012951431, 分享BAT,阿裡面試題、面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。
對於 TableView 來說,Cell 內容的離屏渲染會帶來較大的 GPU 消耗。在 Twitter Demo 中,我為了圖省事兒用到了不少 layer 的圓角屬性,你可以在低性能的設備(比如 iPad 3)上快速滑動一下這個列表,能感受到雖然列表並沒有較大的卡頓,但是整體的平均幀數降了下來。用 Instument 查看時能夠看到 GPU 已經滿負荷運轉,而 CPU 卻比較清閑。為了避免離屏渲染,你應當儘量避免使用 layer 的 border、corner、shadow、mask 等技術,而儘量在後臺線程預先繪製好對應內容。
非同步繪製
我只在顯示文本的控制項上用到了非同步繪製的功能,但效果很不錯。我參考 ASDK 的原理,實現了一個簡單的非同步繪製控制項。這塊代碼我單獨提取出來,放到了這裡:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子類,當它需要顯示內容(比如調用了 [layer setNeedDisplay])時,它會向 delegate,也就是 UIView 請求一個非同步繪製的任務。在非同步繪製時,Layer 會傳遞一個 BOOL(^isCancelled)()這樣的 block,繪製代碼可以隨時調用該 block 判斷繪製任務是否已經被取消。
當 TableView 快速滑動時,會有大量非同步繪製任務提交到後臺線程去執行。但是有時滑動速度過快時,繪製任務還沒有完成就可能已經被取消了。如果這時仍然繼續繪製,就會造成大量的 CPU 資源浪費,甚至阻塞線程並造成後續的繪製任務遲遲無法完成。我的做法是儘量快速、提前判斷當前繪製任務是否已經被取消;在繪製每一行文本前,我都會調用 isCancelled() 來進行判斷,保證被取消的任務能及時退出,不至於影響後續操作。
目前有些第三方微博客戶端(比如 VVebo、墨客等),使用了一種方式來避免高速滑動時 Cell 的繪製過程,相關實現見這個項目:VVeboTableViewDemo。它的原理是,當滑動時,鬆開手指後,立刻計算出滑動停止時 Cell 的位置,並預先繪製那個位置附近的幾個 Cell,而忽略當前滑動中的 Cell。這個方法比較有技巧性,並且對於滑動性能來說提升也很大,唯一的缺點就是快速滑動中會出現大量空白內容。如果你不想實現比較麻煩的非同步繪製但又想保證滑動的流暢性,這個技巧是個不錯的選擇。
全局併發控制
當我用 concurrent queue 來執行大量繪製任務時,偶爾會遇到這種問題:
大量的任務提交到後臺隊列時,某些任務會因為某些原因(此處是 CGFont 鎖)被鎖住導致線程休眠,或者被阻塞,concurrent queue 隨後會創建新的線程來執行其他任務。當這種情況變多時,或者 App 中使用了大量 concurrent queue 來執行較多任務時,App 在同一時刻就會存在幾十個線程同時運行、創建、銷毀。CPU 是用時間片輪轉來實現線程併發的,儘管 concurrent queue 能控制線程的優先順序,但當大量線程同時創建運行銷毀時,這些操作仍然會擠占掉主線程的 CPU 資源。ASDK 有個 Feed 列表的 Demo:SocialAppLayout,當列表內 Cell 過多,並且非常快速的滑動時,界面仍然會出現少量卡頓,我謹慎的猜測可能與這個問題有關。
使用 concurrent queue 時不可避免會遇到這種問題,但使用 serial queue 又不能充分利用多核 CPU 的資源。我寫了一個簡單的工具 YYDispatchQueuePool,為不同優先順序創建和 CPU 數量相同的 serial queue,每次從 pool 中獲取 queue 時,會輪詢返回其中一個 queue。我把 App 內所有非同步操作,包括圖像解碼、對象釋放、非同步繪製等,都按優先順序不同放入了全局的 serial queue 中執行,這樣儘量避免了過多線程導致的性能問題。
更高效的非同步圖片載入
SDWebImage 在這個 Demo 里仍然會產生少量性能問題,並且有些地方不能滿足我的需求,所以我自己實現了一個性能更高的圖片載入庫。在顯示簡單的單張圖片時,利用 UIView.layer.contents 就足夠了,沒必要使用 UIImageView 帶來額外的資源消耗,為此我在 CALayer 上添加了 setImageWithURL 等方法。除此之外,我還把圖片解碼等操作通過 YYDispatchQueuePool 進行管理,控制了 App 匯流排程數量。
其他可以改進的地方
上面這些優化做完後,微博 Demo 已經非常流暢了,但在我的設想中,仍然有一些進一步優化的技巧,但限於時間和精力我並沒有實現,下麵簡單列一下:
列表中有不少視覺元素並不需要觸摸事件,這些元素可以用 ASDK 的圖層合成技術預先繪製為一張圖。
再進一步減少每個 Cell 內圖層的數量,用 CALayer 替換掉 UIView。
目前每個 Cell 的類型都是相同的,但顯示的內容卻各部一樣,比如有的 Cell 有圖片,有的 Cell 里是卡片。把 Cell 按類型劃分,進一步減少 Cell 內不必要的視圖對象和操作,應該能有一些效果。
把需要放到主線程執行的任務劃分為足夠小的塊,並通過 Runloop 來進行調度,在每個 Loop 里判斷下一次 VSync 的時間,併在下次 VSync 到來前,把當前未執行完的任務延遲到下一個機會去。這個只是我的一個設想,並不一定能實現或起作用。
如何評測界面的流暢度
最後還是要提一下,“過早的優化是萬惡之源”,在需求未定,性能問題不明顯時,沒必要嘗試做優化,而要儘量正確的實現功能。做性能優化時,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個流程,優先解決最值得優化的地方。
如果你需要一個明確的 FPS 指示器,可以嘗試一下 KMCGeigerCounter。對於 CPU 的卡頓,它可以通過內置的 CADisplayLink 檢測出來;對於 GPU 帶來的卡頓,它用了一個 1x1 的 SKView 來進行監視。這個項目有兩個小問題:SKView 雖然能監視到 GPU 的卡頓,但引入 SKView 本身就會對 CPU/GPU 帶來額外的一點的資源消耗;這個項目在 iOS 9 下有一些相容問題,需要稍作調整。
我自己也寫了個簡單的 FPS 指示器:FPSLabel 只有幾十行代碼,僅用到了 CADisplayLink 來監視 CPU 的卡頓問題。雖然不如上面這個工具完善,但日常使用沒有太大問題。
最後,用 Instuments 的 GPU Driver 預設,能夠實時查看到 CPU 和 GPU 的資源消耗。在這個預設內,你能查看到幾乎所有與顯示有關的數據,比如 Texture 數量、CA 提交的頻率、GPU 消耗等,在定位界面卡頓的問題時,這是最好的工具。