1. 程式人生 > >關於iOS基礎總結(5)--tableView的優化、cell高度優化、記憶體優化

關於iOS基礎總結(5)--tableView的優化、cell高度優化、記憶體優化

1、tableView的優化
iOS平臺因為UIKit本身的特性,需要將所有的UI操作都放在主執行緒執行,所以有時候就習慣將一些執行緒安全性不確定的邏輯,以及它執行緒結束後的彙總工作等等放到了主執行緒,所以主執行緒包含大量計算、IO、繪製都有可能造成卡頓。

· 可以通過監控runLoop監控監控卡頓,呼叫方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之後,也就是如果我們發現這兩個時間內耗時太長,那麼就可以判定出此時主執行緒卡頓.

· 使用到CFRunLoopObserverRef,通過它可以實時獲得這些狀態值的變化

· 監控後另外再開啟一個執行緒,實時計算這兩個狀態區域之間的耗時是否到達某個閥值,便能揪出這些效能殺手.

· 監控到了卡頓現場,當然下一步便是記錄此時的函式呼叫資訊,此處可以使用一個第三方Crash收集元件PLCrashReporter,它不僅可以收集Crash資訊也可用於實時獲取各執行緒的呼叫堆疊

· 當檢測到卡頓時,抓取堆疊資訊,然後在客戶端做一些過濾處理,便可以上報到伺服器,通過收集一定量的卡頓資料後經過分析便能準確定位需要優化的邏輯

· 設定正確的 reuseidentifer 以重用 cell

· 儘量將 View 設定為不透明,包括 cell 本身(backgroundcolor預設是透明的),圖層混合靠GPU去渲染,如果透明度設定為100%,那麼GPU就會忽略下面所有的layer,節約了很多不必要的運算。模擬器上點選“Debug”選單,然後選擇“color Blended Layers”,會把所有區域分成綠色和紅色,綠色的好,紅色的效能差(經過混合渲染的),當然也有一些圖片雖然是不透明的,但是也會顯示紅色,如果檢查程式碼沒錯的話,一般就是圖片自身的性質問題了,直接聯絡美工或後臺解決就好了。除非必須要用GPU載入的,其他最好要用CPU載入,因為CPU一般不會百分百載入,可以通過CoreGraphics畫出圓角

· 有時候美工失誤,圖片大小給錯了,引起不必要的圖片縮放(可以找美工去改,當然也可以非同步去裁剪圖片然後快取下來),還是使用Instrument的Color Misaligned Images,黃色表示圖片需要縮放,紫色表示沒有畫素對齊。當然一般情況下圖片格式不會給錯,有些圖片格式是GPU不支援的,就還要勞煩CPU去進行格式轉換。還有可以通過Color Offscreen-Rendered Yellow來檢測離屏渲染(就是把渲染結果臨時儲存,等到用的時候再取出,這樣相對於普通渲染更消耗記憶體,使用maskToBounds、設定shadow,重寫drawRect方法都會導致離屏渲染)
避免漸變,cornerRadius在預設情況下,這個屬性只會影響檢視的背景顏色和 border,但是不會離屏繪製,不影響效能。不用clipsToBounds(過多呼叫GPU去離屏渲染),而是讓後臺載入圖片並處理圓角,並將處理過的圖片賦值給UIImageView。UIImageView 的圓角通過直接擷取圖片實現,圓角路徑直接用貝塞爾曲線UIBezierPath繪製(人為指定路徑之後就不會觸發離屏渲染),UIGraphicsBeginImageContextWithOptions。UIView的圓角可以使用CoreGraphics畫出圓角矩形,核心是CGContextAddArcToPoint 函式。它中間的四個引數表示曲線的起點和終點座標,最後一個引數表示半徑。呼叫了四次函式後,就可以畫出圓角矩形。最後再從當前的繪圖上下文中獲取圖片並返回,最後把這個圖片插入到檢視層級的底部。
“Flash updated Regions”用於標記發生重繪的區域

· 如果 row 的高度不相同,那麼將其快取下來

· 如果 cell 顯示的內容來自網路,那麼確保這些內容是通過非同步下載

· 使用 shadowPath 來設定陰影,圖層最好不要使用陰影,陰影會導致離屏渲染(在進入螢幕渲染之前,還看不到的時候會再渲染一次,儘量不要產生離屏渲染)

· 減少 subview 的數量,不要去新增或移除view,要就顯示,不要就隱藏

