Android效能最佳化來龍去脈

joytoy發表於2021-09-09

一款app除了要有令人驚歎的功能和令人髮指互動之外,在效能上也應該追求絲滑的要求,這樣才能更好地提高使用者體驗。


以下是本人在工作中對經歷過的效能最佳化的一些總結,依據故事的發展路線,將其分為了5個部分,分別是:常見的效能問題;產生效能問題的一些可能原因;解決效能問題的套路;程式碼建議及潛在效能問題排查項。

圖片描述

1.png


如看不清大圖,下文會有拆解

一 首先,我們先了解一下都有哪些效能問題


圖片描述

2.png


1、記憶體洩露。

通俗來講,記憶體洩露不僅僅會造成應用記憶體佔用過大,還會導致應用卡頓,造成不好的使用者體驗,至於,為什麼一個“小小的”記憶體洩露會造成應用卡頓,我不得不拿這幅圖來說說話了。

圖片描述

3.png


沒錯,這就是Android開發童鞋需要了解的Generational Heap Memory模型,這裡我們只關心當物件在Young Generation中存活了一段時間之後,如果沒被幹掉,那麼會被移動到Old Generation中,同理,最後會移動到Permanent Generation中。那麼用腳想一想就知道,如果記憶體洩露了,那麼,抱歉,你那塊記憶體隨時間推移自然而然將進入Permanent Generation中,然鵝,記憶體不是白菜,想要多少就有多少,這裡,因為沙盒機制的原因,分配給你應用的記憶體當然是有那麼一個極限值的,你不能逾越(有人笑了,不是有large heap麼,當然我也笑了,我並沒有看到這貨被宗師android玩家青睞過),好了,你那塊造成洩露記憶體的物件佔著茅坑不拉屎,剩下來可以供其他物件發揮的記憶體空間就少了;打個比方,舞臺小了,演員要登臺表演,沒有多餘空間,他就只能等待其他演員下來他才能表演啊,這等待的時間,是沒法連續表演的,所以就卡了嘛。

2、頻繁GC

呵呵,頻繁GC會造成卡頓,想必你經過上面的洗禮,已經知道了為什麼,不錯,當然也是因為“舞臺空間不足,新的演員上臺表演需要先讓表演完的下來”。那麼造成這種現象的原因是什麼呢?

a、記憶體洩露,好的,你懂了,不用講了,這個必須有可能會造成。

b、大量物件短時間被建立,又在短時間內“需要”被釋放,注意這裡的需要,其實是不得不,為什麼,同樣是因為“舞臺空間不夠了”,舉個例子,在onDraw中new 物件,因為onDraw大約16ms會執行一次(wait,你能否確定一下,什麼是大約16ms,對不起,不能,掉幀了就不是,哪怕掉那麼一點點)。腦補一下,每秒中建立大約60個物件,嗯,騷年,你以為Young Generation是白菜麼,想拿多少就拿多少,對不起,這裡是限量的,這裡用完了,在來申請,我就得去回收一些回來,我回收總得耗時間吧,耗時間,好吧,onDraw 等著等著就錯過了下一個16ms的執行了,如是,使用者看起來就卡了。

3、耗電問題

km上有一個問題很尖銳,說是微視看小影片看一會手機就會發燙,所以,使用者一直就很關注耗電問題,不過不好意思,我們的app至今還沒有遇到過嚴重的耗電問題,雖然沒有遇到比較嚴重的耗電問題,不代表就不需要去了解這樣的問題的解決辦法,我總結有:

a、沒有什麼特別重要的資訊,比如,錢到賬,電話來了,100元實打實無門檻代金券方法,等等,請不要打擾使用者,不要頻繁喚醒使用者,否則,結果只能是解除安裝,或者關閉一切通知。

b、適當的做本地快取,避免頻繁請求網路資料,這裡,說起來容易,做起來並非三刀兩斧就能搞定,要配合良好的快取策略,區分哪些是一段時間不會變更的,哪些是絕對不能快取的很重要。

c、對某些執行時間較長的同步操作在使用者充電且有wifi的時候在做,除非使用者強制同步..等等,就不扯太多,因為後面還有很多內容。

4、OOM問題

呵呵,這個問題,想必經過前面1、2的洗禮,你應該已經明白這個什麼原因導致的,你可以想想一下"舞臺上將要上的一個演員是一個巨大胖子,即便不表演的演員都下來了,他還是擠不上去,怎麼辦,演砸了,還能怎麼辦,直接崩潰,散場!"造成這個問題的原因,可能有,(呵呵,保險起見,只能說可能,分析的時候可以從這裡出發)

