iOS開發針對對Masonry下的FPS最佳化討論,iosmasonry
今天部落格的內容就系統的討論一下Masonry對FSP的影響,以及如何更好的使用Masonry。如果你對iOS開發足夠熟悉的話,那麼對Masonry架構應該不陌生。簡單的說,Masonry的誕生讓AutoLayout的使用更為優雅,讓控制項的布局更為方便。使用辯證的觀點來看一個事物的話,凡事都有兩面性,Masonry的使用也不例外。Masonry架構的使用不當會直接影響當UI的FPS。今天我們就來討論一下在使用Masonry時的一些誤區,看一下那些影響效能的使用方式。本篇部落格我們依然會依託於Demo來敘述的一些東西。
之前寫過一篇文章是專門來介紹Masonry架構的,並且對該架構的源碼進行了相關解析,詳細內容請移步於《iOS開發之Masonry架構源碼解析》。
一、Demo綜述
1.運行效果
先入為主,本篇部落格的內容依然是依託於我們特意為本篇部落格所打造的Demo的,首先我們先來看一下Demo運行起來是怎樣的效果,通過Demo我們可以看到那些問題,以及這些問題是如何被解決的。下方就是我們本篇部落格所涉及Demo的運行效果。
從下方的運行效果不難看出,我們是分了6種情況來觀察和判斷Masonry的各種使用方式對FPS的影響如何。上方通過六個SegmentControl可以去切換Cell的布局方式。當然每種布局方式所呈現出來的Cell是相同的。這樣也是做實驗時保持實驗項改變其他項保持一致的原則。我們可以通過右下方FPS指標來直觀的感受一下FPS的變化趨勢。下方這個FPS顯示控制項是從我們之前的Demo中拿過來的。之前的Demo也是關於FPS最佳化的,只不過是關於Cell高度動態計算的FPS最佳化,詳情請移步於《iOS開發之多種Cell高度自適應實現方案的UI流暢度分析》。
下方Cell中所顯示的資料時隨機產生的,左邊的Image也是隨機取的。右邊的Title和Detail都是NSAttributedString並且下方的有些Detail有可能為空白。如果某一條的Detail為空白,那麼該條Detail下方的所有內容的布局上移。稍後會詳細的介紹該Demo以及其中的技術點。
2、類比網路請求
上面Cell中顯示的資料是通過類比網路資料來擷取的,下方就是我們的類比網路層的相關代碼。畢竟是Demo,並且Demo的重點不在網路層上,下方就簡單的寫了一下,代碼比較簡單。就是一個單例+一個類比資料隨機產生的方法,然後把這個隨機產生的資料通過Block回調到網路層的使用者上。具體代碼如下所示:
3、上述Cell的基類XBaseTableViewCell
上面每種Segment中的Cell都是一種獨立的Cell類型,不過這些Cell有一個共同的父類。而這個父類就負責來處理這些Cell所共用的邏輯。下方的這個XBaseTableViewCell就是上述顯示的所有Cell的基類。其中聲明並初始化了Cell上的所有控制項。並且提供了相關的設定值的方法。
從下方的代碼中不難看出,有兩個方法是需要子類進行重寫的,一個是給控制項添加布局的方法addLayoutSubviews, 另一個就是更新布局的方法updateLayoutSubviews方法。每個子類中都會對這兩個方法進行重寫並給出不同的布局方式。
4、Segment中切換Cell的代碼
下方是在相應的VC中的點擊SegmentControl的邏輯代碼, 點擊不同的Segment會選擇不同的Cell然後重新整理TableView,代碼比較簡單就不做過多的贅述了。
二、對上述各種的布局方式進行分析
接下來要做的事情就是分析一下每種布局方式對FSP的影響,下方會對不同的布局情況使用Instrument進行分析並看一下具體的資料。下方分別討論了只使用Masonry的Update操作、Remake操作、先Make後Update、Frame操作以及先Make後Frame操作。
1、update
首先我們來看一下update操作。也就是使用update直接給控制項賦值,這是比較偷懶的一種操作。因為在我們的Demo中在設定cell的值時會更新一些控制項的UI布局,所有我們索性就直接使用Masonry的update,直接給控制項添加約束。在Masonry中的update操作有個特點,就是update一個約束會先在已添加的約束數組中找到該約束,然後更新該約束,如果找不到就install添加相應的約束。從這個update的功能來看其效率是比較低的。
我可以先看一下代碼實現,在子類XUpdateLayoutTableViewCell中,重寫了addLayoutSubviews和updateLayoutSubviews兩個方法。在updateLayoutSubviews方法中,為所有的控制項使用update的方式添加約束。下方這樣寫會在每次設定值的時候都會調用下方的updateLayoutSubviews方法,這樣就會更新cell上的控制項的所有布局,當然,不建議這樣去做,因為這樣會更新那些不需要更新的約束。之所以今天羅列出來,是因為在開發中下方的問題確實存在,也許是因為時間緊張,也許是因為其他原因導致的下方這種代碼實現。
我們先來使用Instruments跑一下上述的Demo,然後直觀的感受一下該Demo的Core Animation的直觀表現。下方就是我們將SegmentControl切換到Update時所對應的FPS資料。從下方的資料我們不難看出,直接用Update添加更新約束這種做法是比較影響FPS的。當然,Cell中還會使用到屬性字串,這個我們稍後會討論一下的。
我們可以來跑一下Update狀態下的Time Profile。如下所示,從下方的結果中不難看出,在Cell更新資料時,有兩塊的操作比較耗時。一個是Masonry的update操作,另一個則是Label設定NSAttributedString的操作。因為我們使用的每個Label都會賦值一個屬性字串,這個是比較耗時的操作。還有一個要明確一點的是,屬性字串的建立和產生並不會佔用多少時間,而屬性字串的賦值和渲染所佔用的時間是比較多的,這一點從下方的Time Profile中也是不難看出的。
2、remake
接下來我們在來看一下Remake操作,從下方的Core Animation的結果中不難看出,其所表現出來的效果還不如使用Update操作呢。下方的FPS比Update要低一些,這也與remake自身的操作有關係,remake從字面意思來看就是重新製作,如果之前已經添加過約束的話就先移除掉,然後再添加新的約束。
下方是Remake所對應的Time Profile,從結果中我們可以看出布局更新佔用了66.6%的耗時,而且33%的install耗時中uninstall佔用了10%左右的開銷。在Masonry中remake效率是最低的。稍後我們會繼續進行討論。
3、make + update
討論完update和remake, 我們來討論一下使用Masonry的常規做法。就是使用make來初始化控制項的布局,使用update來更新所需要更新的約束。因為代碼比較簡單,就不一一往上貼了,但是跑一下使用Instrument跑一下還是很有必要的。下方是make + update 的方式的Core Animation所跑出來的結果。但從下方的FSP結果來看,還是要比之前只使用update或者remake的效果要好一些的,不過下方的FPS還是不高,稍後我們會將下方的資料進行細化。
該部分的Time Profile就不跑了,因為設定值的時候我們依然採用的Update來更新的約束,只不過不是更新所有的約束,而是更新那些只需要更新的約束。因為更新的約束的量會少一些,所有FPS的表現效果會比之前更新所有的約束會更好一些。make + update的方式會是FPS稍微改善一些,但是從下方的圖中我們可以看出,改善的並不是特別好。
4、frame + frame
接下來,我們就不用Masonry來布局了,我們直接使用Frame布局。因為Autolayout最終仍然會轉換為Frame布局的,很顯然Frame布局在效能方面是優於Autolayout布局的。接下來我們就來使用Frame布局然後使用Frame更新。下方的FPS還算說得過去,不過還不是滿格,其大部分原因就是因為NSAttrubitedString的原因了。
我們可以看一下更新Frame的Time Profile,如下所示。從下方的中,我們不難看出update frame的時間佔比只佔到了2.5%,之前更新約束能佔到60%左右,可以看到使用Frame布局的好處。從下方的分析結果中不難看出,現在影響FPS主要因素已經從更新布局轉化到了AttributeString的設定。這也是上述FPS沒有滿格的原因。
6、make + frame
Masonry的誕生就是為了方便控制項布局的,Frame布局不夠靈活,適配起來比較繁瑣,所以才有了AutoLayout。不過雖然AutoLayout可以很方便的適配螢幕,可是其效能方面表現的不是特別好。那麼我們可不可以將兩者進行結合呢。也就是說使用make來初始化控制項的布局,使用Frame來更新布局。當然這一過程不是簡單的在設定值的時候更新一下Frame就可以的,因為在Cell設定值的時候去更新Frame是沒用的,因為更新完Frame後,在渲染顯示的時候,還是會按照AutoLayout的布局來顯示的。我們需要做的是將Frame布局放到Autolayout布局之後,此處我們要做的就是將更新Frame的相關代碼放到下一個Runloop中來執行。更新Frame代碼如下:
在cell中是make初始化控制項布局,使用Frame更新布局,和Frame+Frame的方式差不多,只不過是使用Masonry布局時,在首屏載入的時候不如Frame布局,以後更新是一樣的。下方是使用Masonry+Frame的形式的Core Animation的結果。效果雖然比上一部分會稍微差一些,但是最終的效果還是滿Ok的。
三、總結
本篇部落格只討論Masonry的布局方式對FPS的影響,至於上述的NSAttributeString的問題不做過多贅述了。如果根據業務需要,有許多富文本的展示影響了FPS的話,那麼可以採用其他的方式來進行最佳化,比如使用AsynDisplayKit所提供的相關Node進行顯示等等。在部落格的結尾,還是有必要做一個總結的。
下方是我們在代碼中更為細化的資料,從資料中不難看出Remake的效能是最差的,所以我們在使用Masonry時盡量要少使用Remake。對控制項的更新只一味的選擇使用Update也不是一個好的選擇,如果要使用Masonry架構還要對控制項進行布局更新的話,最好是把那些不變的約束和需要更新的約束分開。使用make來添加相關約束,使用update來修改需要更新的約束。當然使用Frame布局的效能會好一些,不過版面配置階段過於繁瑣不便於進行螢幕的適配。當然也可以使用Masonry進行布局使用Frame進行布局的更新,當然需要注意的是Frame布局更新的時機,需在Autolayout載入的時機後進行。
下方是進行了統一的資料統計,當然是針對本篇部落格所對應的Demo的。下方表格中統計了一次更新cell布局所採用的不同方式的平均時間,從下方的資料中我們不難看粗Remake的更新布局用時最多,消耗了12+ms, 而Update所有的約束用時也是不少,一次更新布局使用了9+ms。而只更新需要更新的布局用時7+ms, 稍微要比更新所有的布局要好一些。當然直接修改Frame的用時最少,只用了0.06+ms的時間,從該資料可以直觀的感受到Frame布局的效率性。
而右邊還給出了一個屬性字串的建立和賦值的用時,其中我們可以看到,屬性字串的建立耗時並不是太多,而比較耗時的是屬性字串的賦值,每次賦值佔用了0.7ms, 如果是10個的話,那麼賦值時間就是7ms, 如果屬性字串的內容再複雜一些,那麼用時肯定會比這個高。當然我們可以使用第三方提供的一些控制項和方法將這部分時間給最佳化掉,這個可以放到以後再討論。
今天的部落格就到這兒吧,目的是在使用Masonry時要合理的進行使用,有必要時,可以使用Frame進行布局。
上述demo -github分享連結:https://github.com/lizelu/FPSProfileDemo