提供了基於Swift3.0模仿的新浪微博的Demo,大家可以下載看一看:基於Swift3.0高仿的微部落格戶端,裡面針對於微博首頁的複雜頁面的優化做了很多的處理,頁面的FPS 一直保持在59 ~ 60 。
看下demo的效果:
CPU 和GPU
關於繪圖和動畫有兩種處理方式CPU(中央處理器)和GPU(圖形處理器),CPU的工作都在軟體層面,而GPU的在硬體層面。
總的來說,可以使用CPU做任何事情,但是對於影象的處理,通常GPU會更快,因為GPU使用影象對高度並行浮點運算做了優化(儘管我也不知道是什麼鬼??),所以,我們想盡可能的把螢幕渲染的工作交給硬體去處理,而問題在於GPU並沒有無限制處理的效能,一旦資源用盡,即使CPU並沒有完全佔用,GPU效能還是會下降。
所以,目前大多的效能優化都是關於智慧利用GPU和CPU,平衡它們之間工作負載。
測量,而不是猜測
現在知道哪些可能會影響效能,該如何修復呢?有許多傳統的詭計來優化,但如果盲目使用的話,可能會造成更多效能上的問題,而不是優化了
如何正確的測量而不是猜測這點很重要,根據效能相關知識寫出的程式碼不同於倉促優化,前者是正確的姿勢,後者則是在浪費生命
那改如何測量,第一步就是確保在真實環境下測試你的程式
真機測試,而不是模擬器
當你開始做一些效能方面的工作時候,一定要在真機上測試,而不是模擬器,模擬器雖然可以加快開發效率,但卻不能準確提供真機的效能引數
模擬器執行在Mac上,然而Mac上的cpu比ios裝置要快很多,相反,Mac上的GPU和ios裝置上的不一樣,模擬器不得已需要在軟體層面(CPU)模擬ios裝置,所以GPU的相關操作在模擬器上面執行的會更慢
另一件重要的就是效能測試的時候一定要用釋出配置,而不是除錯模式,因為當用釋出環境打包的時候,編譯器會引入一些提高效能的優化,比如:去除除錯符號或者移除並重新組織程式碼,因為可以自己做到這些,比如禁用NSlog、print語句,因為 ,只需要關心釋出效能。
測試幀率
可以在程式中使用CADisplayLink來測量幀率,在螢幕上顯示出來,我用Swift3.0模仿YY大神的程式碼寫了一個簡單的FPS指示器 YWFPSLabel,使用CADisplayLink監視FPS數值,日常開發的時候,可以有直接的體現,不用再靠猜了…
YWFPSLabel整合也很方便,在AppDelegate的application方法里加下面兩句即可
1 2 |
let FPSLab = YWFPSLabel(frame: CGRect()) UIApplication.shared.keyWindow!.addSubview(FPSLab) |
不知道大家有木有看到頭部那個小label啊~~~
但是應用內的FPS顯示並不能完全真實的測量出效能,因為它僅僅能測試出應用內的幀率,還有很多是動畫都是在應用外發生(在渲染程式中處理),但應用內FPS計數可以對一些效能問題提供參考,一旦找到問題,需要更多的精確詳細資訊來定位問題所在,我們就要使用Instuments了,它可以看到更多準確是資訊,檢視到所有與顯示的資料。
Instuments
Instuments是Xcode套件中沒有被充分利用的工具,很多iOS開發者從來沒用過Instrument,特別是通過短暫培訓出來的同學們,所以,很多面試官也會問效能條調優方面的知識,來判斷面試的同學是否真正應用對年開發經驗。
-
Activity Monitor
個人覺的很像Windows的工作管理員,可以檢視所有的程式,以及程式的記憶體、cpu使用百分比等資料等,就不多介紹了,開啟一看大概就知道怎麼回事
-
Allocations
管理記憶體是app開發中最重要的一個方面,對於開發者來說,在程式架構中減少記憶體的使用通常都是使用Allocations去定位和找出減少記憶體使用的方式
接下來,談一下記憶體洩漏的兩種情況 - 第一種:為物件A申請了記憶體空間,之後再也沒用過物件A,也沒釋放過A導致記憶體洩漏,這種是Leaked Memory記憶體洩漏
- 第二種:類似於遞迴,不斷地申請記憶體空間導致的記憶體洩漏,這種情況是Abandoned Momory
此工具可以讓開發者很好的瞭解每個方法佔用記憶體的情況,並定位相關的程式碼右鍵就可以開啟Xcode自動定位到相關佔用記憶體方法的程式碼上
第二種情況可以根據下圖的操作清晰的找到對用的程式碼問題
解釋一下,第二種情況我們應該如何操作,重複的執行一系列的操作時候記憶體不會繼續增加,比如開啟和關閉一個視窗,這樣的操作,每一次操作的前後,記憶體應該是相同的,通過多次迴圈操作,記憶體不會遞增下去,通過這種分析結果,觀察記憶體分配趨勢,當發現不正確的結果或者矛盾的結果,就可以研究是不是Abandoned Momory的問題,並可以修正這個問題了
Core Animation
之前說過自己寫的YWFPSLabel只能檢測應用內的FPS,而此工具考慮到了程式外的動畫,理想的FPS值為60左右,過低的話就用該進性優化了,根據WWDC的說法,當FPS 低於45的時候,使用者就會察覺到到滑動有卡頓
圈著數字紅色方框中的數字,代表著FPS值,理論上60最佳,實際過程中59就可以了,說明就是很流暢的,說明一下操作方式:在手指不離開螢幕的情況下,上下滑動螢幕列表
介紹一下Deug Display中選項的作用
-
Color Blended Layers(混合過度繪製)
開啟此選項螢幕的效果圖如下:
這個選項基於渲染程度對螢幕中的混合區域進行綠到紅的高亮(也就是多個半透明圖層的疊加),由於重繪的原因,混合對GPU效能會有影響,同時也是滑動或者動畫掉幀的罪魁禍首之一
GPU每一幀的繪製的畫素有最大限制,這個情況下可以輕易繪製整個螢幕的畫素,但如果發生重疊畫素的關係需要不停的重繪同一區域的,掉幀和卡頓就有可能發生
GPU會放棄繪製那些完全被其他圖層遮擋的畫素,但是要計算出一個圖層是否被遮擋也是相當複雜並且會消耗CPU的資源,同樣,合併不同圖層的透明重疊元素消耗的資源也很大,所以,為了快速處理,一般不要使用透明圖層,
1). 給View新增一個固定、不透明的顏色
2). 設定opaque 屬性為true
但是這對效能調優的幫助並不大,因為UIView的opaque 屬性預設為true,也就是說,只要不是認為設定成透明,都不會出現圖層混合
而對於UIIimageView來說,不僅需要自身需要不是透明的,它的圖片也不能含有alpha通道,這也上圖9張圖片是綠色的原因,因此影象自身的性質也可能會對結果有影響,所以你確定自己的程式碼沒問題,還出現了混合圖層可能就是圖片的問題了
而針對於螢幕中的文字高亮成紅色,是因為一沒有給文字的label增加不透明的背景顏色,而是當UILabel內容為中文時,label的實際渲染區域要大於label的size,因為外圍有了一圈的陰影,才會出現圖層混合我們需要給中文的label加上如下程式碼:
1 2 3 4 |
retweededTextLab?.layer.masksToBounds = true retweededTextLab?.backgroundColor = UIColor.groupTableViewBackground statusLab.layer.masksToBounds = true statusLab.backgroundColor = UIColor.white |
看下效果圖:
那些label的顏色也變成藍色的了,這裡有一點需要說明一下,
1). statusLab.layer.masksToBounds = true
單獨使用不會出現離屏渲染
2). 如果對label設定了圓角的話,圓角部分會離屏渲染,離屏渲染的前提是點陣圖發生了形變
-
Color Hits Green and Misses Red(光柵化快取圖層的命中情況)
這個選項主要是檢測我們有無濫用或正確使用layer的shouldRasterize屬性.成功被快取的layer會標註為綠色,沒有成功快取的會標註為紅色。
很多檢視Layer由於Shadow、Mask和Gradient等原因渲染很高,因此UIKit提供了API用於快取這些Layer,self.layer.shouldRasterize = true
系統會將這些Layer快取成Bitmap點陣圖供渲染使用,如果失效時便丟棄這些Bitmap重新生成。圖層Rasterization柵格化好處是對重新整理率影響較小,壞處是刪格化處理後的Bitmap快取需要佔用記憶體,而且當圖層需要縮放時,要對刪格化後的Bitmap做額外計算。 使用這個選項後時,如果Rasterized的Layer失效,便會標註為紅色,如果有效標註為綠色。當測試的應用頻繁閃現出紅色標註圖層時,表明對圖層做的Rasterization作用不大。
在測試的過程中,第一次載入時,開啟光柵化的layer會顯示為紅色,這是很正常的,因為還沒有快取成功。但是如果在接下來的測試,。例如我們來回滾動TableView時,我們仍然發現有許多紅色區域,那就需要謹慎對待了
-
Color Copied Image (拷貝的圖片)
這個選項主要檢查我們有無使用不正確圖片格式,由於手機顯示都是基於畫素的,所以當手機要顯示一張圖片的時候,系統會幫我們對圖片進行轉化。比如一個畫素佔用一個位元組,故而RGBA則佔用了4個位元組,則1920 x 1080的圖片佔用了7.9M左右,但是平時jpg或者png的圖片並沒有那麼大,因為它們對圖片做了壓縮,但是是可逆的。所以此時,如果圖片的格式不正確,則系統將圖片轉化為畫素的時間就有可能變長。而該選項就是檢測圖片的格式是否是系統所支援的,若是GPU不支援的色彩格式的圖片則會標記為青色,則只能由CPU來進行處理。CPU被強制生成了一些圖片,然後傳送到渲染伺服器,而不是簡單的指向原始圖片的的指標。我們不希望在滾動檢視的時候,CPU實時來進行處理,因為有可能會阻塞主執行緒。
-
Color Immediately (顏色立即更新)
通常 Core Animation 以每秒10此的頻率更新圖層的除錯顏色,對於某些效果來說,這可能太慢了,這個選項可以用來設定每一幀都更新(可能會影響到渲染效能,所以不要一直都設定它)
-
Color Misaligned Image (圖片對齊方式)
這裡會高亮那些被縮放或者拉伸以及沒有正確對齊到畫素邊界的圖片,即圖片Size和imageView中的Size不匹配,會使圖過程片縮放,而縮放會佔用CPU,所以在寫程式碼的時候保證圖片的大小匹配好imageView,如下圖所示:
圖片尺寸 170 * 220px
1 2 3 |
let imageView = UIImageView(frame: CGRect(x: 50, y: 100, width: 170, height: 220)) imageView.image = UIImage(named: "cat") view.addSubview(imageView) |
蘋果的單位以點計算,而 imageView的尺寸是170 220 pt 而圖片是 170 220px,所以相當於在螢幕上對圖片方法了一倍,看效果圖如下:
可以看到圖片高亮成黃色顯示,更改下imageView的大小
1 2 3 |
let imageView = UIImageView(frame: CGRect(x: 50, y: 100, width: 85, height: 110)) imageView.image = UIImage(named: "cat") view.addSubview(imageView) |
看下效果圖
當imageView和image的大小一致的時候,就正常顯示了
-
Color Offscreen- Rendered Yellow (離屏渲染)
這裡會把那些需要離屏渲染的到圖層高亮成黃色,而出發離屏渲染的可能有
1 2 3 4 5 6 |
/* 圓角處理 */ view.layer.maskToBounds = truesomeView.clipsToBounds = true /* 設定陰影 */ view.shadow.. /* 柵格化 */ view.layer.shouldRastarize = true |
針對柵格化處理,我們需要指定螢幕的解析度
1 2 3 4 5 6 7 8 9 10 |
//離屏渲染 - 非同步繪製 耗電 self.layer.drawsAsynchronously = true //柵格化 - 非同步繪製之後 ,會生成一張獨立的圖片 cell 在螢幕上滾動的時候,本質上滾動的是這張圖片 //cell 優化,要儘量減少圖層的數量,想當於只有一層 //停止滾動之後,可以接受監聽 self.layer.shouldRasterize = true //使用 “柵格化” 必須指定解析度 self.layer.rasterizationScale = UIScreen.main.scale |
指定陰影的路徑,可以防止離屏渲染
1 2 |
// 指定陰影曲線,防止陰影效果帶來的離屏渲染 imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath |
這行程式碼制定了陰影路徑,如果沒有手動指定,Core Animation會去自動計算,這就會觸發離屏渲染。如果人為指定了陰影路徑,就可以免去計算,從而避免產生離屏渲染。
設定cornerRadius本身並不會導致離屏渲染,但很多時候它還需要配合layer.masksToBounds = true使用。根據之前的總結,設定masksToBounds會導致離屏渲染。解決方案是儘可能在滑動時避免設定圓角,如果必須設定圓角,可以使用光柵化技術將圓角快取起來:
1 2 3 4 5 |
// 設定圓角 label.layer.masksToBounds = true label.layer.cornerRadius = 8 label.layer.shouldRasterize = true label.layer.rasterizationScale = layer.contentsScale |
如果介面中有很多控制元件需要設定圓角,比如tableView中,當tableView有超過25個圓角,使用如下方法
1 2 |
view.layer.cornerRadius = 10 view.maskToBounds = Yes |
那麼fps將會下降很多,特別是對某些控制元件還設定了陰影效果,更會加劇介面的卡頓、掉幀現象,對於不同的控制元件將採用不同的方法進行處理:
1). 對於label類,可以通過CoreGraphics來畫出一個圓角的label
2). 對於imageView,通過CoreGraphics對繪畫出來的image進行裁邊處理,形成一個圓角的imageView,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/// 建立圓角圖片 /// /// - parameter radius: 圓角的半徑 /// - parameter size: 圖片的尺寸 /// - parameter backColor: 背景顏色 預設 white /// - parameter lineWith: 圓角線寬 預設 1 /// - parameter lineColor: 線顏色 預設 darkGray /// /// - returns: image func yw_drawRectWithRoundCornor(radius: CGFloat, size: CGSize, backColor: UIColor = UIColor.white, lineWith: CGFloat = 1, lineColor: UIColor = UIColor.darkGray) -> UIImage? { let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) let bezier = UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.allCorners, cornerRadii: CGSize(width: radius, height: radius)) backColor.setFill() UIRectFill(rect) bezier.addClip() draw(in: rect) bezier.lineWidth = 1 lineColor.setStroke() bezier.stroke() let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return result } |
-
Color Compositing Fast-Path Blue
這個選項會對任何直接使用OpenGL繪製的圖層進行高亮,如果僅僅使用UIKit或者Core Animation的API,不會有任何效果,恕我才疏學淺,所以,我在測試的時候,確實在螢幕上沒有任何反應的,openGL 繪製,我也不會,所以,就不知道到底會有什麼效果了,哪位大神會的話,貼段程式碼,給我試試啊~~~
-
Flash Updated Regions (Core Graphics 繪製的圖層)
此選項會對重繪的內容進行高亮成黃色,也就是軟體層面使用Core Graphics 繪製的圖層。我測試的時候,好像有點問題,這種解釋,不知道是不是我寫程式碼的問題,所以,就不多說了
上面說的這些高亮圖層,幾個常用的選項在模擬器裡面可以直接除錯,非常方便
紅框中的選項,上面都有解釋,這裡就不說了,勾選項,開啟模擬器,一看就知道了~
麻蛋、Core Animation 部分終於扯完了,扯了好多啊。。。
-
Leaks
又一個灰常重要的工具,主要檢查記憶體洩漏,在前面Allcations裡面我們提到記憶體洩漏分兩種,現在我們研究Leaked Memory, 從使用者使用角度來看,記憶體洩漏本身不會產生什麼危害,作為使用者,根本感覺不到記憶體洩漏的存在,真正的危害在於記憶體洩漏的堆積,最終會耗盡系統所有的記憶體。我們直接看圖:
介面的介紹
在 instruments 中,雖然選擇了 Leaks 模板,但預設情況下也會新增 Allocations 模板.基本上凡是記憶體分析都會使用 Allocations 模板, 它可以監控記憶體分佈情況。
選中 Allocations 模板3區域會顯示隨著時間的變化記憶體使用的折線圖,同時在4區域會顯示記憶體使用的詳細資訊,以及物件分配情況.
點選 Leaks 模板, 可以檢視記憶體洩露情況。如果在3區域有 紅X 出現, 則有記憶體洩露, 4區域則會顯示洩露的物件.
打用leaks進行監測:點選洩露物件可以在(下圖)看到它們的記憶體地址, 佔用位元組, 所屬框架和響應方法等資訊.開啟擴充套件檢視, 可以看到右邊的跟蹤堆疊資訊,4 黑色程式碼最有可能出現記憶體洩漏的方法
監測結果的分析,
Time Profiler
在開發的過程中,我們經常能感覺到,點選某一按鈕,或者做了某一操作,有卡頓,這就是延遲,那使用此工具,就可以揪出耗時的函式,先看一下,除錯介面介紹:
根據檢視的相關耗時操作,我們就可以右鍵定位當耗時的方法:
寫一個簡單例子看一下:
程式碼截圖如下:
這時候,我們把迴圈放到子執行緒來做
1 2 3 4 5 6 7 8 9 10 11 |
@IBAction func btnAction(_ sender: AnyObject) { let svc = SecondViewController() svc.title = "第二個頁面" //全域性佇列非同步執行 DispatchQueue.global().async { for i in 0..<8888 { print(i) } } navigationController?.pushViewController(svc, animated: true) } |
看效果圖:
到這裡比較重要Instrument除錯工具介紹的差不多了,說一個Xcode8.0新出的功能,很好用也很重要的功能:
還是以例子說說吧,Viewcontroller裡面一個button,點選跳到SecondViewcontroller,SecondViewcontroller裡面有個View,view裡面有個button,button點選回到ViewController,實現是通過view的屬性拿到SecondviewConroller的引用,pop回去
子view的程式碼如下:
1 2 3 4 5 6 |
class SubView: UIView { var delegate: SecondViewController? @IBAction func brnAction(_ sender: AnyObject) { delegate?.navigationController!.popViewController(animated: true) } } |
當我們從第二個控制器,回到第一個控制器的時候,我們點一下,剛那個按鈕,看圖:
第二個控制器和子View都記憶體中,我們很容易,就可以發現問題了,這是因為,SecondViewController強引用了SubView,而我們SubView也強引用了SecondViewcontroller,就造成相互強引用,引用計數器不能為0,不能銷燬了,我們只要把屬性前面加個weak,變成弱引用就可以了
1 2 3 4 |
weak var delegate: SecondViewController? @IBAction func brnAction(_ sender: AnyObject) { delegate?.navigationController!.popViewController(animated: true) } |
這時候,我們從第二個控制器pop回來的時候,看下記憶體:
現在就沒問題了,怎樣這個工具是不是挺好用啊
還有一些針對TableView,collectionView的優化,有空再聊吧,
- 宣告:
1.本文有些文字描述,來自於其他部落格和文章,因為覺的描述挺好(好吧我承認、自己也寫不出來),所以就拿來用了,若有不妥的地方,請告知我,我會立即刪除修改
2.因為本人的水平有限,可能有寫的不對的地方,歡迎各位大大多指點、不勝感激
最後的最後:碼字不易,如果對你有用,點個喜歡唄!