a、記憶體洩露了,想必你會心一笑。

b、大量不可見的物件佔據記憶體,這個其實,很常見,只是大家可能一直不太關心罷了,比如,請求介面返回了列表有100項資料,每項資料比如有100個欄位,其中你使用者展示資料的只有10幾個而已,但是,你解析的時候,剩下的99個不知不覺吃了你的記憶體,當,有個胖子要記憶體時,呵呵,嗝屁了。

c、還有一種很常見的場景是一個頁面多圖的場景,明明每個圖只需要載入一個100_100的,你卻使用原始尺寸(1080_1980)or更大,而且你一下子還載入個幾十張,扛得住麼?所以瞭解一下inSampleSize,或者,如果圖片歸你們上傳管理,你可以藉助永珍優圖,他為你做了剪下好不同尺寸的圖片,這樣省得你在客戶端做圖片縮放了。

二 以上了解了一些效能問題,這裡,簡單的串一串導致這些效能問題的原因


圖片描述

4.png


1、人為在ui執行緒中做了輕微的耗時操作,導致ui執行緒卡頓

嗯,很多小夥伴不以為然,以為在onCreate中讀一下pref算什麼,解析下json資料算得了什麼,可實際情況是並不是這樣的,正確的做法是,將這些操作使用非同步封裝起來,小夥伴可以瞭解一下rxjava,現在最新版本已經是rxjava2了,如果不清楚使用方式,可以Google一下。

2、layout過於複雜,無法在16ms完成渲染

這個很多小夥伴深有體會了,這裡簡單的瞭解下,我們先簡單的把渲染大概分為"layout","measure""draw"這麼幾個階段,當然你不要以為實際情況也是如此,好,層級複雜,layout,measure可能就用到了不該用的時間,自然而然,留給draw的時間就可能不夠了,自然而然就悲劇了。那麼以前給出的很多建議是,使用RelativeLayout替換LinearLayout,說是可以減少佈局層次,然鵝,現在請不要在建議別人使用RelativeLayout,因為ConstraintLayout才是一個更高效能的消滅佈局層級的神器。ConstraintLayout 基於Cassowary演算法,而Cassowary演算法的優勢是在於解決線性方程時有極高的效率,事實證明,線性方程組是非常適合用於定義使用者介面元素的引數。由於人們對圖形的敏感度非常高,所以UI的渲染速度顯得非常重要。因此在2016年,iOS和Android都基於Cassowary演算法來研發了屬於自己的佈局系統,這裡是ConstraintLayout與傳統佈局RelativeLayout,LinearLayout實現時的效能對比,不過這裡是老外的測試資料,原文可以參考這裡。demo中也提供了測試的方法,感興趣的小夥伴可以嘗試一下咯。

圖片描述

5.png


測量/佈局(單位:毫秒,100 幀的平均值)

3、同一時間執行的動畫過多,導致CPU或者GPU負載過重

這裡主要是因為動畫一般會頻繁變更view的屬性,導致displayList失效,而需要重新建立一個新的displayList,如果動畫過多,這個開銷可想而知,如果你想了解得更加詳細,推薦看這篇咯,知識點在第5節那裡。

4、view過度繪製的問題。

view過度繪製的問題可以說是我們在寫佈局的時候遇到的一個最常見的問題之一,可以說寫著寫著一不留神就寫出了一個過度繪製,通常發生在一個巢狀的viewgroup中,比如你給他設定了一個不必要的背景。這方面問題的排查不太難,我們可以透過手機設定裡面的開發者選項,開啟Show GPU Overdraw的選項,輕鬆發現這些問題,然後儘量往藍色靠近。

圖片描述

6.png


5、gc過多的問題,這裡就不在贅述了,上面已經講的非常直接了。

6、資源載入導致執行緩慢。

有些時候避免不要載入一些資源,這裡有兩種解決的辦法,使用的場景也不相同。

a、預載入,即還沒有來到路徑之前,就提前載入好,誒,好像x5核心就是醬紫哦。

b、實在是要等到用到的時候載入,請給一個進度條,不要讓使用者乾等著,也不知道什麼時候結束而造成不好的使用者體驗。

7、工作執行緒優先順序設定不對,導致和ui執行緒搶佔cpu時間。