· 在 cellForRowAtIndexPath 中儘量做更少的操作,最好是在別的地方算好,這個方法裡只做資料的顯示,如果需要做一些處理,那麼最好做一次之後將結果儲存起來.

· 使用適當的資料結構來儲存需要的資訊,不同的結構會帶來不同的操作代價
使用,rowHeight , sectionFooterHeight 和 sectionHeaderHeight 來設定一個恆定高度 , 而不是從代理(delegate)中獲取

· cell做資料繫結的時候,最好在willDisPlayCell裡面進行,其他操作在cellForRowAtIndexPath,因為前者是第一頁有多少條就執行多少次,後者是第一次載入有多少個cell就執行多少次,而且呼叫後者的時候cell還沒顯示

· 讀取檔案,寫入檔案,最好是放到子執行緒,或先讀取好,在讓tableView去顯示

· tableView滾動的時候,不要去做動畫(微信的聊天介面做的就很好,在滾動的時候,動態圖就不讓他動,滾動停止的時候才動,不然可能會有點影響流暢度)。在滾動的時候載入圖片,停止拖拽後在減速過程中不載入圖片,減速停止後加載可見範圍內圖片

2、優化tableViewCell高度
· 一種是針對所有 Cell 具有固定高度的情況,通過:self.tableView.rowHeight = 88;
指定了一個所有 cell 都是 88 高度的 UITableView,對於定高需求的表格,強烈建議使用這種(而非下面的)方式保證不必要的高度計算和呼叫。

· 另一種方式就是實現 UITableViewDelegate 中的:heightForRowAtIndexPath:需要注意的是,實現了這個方法後,rowHeight 的設定將無效。所以,這個方法適用於具有多種 cell 高度的 UITableView。

· iOS7之後出了了estimatedRowHeight,面對不同高度的cell,只要給一個預估的值就可以了,先給一個預估值,然後邊滑動邊計算,但是缺點就是

·· 設定估算高度以後,tableView的contentSize.height是根據cell高度預估值和cell的個數來計算的,導致導航條處於很不穩定的狀態,因為contentSize.height會逐漸由預估高度變為實際高度,很多情況下肉眼是可以看到導航條跳躍的
·· 如果是設計不好的上拉載入或下拉重新整理,有可能使表格滑動跳躍
·· 估算高度設計初衷是好的,讓載入速度更快,但是損失了流暢性,與其損失流暢性,我寧願讓使用者載入介面的時候多等那零點幾秒

· iOS8 WWDC 中推出了 self-sizing cell 的概念,旨在讓 cell 自己負責自己的高度計算,使用 frame layout 和 auto layout 都可以享受到:

·· self.tableView.estimatedRowHeight = 213;
self.tableView.rowHeight = UITableViewAutomaticDimension;
如果不加上估算高度的設定,自動算高就失效了
·· 這個自動算高在 push 到下一個頁面或者轉屏時會出現高度特別詭異的情況,不過現在的版本修復了。

· 相同的程式碼在 iOS7 和 iOS8 上滑動順暢程度完全不同,iOS8 莫名奇妙的卡。很大一部分原因是 iOS8 上的算高機制大不相同,從 WWDC 也倒是能找到點解釋,cell 被認為隨時都可能改變高度(如從設定中調整動態字型大小),所以每次滑動出來後都要重新計算高度。

·· dequeueReusableCellWithIdentifier:forIndexPath: 相比不帶 “forIndexPath” 的版本會多呼叫一次高度計算
·· iOS7 計算高度後有”快取“機制,不會重複計算;而 iOS8 不論何時都會重新計算 cell 高度

· 使用 UITableView+FDTemplateLayoutCell(百度知道負責人孫源) 無疑是解決算高問題的最佳實踐之一,既有 iOS8 self-sizing 功能簡單的 API,又可以達到 iOS7 流暢的滑動效果,還保持了最低支援 iOS6

· FDTemplateLayoutCell 的高度預快取是一個優化功能,利用RunLoop空閒時間執行預快取任務計算,當用戶正在滑動列表時顯然不應該執行計算任務影響滑動體驗。

·· 當用戶正在滑動 UIScrollView 時,RunLoop 將切換到 UITrackingRunLoopMode 接受滑動手勢和處理滑動事件(包括減速和彈簧效果),此時,其他 Mode (除 NSRunLoopCommonModes 這個組合 Mode)下的事件將全部暫停執行,來保證滑動事件的優先處理,這也是 iOS 滑動順暢的重要原因
·· 註冊 RunLoopObserver 可以觀測當前 RunLoop 的執行狀態,並在狀態機切換時收到通知:

