原文連接 : Speed up your app
原文做者 : UDI COHEN
譯文出自 : 開發技術前線 www.devtf.cn。未經容許,不得轉載!
譯者 : zijianwang90
校對者:
狀態 : 完成html
幾周以前,我在Droidcon NYC上有過一次關於Android性能優化的演講。java
我在這個演講中花費了大量的時間,由於我想經過真實的例子展示性能問題,以及我是經過什麼樣的工具去發掘這些問題的。由於時間緣由,在演講中我不得不捨棄一半的內容。在這篇文章中,我會總結在演講中我所討論的全部內容,而且給出實例android
點此連接進入演講視頻git
如今,咱們來逐一討論我在演講中說起的一些重點內容,但願個人闡述足夠的清晰。首先,在我進行性能優化的時候我遵循以下原則:github
每當我遇到性能問題,或者嘗試發現性能問題的時候,我會遵循以下原則:web
Systrace是一個很是好但卻有可能被你忽視的工具,這是由於開發者們每每不肯定Systrace可以爲他們提供什麼樣的信息。shell
Systrace會展現一個運行在手機上程序情況的概覽。這個工具提醒了咱們手機實際上是一個能夠在同一時間完成不少工做的電腦。在最近的一次SDK更新中,這個工具在數據分析能力上獲得了提高,用以幫助咱們尋找性能問題之所在。緩存
下面讓咱們來看看Systrace長什麼樣子:安全
你能夠經過Android Device Monitor Tool或者是命令行來生成Systrace文件,想了解更多猛戳此處。性能優化
在視頻中,我向你們介紹了Systrace中不一樣區域的功能。固然最有趣的仍是Alerts和Frames兩欄,它們展現了經過手機來的數據而生成出來的可視化分析結果。讓咱們來選擇最上方的alerts瞧瞧:
這個警告指出了,有一個View#draw()方法執行了比較長的時間。咱們能夠在下面看到問題的描述,連接,甚至是相關的視頻。下面咱們看 Frames這一行,能夠看到這裏展現了被繪製出來的每一幀,而且用綠、黃、紅三顏色來區分它們在繪製時的性能。咱們選一個紅色幀來瞅瞅:
在最下方,咱們看到了與這一幀所相關的一些警告。在這三個警告中,有一個是咱們上面所提到的(View#draw())。接下來咱們在這一幀處放大並在下方展開「Inflation during ListView recycling」這條警告:
咱們能夠看到警告部分的總耗時,32毫秒,遠高於了咱們對保障60fps所需的16毫秒繪製時間。同時還有更多的ListView每一個條目的繪製時 間,大約是6毫秒每一個條目,總共五個。而Description描述項中的內容會幫助咱們理解問題,甚至提供問題的解決方案。回到咱們上一張圖片,咱們可 以在「inflate」這一個塊區處放大,而且觀察究竟是哪些View在被填充過程當中耗時比較嚴重。
下面是另一個渲染過慢的實例:
在選擇了某一幀以後,咱們能夠按「m」鍵來高亮這一幀,而且在上方看到了這一部分的耗時,如圖,咱們看到了這一陣的繪製總共耗時超過19毫秒。而當咱們展開這一幀惟一的一個警告時,咱們發現了「Scheduling delay」這條錯誤。
Scheduling delay(調度延遲)的意思就是一個線程在處理一塊運算的時候,在很長一段時間都沒有被分配到CPU上面作運算,從而致使這個線程在很長一段時間都沒有完成工做。咱們選擇這一幀中最長的一塊,從而獲得更加詳細的信息:
在紅框區域內,咱們看到了「Wall duration」,他表明着這一區塊的開始到結束的耗時。之因此叫做「Wall duration」,是由於他就像是牆上的一個時鐘,從線程的一開始就爲你計時。
可是,CPU Duration一項中顯示了實際CPU在處理這一區塊所消耗的時間。
很顯然,兩個時間的差距仍是很是大的。整個區塊耗時18毫秒,而在這之中CPU只消耗了4毫秒的時間去運算。這就有點奇怪了,因此咱們應該看一下在這整個過程之中,CPU去幹嘛了。
能夠看到,全部四個線程都很是的繁忙。
選擇其中的一個線程會告訴咱們是哪一個程序在佔用他,在這裏是一個包名爲com.udinic.keepbusyapp的程序。在這裏,因爲另一個程序佔用CPU,致使了咱們的程序未能得到足夠的CPU資源。
可是這種狀況實際上是暫時的,由於被其餘後臺應用佔用CPU的狀況並很少見(- -),但仍有其餘應用的線程或是主線程佔用CPU。而Traceview也只能爲咱們提供一個概覽,他的深度是有限的。因此要找到咱們app中究竟是什麼 讓咱們的CPU繁忙,咱們還要藉助另外一個工具——Traceview。
Traceview是一個性能測試工具,展現了全部方法的的運行時間。下面讓咱們來瞅瞅他是啥樣的:
這個工具能夠從Android Device Monitor中打開也能夠經過代碼打開。更多的消息信息清看這裏。
下面讓咱們來看看每一列的含義:
我打開一個滑動不太順滑的應用。開啓記錄,滑動一點後中止記錄。展開getView()方法,以下圖:
這個方法被調用了12次,每次CPU會消耗3毫秒左右,可是每次調用的總耗時卻高達162毫秒!絕對有問題啊!
而看看這個方法的children,咱們能夠看到這其中的每一個方法在耗時方面是如何分佈的。Thread.join()方法戰局了98%的 inclusive real time。這個方法在等待另外一個線程結束的時候被調用。在Children中另一個方法就是Tread.start()方法,而之因此整個方法耗時很 長,我猜想是由於在getView()方法中啓動了線程而且在等待它的結束。
可是這個線程在哪兒?
咱們在getView()方法中並不能看到這個線程作了什麼,由於這段邏輯不在getView()方法之中。因而我找到了Thread.run()方法,就是在線程被建立出來時候所運行的方法。而跟隨這個方法一路向下,我找到了問題的元兇。
我發現了BgService.doWork()方法的每次調用花費了將近14毫秒,而且有四十個這東西!並且getView()中還有可能調用屢次 這個方法,這就解釋了爲何getView()方法執行時間如此之長。這個方法讓CPU長時間的保持在了繁忙狀態。而看看Exclusive CPU time,咱們能夠看到他佔據了80%的CPU時間!此外,根據Exclusive CPU time排序,能夠幫咱們更好的定位那些耗時很長的方法,而他們頗有可能就是形成性能問題的罪魁禍首。
關注這些耗時方法,例如getView(),View#onDraw()等方法,能夠很好的幫助咱們尋找爲何應用運行緩慢的緣由。但有些時候,還 會有一些其餘的東西來佔用寶貴的CPU資源,而這些資源若是被運用在UI的繪製上,也許咱們的應用會更加流暢。Garbage Collector垃圾回收機制會不時的運行,回收那些沒用的對象,一般來說這不會影響咱們在前臺運行的程序。但若是GC被運行的過於頻繁,他一樣能夠影 響咱們應用的執行效率。而咱們該如何知道回收的是否過於頻繁了呢…
Android Studio在最近的更新中給予了咱們更增強大的工具去分析性能問題。在底部Android選項中的Memory選項卡,會顯示有多大的數據在何時被分配到了堆內存之中,他是長成這個樣子的:
而當圖表中出現一個小的下滑的時候,說明GC回收發生了,他清除了沒必要要的對象而且騰出了必定的堆空間。而在這張圖表的左側有兩個工具供咱們使用,Head dump和Allocation Tracker。
爲了找出究竟是什麼正在佔用咱們的堆內存,咱們可使用左邊的heap dump按鈕。他會提供一個堆內存佔用狀況的快照,而且會在Android Studio中打開一個單獨的報告界面。
在左側,咱們看到一個圖標展現了堆中全部的實例,按照類進行分組。而對於每個實例,會展現有多少個實例的對象被分配到堆中,以及他們的所佔用的空 間(Shallow size淺尺寸),以及這些對象在內存中仍然佔用的空間,後者告訴了咱們多少的內存空間將會被釋放若是這些實例被釋放。這個工具可讓咱們直觀的觀察處內 存是被如何佔用的,幫助咱們分析咱們使用的數據結構和對象之間的關係,以便發現問題並使用更加高效的數據結構,解開和對象之間的關聯,而且下降 Ratained Memory的佔用。而最終目的,就是儘量的下降咱們的內存佔用。
回過頭來看圖表,咱們發現MemoryActivity存在39個實例,這對於一個Activity來講有點奇怪。在右邊選擇其中的一個實例,會在下方看到全部的對這個實例的引用樹狀列表。
其中一個是ListenersManager對象中的一個集合。而觀察這個activity的其餘實例,就會他們都由於這個對象而被保留在了內存之中。這也解釋了爲何這些對象佔用瞭如此多的內存:
這個現象就叫作「內存泄露」,咱們的activity已經被銷燬,可是他們的對象卻由於始終被引用着而沒法被垃圾回收。咱們能夠避免這種狀況,例如 確保這些對象再被銷燬後不會被其餘對象一直引用着。在咱們這個例子中,在Activity被銷燬後,ListernesManager並不須要保持着對這 些對象的引用。因此解決辦法就是在onDestroy()回調方法中移除這些引用。
內存泄露以及其餘較大的對象會在堆中佔據不少的控件,它們減小着可用內存的同時也頻繁的形成垃圾回收。而垃圾回收又回形成CPU的繁忙,而堆內存並不會變得更大,最終就會致使更悲劇的結果發生:OutOfMemoryException內存溢出,並致使程序崩潰。
另一個更先進的工具就是Eclipse Memory Analyzer Tool (Eclipse MAT):
這個工具能夠作全部Android Studio能夠作的,而且辨別可能出現的內存泄露,以及提供更加高級的搜索功能,例如搜索全部大於2MB的Bitmap實例,或是搜索全部空的Rect對象。
另一個很好的工具是LeakCanary,是一個第三方庫,能夠觀察應用中的對象而且確保它們沒有形成泄漏。而若是形成泄漏了,會有一個推送來提醒你在哪裏發生了什麼。
咱們能夠在內存圖表的左側找到Allocation Tracker的啓動和中止按鈕。他會生成一個在必定時間內被生成的全部實例的報告,而且按照類分型分組:
或者按照方法分組:
同時它還能經過美觀的可視化界面,告訴咱們哪些方法或類擁有最多的實例。
利用這些信息,咱們能夠找到哪些佔用過多內存,引起過屢次垃圾回收且又對耗時很是敏感的方法。咱們也能夠利用這個工具找到不少短命的相同類的實例,從而能夠考慮使用對象池的思想去儘可能的減小過多的實例被建立。
如下是一些我寫代碼時候遵循的規律或是技巧:
在Android 1.4中的一個全新工具,就是能夠查看GPU繪製。
每一條線意味着一幀被繪製出來,而每條線中的不一樣顏色又表明着在繪製過程當中的不一樣階段:
在Marshmallow版本中,有更多的顏色被加了進來,例如Measure/Layout階段,input handling輸入處理,以及一些其餘的:
在使用這些功能以前,你須要在開發者選項中開啓GPU rendering(GPU呈現模式分析):
接下來咱們就能夠經過如下這條adb命令獲得咱們想要獲得的全部信息:
咱們能夠本身收集這些信息並建立圖表。這個命令也會打印出一些其餘有用的信息,例如view層級中的層數,display lists的大小等等。在Marshmallow中,咱們也會獲得更多的信息:
若是咱們須要自動化測試咱們的app,那麼咱們能夠本身建立服務器去運行在特定節點執行這些命令(如列表滾動,重度動畫等),並觀察這些數值的變 動。這能夠幫助咱們找出在哪裏出現了性能的降低,而且產品上線以前找到問題的所在。咱們也可以經過」framestats」關鍵字來找到更多更加精確的數 據,這裏有更詳盡的解釋。
但這可不是獲取GPU Rendering數據的惟一方式!
咱們在開發者選項中看過了GPU呈現模式分析內的Profile GPU Rendering」選項後,還有另一個選項就是」On screen as bars」(在屏幕上顯示爲條形圖)。打開這個後,咱們就能夠直觀的看到每一幀在繪製過程當中所消耗的時間,綠色的橫線則表明16ms的60fps零界值。
在右邊的例子中,咱們能夠看到不少幀都超出了綠線,這也意味着它花了多餘16毫秒的時間去繪製。而藍色佔據了這些線條的主體,咱們知道這多是由於 過多或是過於複雜的view在被繪製。在這種狀況下,當我滑動列表,由於列表中view的結構比較複雜,有一些view已經被繪製完成而一些由於過於複雜 還處於繪製階段,而這可能就是形成這些幀超過綠線的緣由——繪製起來實在太複雜了。
我很是喜歡這個工具,同時也由於那麼多人徹底不用而感到一絲的悲涼。
使用Hierarchy Viewer,咱們能夠得到性能數據,觀察View層級中的每個View,而且能夠看到全部View的屬性。咱們一樣能夠導出theme數據,這樣能夠 看到每個style中的屬性值,可是咱們只能在單獨運行Hierarchy Viewer的時候才能這麼幹,而非經過Android Monitor。一般在我進行佈局設計以及佈局優化的時候,我會使用到這個工具。
在正中間咱們看到的樹狀結構就表明了View的層級。View的層級能夠很寬,但若是太寬的話(10層左右),也許會在佈局和測量階段消耗大量的性 能。在每一次View經過View#onMeasure()方法測量的時候,或是經過View#onLayout()方法佈局他的全部子view的時候, 這些方法又回傳遞到它全部的子view上面而且重頭來過。有的佈局會將上述步驟作兩次,例如RelativeLayout以及某些經過配置的 LinearLayout,而若是它們又層層嵌套,那麼這些方法的傳遞會大量的增長。
在右下方,咱們看到了一個咱們佈局的「藍圖」,標註了每個view的位置。當咱們點擊這裏(或者從樹狀結構中),咱們會在左側看到他全部的屬性。 在設計佈局時候,有時候我不肯定爲何一個view被擺在那裏,而使用這個工具,我能夠在樹狀圖中找到這個view,選擇,並觀察他在預覽窗口中的位置。 我還經過view在屏幕上最終的繪製尺寸,來設計有趣的動畫,而且使用這些信息讓動畫或者View的位置更加的精準。我也能夠經過這個工具來尋找被其餘 View不當心蓋住從而找不到的View,等等等等。
對於每個view咱們能夠得到他測量、佈局以及繪製的用是和它所包含的全部子view。在這裏顏色表明了這個view在繪製過程當中,相比樹中其餘 view的性能表現,這是咱們找到這些性能不足view的最佳途徑。鑑於咱們可以看到全部view的預覽,咱們能夠沿着樹狀圖,跟隨view被建立的順 序,找尋那些能夠被捨棄的多餘步驟。而其中之一,也是對性能影響很是大的,就是過分繪製。
正如咱們在GPU Profiling部分看到的,在Execute黃色階段,若是GPU有過多的東西要在屏幕上繪製,整個階段會消耗更多的時間,同事也增長了每一幀所消耗 的時間。過分繪製每每發生在咱們須要在一個東西上面繪製另一個東西,例如在一個紅色的背景上畫一個黃色的按鈕。那麼GPU就須要先畫出紅色背景,再在他 上面繪製黃色按鈕,此時過分繪製就是不可避免的了。若是咱們有太多層須要繪製,那麼則會過分的佔用GPU致使咱們每幀消耗的時間超過16毫秒。
使用「Debug GPU Overdraw」(調試過分繪製)功能,全部的過分繪製會以不一樣顏色的形式展現在屏幕上。1x或是2x的過分繪製沒啥問題,即使是一小塊淺紅色區域也不算太壞,但若是咱們看到太多的紅色區域在屏幕上,那可能就有問題了,讓咱們來看幾個例子:
在左邊的例子中,咱們看到列表部分是綠色的,一般還OK,可是在上方發生覆蓋的區域是一片紅色,那就有問題了。在右邊的例子中,整個列表都是淺紅 色。在兩個例子中,都各有一個不透明的列表存在2x或3x的過分繪製。這些過分繪製可能發生在咱們給Activity或Fragment設置了全屏的背 景,同時又給ListView以及ListView的條目設置了背景色。而經過只設置一次背景色便可解決這樣的問題。
注意:默認的主題會爲你指定一個默認的全屏背景色,若是你的activity又一個不透平的背景蓋住了默認的背景色,那麼你能夠移除主題默認的背景 色,這樣也會移除一層的過分繪製。這能夠經過配置主題配置或是經過代碼的方法,在onCreate()方法中調用 getWindow().setBackgroundDrawable(null)方法來實現。
而使用Hierarchy Viewer,你能夠導出一個全部view層級的PSD文件,在Photoshop中打開,而且調查不一樣的layout以及不一樣的層級,也可以發現一些在 佈局中存在的過分繪製。而使用這些信息能夠移除沒必要要的過分繪製。並且,不要看到綠色就知足了,衝着藍色去!
使用透明度可能會影響性能,可是要去理解爲何,讓咱們瞅瞅當咱們給view設置透明度的時候到底發生了什麼。咱們來看一下下面這個佈局:
咱們看到這個layout中又三個ImageView而且重疊擺放。在使用最常規的設置透明度的方法setAlpha()時,方法會傳遞到沒一個子 view上面,在這裏是每個ImageView。然後,這些ImageView會攜帶新的透明值被繪製入幀緩衝區。而結果就是:
這並非咱們想要看到的結果。
由於每個ImageView都被賦予了一個透明值,致使了本應覆蓋的部分被融合在一塊兒。幸運的是,系統爲咱們解決了這個問題。佈局會被複制到一個非屏幕區域緩衝區中,而且以一個總體來接收透明度,其結果再被複制到幀緩衝區。結果就是:
可是,咱們是要付出性能上面的代價的。
假如在幀緩衝區內繪製以前,還要在off-screen緩衝區中繪製一遍的話,至關於增長了一層不可見的繪製層。而系統並不知道咱們是但願這個透明 度以何種的形式去展示,因此係統一般會採用相對複雜的一種。可是也有不少設置透明度的方法可以避免在off-screen緩衝區中的複雜操做:
在Honeycomb版本中引入了硬件加速(Hardware Accleration)後,咱們的應用在繪製的時候就有了全新的繪製模型。它引入了DisplayList結構,用來記錄View的繪製命令,以便更快的進行渲染。但還有一些很好的功能開發者們每每會忽略或者使用不當——View layers。
使用View layers(硬件層),咱們能夠將view渲染入一個非屏幕區域緩衝區(off-screen buffer,前面透明度部分提到過),而且根據咱們的需求來操控它。這個功能主要是針對動畫,由於它能讓複雜的動畫效果更加的流暢。而不使用硬件層的 話,View會在動畫屬性(例如coordinate, scale, alpha值等)改變以後進行一次刷新。而對於相對複雜的view,這一次刷新又會連帶它全部的子view進行刷新,並各自從新繪製,至關的耗費性能。使 用View layers,經過調用硬件層,GPU直接爲咱們的view建立一個結構,而且不會形成view的刷新。而咱們能夠在避免刷新的狀況下對這個結構進行進行 不少種的操做,例如x/y位置變換,旋轉,透明度等等。總之,這意味着咱們能夠對一個讓一個複雜view執行動畫的同時,又不會刷新!這會讓動畫看起來更 加的流暢。下面這段代碼咱們該如何操做:
很簡單,對吧?
是的,可是再使用硬件layers的時候仍是有幾點要牢記在心:
而對於第二個問題,咱們也有一個可視化的辦法來觀察硬件層更新。使用開發者選項中的「Show hardware layers updates」(顯示硬件層更新)
當打開該選項後,View會在硬件層刷新的時候閃爍綠色。在好久之前我有一個ViewPager在滑動的時候有點不流暢。在開發者模式啓動這個選項後,我再次滑動ViewPager,發現了以下狀況:
左右兩頁在滑動的時候徹底變成了綠色!
這意味着他們在建立的時候使用了硬件層,並且在滑動的時候也界面也進行了刷新。而當我在背景上面使用時差效果而且讓條目有一個動畫效果的時候,這些 處理確實會讓它進行刷新,可是我並無對ViewPager啓動硬件層。在閱讀了ViewPager的源碼後,我發現了在滑動的時候會自動爲左右兩頁啓動 一個硬件層,而且在滑動結束後移除掉。
在兩頁間滑動的時候建立硬件層也是能夠理解的,但對我來講小有不幸。一般來說加入硬件層是爲了讓ViewPager的滑動更加流暢,畢竟它們相對複雜。但這不是個人app所想要的,我不得不經過一些編碼來移除硬件層。
硬件層其實並非什麼酷炫的東西。重要的是咱們要理解他的原理而且合理的使用他們,要否則你確實會遇到一些麻煩。
在準備上述這一系列例子的過程當中,我進行了不少的編碼去模擬這些情景。你能夠在這個Github項目中找到這些代碼,同時也能夠在Google Play中找到。我用不一樣的Activity區分了不一樣的情景,而且儘可能將他們的用文檔解釋清楚,以便於幫助你們理解不一樣的Activity中是出現哪一種問題。你們能夠邊閱讀各個Activity的javadoc的同時,利用咱們前面講到的工具去玩兒這個App。
隨着安卓系統的不斷進化,你有話你的應用的手段也在不斷變多。不少全新的工具被引入到了SDK中,以及一些新的特性被加入到了系統中(比如硬件層這東西)。因此與時俱進和懂得取捨是很是重要的。
這是一個很是棒的油管播放列表,叫Android Performance Patterns,一些谷歌出品的短視頻,講解了不少與性能相關的話題。你能夠找到不一樣數據結構之間的對比(HashMap vs ArrayMap),Bitmap的優化,網絡優化等等,吐血推薦!
加入Android Performance Patterns的G+社羣,和你們一塊兒討論,分享心得,提出問題!
更多有意思的連接:
我真心但願你經過這篇文章得到到了足夠豐富的信息和信心,從今天開始優化你的應用吧!
嘗試用工具去記錄,並經過一些開發者選項中的選項,開搞吧。歡迎來G+上分享你在安卓性能優化上面的心得!