使用Rxjava的小夥伴要注意這點,設定任務的執行執行緒可能會對你的效能產生較大的影響,沒有使用的小夥伴也不能太過大意。

8、靜態變數。

嘿嘿,大家一定有過在application中設定靜態變數的經歷,遙想當年,為了越過Intent只能傳遞1M以下資料的坑,我在application中設定了一個靜態變數,用於兩個activity“傳遞(共享)資料”,然而,一步小心,資料中,有著前一個activity的尾巴,因此洩露了。不光是這樣的例子,隨便舉幾個:

a、你用靜態集合儲存過資料吧?

b、某某單例的Manger,比如管理AudioManger遇到過吧?

三 既然遇到問題分析也有了,那麼接下來,自然而然是如何使用各種刀棒棍劍來解決這些問題了


圖片描述

7.png


1、GPU過度繪製,定位過度繪製區域

這裡直接在開發者選項,開啟Show GPU Overdraw,就可以看到效果,輕鬆發現哪塊需要最佳化,那麼具體如何去最佳化

a、減少佈局層級,上面有提到過,使用ConstraintLayout替換傳統的佈局方式。如果你對ConstraintLayout不瞭解,沒有關係,這篇文章教你15分鐘瞭解如何使用ConstraintLayout。

b、檢查是否有多餘的背景色設定,我們通常會犯一些低階錯誤--對被覆蓋的父view設定背景,多數情況下這些背景是沒有必要的。

2、主執行緒耗時操作排查。

a、開啟strictmode,這樣一來,主執行緒的耗時操作都將以告警的形式呈現到logcat當中。

b、直接對懷疑的物件加@DebugLog,檢視方法執行耗時。DebugLog註解需要引入外掛hugo,這個是Android之神JakeWharton的早期作品,對於監控函式執行時間非常方便,直接在函式上加入註解就可以實現,但是有一個缺點,就是JakeWharton釋出的最後一個版本沒有支援release版本用空方法替代監控程式碼,因此,我這裡釋出了一個到公司的maven倉庫,引用的方式和官網類似,只不過,地址是:'com.tencent.tip:hugo-plugin:2.0.0-SNAPSHOT'。

3、對於measure,layout耗時過多的問題

一般這類問題是優於佈局過於複雜的原因導致,現在因為有ConstraintLayout,所以,強烈建議使用ConstraintLayout減少佈局層級,問題一般得以解決,如果發現還存在效能問題,可以使用traceView觀察方法耗時,來定位下具體原因。

4、leakcany

這個是記憶體洩露監測的銀彈,大家應該都使用過,需要提醒一下的是,要注意

dependencies {

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'

releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

}

引入方式,releaseImplementation保證在釋出包中移除監控程式碼,否則,他自生不停的catch記憶體快照,本身也影響效能。

5、onDraw裡面寫程式碼需要注意

onDraw優於大概每16ms都會被執行一次,因此本身就相當於一個forloop,如果你在裡面new物件的話,不知不覺中就滿足了短時間內大量物件建立並釋放,於是頻繁GC就發生了,嗯,記憶體抖動,於是,卡了。因此,正確的做法是將物件放在外面new出來。

6、json反序列化問題

json反序列化是指將json字串轉變為物件,這裡如果資料量比較多,特別是有相當多的string的時候,解析起來不僅耗時,而且還很吃記憶體。解決的方式是:

a、精簡欄位,與後臺協商,相關介面剔除不必要的欄位。保證最小可用原則。

b、使用流解析,之前我考慮過json解析最佳化,在Stack Overflow上搜尋到這個。於是瞭解到Gson.fromJson是可以這樣玩的,可以提升25%的解析效率。

圖片描述

8.png


7、viewStub&merge的使用。

這裡merge和viewStub想必是大家非常瞭解的兩個佈局元件了,對於只有在某些條件下才展示出來的元件,建議使用viewStub包裹起來,同樣的道理,include 某佈局如果其根佈局和引入他的父佈局一致,建議使用merge包裹起來,如果你擔心preview效果問題,這裡完全沒有必要,因為你可以

tools:showIn=""屬性,這樣就可以正常展示preview了。

8、載入最佳化

這裡並沒有過多的技術點在裡面,無非就是將耗時的操作封裝到非同步中去了,但是,有一點不得不提的是,要注意多程式的問題,如果你的應用是多程式,你應該認識到你的application的oncreate方法會被執行多次,你一定不希望資源載入多次吧,於是你只在主程式載入,如是有些坑就出現了,有可能其他程式需要那某份資源,然後他這個程式缺沒有載入相應的資源,然後就嗝屁了。

