[轉載]Android性能優化來龍去脈總結

轉載來自這里

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


image.png

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

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

image.png

1、內存泄露。

通俗來講,內存泄露不僅僅會造成應用內存占用過大,還會導致應用卡頓,造成不好的用戶體驗,至于,為什么一個“小小的”內存泄露會造成應用卡頓,我不得不拿這幅圖來說說話了。

image.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、還有一種很常見的場景是一個頁面多圖的場景,明明每個圖只需要加載一個100100的,你卻使用原始尺寸(10801980)or更大,而且你一下子還加載個幾十張,扛得住么?所以了解一下inSampleSize,或者,如果圖片歸你們上傳管理,你可以借助萬象優圖,他為你做了剪切好不同尺寸的圖片,這樣省得你在客戶端做圖片縮放了。

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

image.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中也提供了測試的方法,感興趣的小伙伴可以嘗試一下咯。

image.png

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

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

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

4、view過度繪制的問題。

view過度繪制的問題可以說是我們在寫布局的時候遇到的一個最常見的問題之一,可以說寫著寫著一不留神就寫出了一個過度繪制,通常發生在一個嵌套的viewgroup中,比如你給他設置了一個不必要的背景。這方面問題的排查不太難,我們可以通過手機設置里面的開發者選項,打開Show GPU Overdraw的選項,輕松發現這些問題,然后盡量往藍色靠近。

image.png

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

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

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

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

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

7、工作線程優先級設置不對,導致和ui線程搶占cpu時間。

使用Rxjava的小伙伴要注意這點,設置任務的執行線程可能會對你的性能產生較大的影響,沒有使用的小伙伴也不能太過大意。

8、靜態變量。

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

a、你用靜態集合保存過數據吧?

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

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

image.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%的解析效率。

image.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之間如何共享數據呢?好,看我怎么操作。

image.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不斷的失效并重現創建,如果你想了解的更加詳細,可以查看這里

image.png

// Set the layer type to hardware

myView.setLayerType(View.LAYER_TYPE_HARDWARE, null);

// Setup the animation

ObjectAnimator animator = ObjectAnimator.ofFloat(myView,View.TRANSLATION_X, 150);

// Add a listener that does cleanup

animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
myView.setLayerType(View.LAYER_TYPE_NONE, null);
}
});

11耗電優化

這里僅僅只是建議;

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

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

四 接下來的一些內容就比較輕松了,是關于一些代碼的建議

image.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耗時較多。

五 總結

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


參考資料

http://developers.googleblog.cn/2017/09/constraintlayout.html

http://hukai.me/android-performance-patterns

https://juejin.im/entry/59396e01fe88c2006afc3862

https://github.com/JakeWharton/hugo

https://stackoverflow.com/questions/15509544/optimizing-gson-deserialization

https://medium.com/livefront/recyclerview-trick-selectively-bind-viewholders-with-payloads-4b28e3d2cce8


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。