RunLoop開始
RunLoop即將處理Timer
RunLoop即將處理Source
RunLoop即將進入休眠狀態
RunLoop即將從休眠狀態被事件喚醒
RunLoop退出

·· 分解成多個RunLoop Source任務,假設列表有 20 個 cell,載入後展示了前 5 個,那麼開啟估算後 table view 只計算了這 5 個的高度,此時剩下 15 個就是“預快取”的任務,而我們並不希望這 15 個計算任務在同一個 RunLoop 迭代中同步執行,這樣會卡頓 UI,所以應該把它們分別分解到 15 個 RunLoop 迭代中執行,這時就需要手動向 RunLoop 中新增 Source 任務(由應用發起和處理的是 Source 0 任務)

3、談談記憶體的優化和注意事項(使用Instrument工具的CoreAnimation、GPU Driver、I/O操作,檢查fps數值)
· 重用問題:比如UITableViewCell、UICollectionViewCell、UITableViewHeaderFooterViews等設定正確的reuseIdentifier,充分重用

· 懶載入控制元件、頁面:對於不是立刻使用的資料,都應該使用延遲載入的方式,比如網路連線失敗的提示介面,可能一直都用不到

· 使用Autorelease Pool:在某些迴圈建立臨時變數處理資料時,自動釋放池以保證能及時釋放記憶體

· 不要使用太多的xib/storyboard:載入時會將其內部的圖片在內的所有資源載入記憶體,即使未來很久才會需要使用,相對於純程式碼寫的延遲載入,在效能和記憶體上就差了很多

· 資料快取:對於cell的行高要快取起來,使用reloadData效率也極高,對於網路資料,不需要每次都請求的,應該快取起來,可以寫入資料庫,也可以通過plist檔案儲存

· 選擇正確的資料結構:針對不同的業務場景選擇最合適的資料結構是寫出高效程式碼的基礎

陣列:有序的一組值,使用索引查詢起來很快,使用值查詢的很慢,插入/刪除 很慢
字典:儲存鍵值對對,用鍵查詢比較快
集合:無序的一組值,用值來查詢很快,插入/刪除很快

· gzip/zip壓縮:當從伺服器下載相關附件時,可以通過 zip壓縮後再下載,使得記憶體更小,下載速度也更快

· 重大開銷物件:一些objects的初始化很慢,比如NSDateFormatter和 NSCalendar,但是又無可避免的需要使用,通常作為屬性儲存起來,避免反覆使用

· 避免反覆處理資料:需要應用需要從伺服器載入資料,常為JSON或者XML格式的資料,在伺服器端或者客戶端使用相同的資料結構很重要

· 選擇圖片時,要對圖片進行壓縮處理,根據不同的情況選擇不同的圖片載入方式,-imageNamed:讀取到記憶體後會快取下來,適合圖片資源較小,使用很頻繁的圖片;-initWithContentsOfFiles:僅載入圖片而不快取,適合較大的圖片。若是collectionView中使用大量圖片的時候,可以用UIVIew.layer.contents=(__bridge id _Nullable)(model.clipedImage.CGImage);這樣就更輕量級一些

· 當然有時候也會用到一些第三方,比如在使用UICollectionView和UITableView的時候,Facebook有一個框架叫AsyncDisplayKit,這個庫就可以很好地提升滾動時流暢性以及圖片非同步下載功能(不支援sb和autoLayout,需要手動進行約束設定),AsyncDisplayKit用相關node類,替換了UIView和它的子類,而且是執行緒安全的。它可以非同步解碼圖片,調整圖片大小以及對圖片和文字進行渲染,把這些操作都放到子執行緒,滑動的時候就流暢許多。我認為這個庫最方便的就是實現圖片非同步解碼。UIImage顯示之前必須要先解碼完成,而且解碼還是同步的。尤其是在UICollectionView/UITableView 中使用 prototype cell顯示大圖,UIImage的同步解碼在滾動的時候會有明顯的卡頓。另外一個很吸引人的點是AsyncDisplayKit可以把view層次結構轉成layer。因為複雜的view層次結構開銷很大,如果不需要view特有的功能(例如點選事件),就可以使用AsyncDisplayKit 的layer backing特性從而獲得一些額外的提升。當然這個庫還處於開發階段,還有一些地方地方有待完善,比如不支援快取,我要使用這個庫的時候一般是結合Alamofire和AlamofireImage實現圖片的快取