9、重新整理最佳化。

這點在我之前的文章中有提到過,這裡舉兩個例子吧。

a、對於列表的中的item的操作,比如對item點贊,此時不應該讓整個列表重新整理,而是應該只重新整理這個item,相比對於熟練使用recyclerView的你,應該明白如何操作了,不懂請看這裡,你將會明白什麼叫做recyclerView的區域性重新整理。

b、對於較為複雜的頁面,個人建議不要寫在一個activity中,建議使用幾個fragment進行組裝,這樣一來,module的變更可以只重新整理某一個具體的fragment,而不用整個頁面都走重新整理邏輯。但是問題來了,fragment之間如何共享資料呢?好,看我怎麼操作。

圖片描述

9.png


Activity將資料這部分抽象成一個LiveData,交個LiveDataManger資料進行管理,然後各個Fragment透過Activity的這個context從LiveDataManger中拿到LiveData,進行操作,通知activity資料變更等等。哈哈,你沒有看錯,這個確實和Google的那個LiveData有點像,當然,如果你想使用Google的那個,也自然沒問題,只不過,這個是簡化版的。專案的引入

'com.tencent.tip:simple_live_data:1.0.1-SNAPSHOT'

10、動畫最佳化

這裡主要是想說使用硬體加速來做最佳化,不過要注意,動畫做完之後,關閉硬體加速,因為開啟硬體加速本身就是一種消耗。下面有一幅圖,第二幅對比第一幅是說開啟硬體加速和沒開啟的時候做動畫的效果對比,可以看到開啟後的渲染速度明顯快不少,開啟硬體加速就一定萬事大吉麼?第三幅圖實際上就說明,如果你的這個view不斷的失效的話,也會出現效能問題,第三圖中可以看到藍色的部曲線圖有了一定的起色,這說明,displaylist不斷的失效並重現建立,如果你想了解的更加詳細,可以檢視這裡

圖片描述

10.png


// Set the layer type to hardwaremyView.setLayerType(View.LAYER_TYPE_HARDWARE, null);// Setup the animationObjectAnimator animator = ObjectAnimator.ofFloat(myView,View.TRANSLATION_X, 150);// Add a listener that does cleanupanimator.addListener(new AnimatorListenerAdapter() {undefined     @Override     public void onAnimationEnd(Animator animation) {          myView.setLayerType(View.LAYER_TYPE_NONE, null);   } });

11耗電最佳化

這裡僅僅只是建議;

a、在定位精度要求不高的情況下,使用wifi或行動網路進行定位,沒有必要開啟GPS定位。

b、先驗證網路的可用性,在傳送網路請求,比如,當使用者處於2G狀態下,而此時的操作是檢視一張大圖,下載下來可能都200多K甚至更大,我們沒必要去傳送這個請求,讓使用者一直等待那個菊花吧。

四 接下來的一些內容就比較輕鬆了,是關於一些程式碼的建議


圖片描述

11.png


這裡不一一細講了,僅僅挑標記的部分說下。

pb->model這裡的最佳化就不在贅述,前面有講如何最佳化。

然後建議使用SparseArray代替HashMap,這裡是Google建議的,因為SparseArray比HashMap更省記憶體,在某些條件下效能更好,主要是因為它避免了對key的自動裝箱比如(int轉為Integer型別),它內部則是透過兩個陣列來進行資料儲存的,一個儲存key,另外一個儲存value,為了最佳化效能,它內部對資料還採取了壓縮的方式來表示稀疏陣列的資料,從而節約記憶體空間。

不到不得已,不要使用wrap_content,,推薦使用match_parent,或者固定尺寸,配合gravity="center",哈哈,你應該懂了的。

那麼為什麼說這樣會比較好。

因為 在測量過程中,match_parent和固定寬高度對應EXACTLY ,而wrap_content對應AT_MOST,這兩者對比AT_MOST耗時較多。

五 總結


這是以上關於我在工作中遇到的效能問題的及處理的一些總結,效能最佳化設計的方方面面實在是太多太多,本文不可能將全部的效能問題全部總結的清清楚楚,或許還多多少少存在一些紕漏之處,有不對的地方歡迎指出補充。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4650/viewspace-2803483/,如需轉載,請註明出處,否則將追究法律責任。

相關文章