書籍推薦
市面上android性能優(yōu)化的書籍不多
因為性能優(yōu)化這塊稍微深入一點,涉及知識的深度和廣度就比較大了,而且性能優(yōu)化依賴很多的平臺相關(guān)的工具和分析技巧坦弟,導(dǎo)致通用性和實效性又不太高蹋辅,所以以下書籍的內(nèi)容也比較淺嘗輒止
- 總體不錯照筑,但是給出的實例場景對于日常的開發(fā)工作來說比較不常見到
- 網(wǎng)絡(luò)優(yōu)化這章寫的感覺最好炸卑,初看一遍感覺有不少啟示既鞠,正在繼續(xù)啃
- 工具介紹的比較多
- 給出的例子比較常見,但是介紹的比較潦草
- 夾帶私貨
- jni層的知識和優(yōu)化講的比較多
- 除去jni的部分之后的內(nèi)容比較像一個個tips盖文,內(nèi)容比較散
內(nèi)存性能分析及優(yōu)化的意義
Overview of memory management
內(nèi)存管理介紹
OOM
- 系統(tǒng)分配給app的堆內(nèi)存是有上限的嘱蛋,不是系統(tǒng)空閑多少內(nèi)存app就可以用多少,getMemoryClass()可以獲取到這個值
- 可以在manifest文件中設(shè)置largeHeap為true,這樣會增大堆內(nèi)存上限五续,getLargeMemoryClass()可以獲取到這個值
- 超出虛擬機(jī)堆內(nèi)存上限會造成OOM
Low Memory Killer
- android內(nèi)存管理使用了分頁(paging)和內(nèi)存映射(memory-mapping)技術(shù)洒敏,但是沒有使用swap,而是使用Low Memory Killer策略來提升物理內(nèi)存的利用率 疙驾,導(dǎo)致除了gc和殺死進(jìn)程回收物理內(nèi)存之外沒有其他方式來利用已經(jīng)被占用的內(nèi)存
- 當(dāng)前臺應(yīng)用切換到后臺后凶伙,系統(tǒng)并不結(jié)束它的進(jìn)程,而是把它緩存起來它碎,供下次啟動函荣。當(dāng)系統(tǒng)內(nèi)存不足時,按最近最少使用+優(yōu)先釋放內(nèi)存使用密集的策略釋放緩存進(jìn)程扳肛。
GC
- 內(nèi)存使用的多也會造成GC速度變慢傻挂,造成卡頓
- 內(nèi)存占用過高,在創(chuàng)建對象時內(nèi)存不足,很容易造成觸發(fā)GC影響APP性能
綜上
關(guān)注并減少應(yīng)用的內(nèi)存消耗可以減少oom的概率挖息,在內(nèi)存緊張的場景下獲得更好的用戶體驗金拒,也可以增加應(yīng)用的后臺存活時間
工具介紹
- GC-LOG
- dumpsys meminfo
- Profiler
- jhat
dumpsys procstats
用來衡量一段時間內(nèi)應(yīng)用消耗內(nèi)存的情況
- PSS:Proportional Set Size按比例分配共享內(nèi)存的實際內(nèi)存
- USS:Unique Set Size進(jìn)程私有內(nèi)存
PSS=USS+共享內(nèi)存/共享內(nèi)存的進(jìn)程數(shù)
(最小PSS-平均PSS-最大PSS/最小USS-平均USS-最大USS)
LeakCanary
檢測內(nèi)存泄漏的工具
MAT
比較常用的內(nèi)存dump文件分析工具
使用方法
- 使用Memory Profiler Dump內(nèi)存數(shù)據(jù)
- 導(dǎo)出的hprof文件不是MAT的標(biāo)準(zhǔn)文件,需要使用sdk帶的hprof-conv轉(zhuǎn)換
hprof-conv -z src dst //-z可以排除android框架創(chuàng)建的對象
使用場景
- 總體性找出內(nèi)存優(yōu)化的瓶頸
- 只有dump文件的現(xiàn)實場景套腹,或者無法定位具體問題等只有現(xiàn)場而沒有線索的情況下庖丁解牛的工具
- 對于專項功能的內(nèi)存優(yōu)化感覺不如代碼調(diào)試+profiler
分析場景構(gòu)建
性能測試的一些注意點
- 需要考慮盡量真實的場景
- 需要關(guān)閉log等調(diào)試組件避免干擾
常見的性能測試方式
- 切換到后臺
- 反復(fù)執(zhí)行功能
- 長時間執(zhí)行功能
- 多個場景來回切換
容易出現(xiàn)內(nèi)存問題的場景
- 包含了圖片顯示的界面
- 網(wǎng)絡(luò)傳輸大量數(shù)據(jù)的場景
- 需要緩存數(shù)據(jù)的場景
常見的內(nèi)存問題
內(nèi)存泄漏
內(nèi)存泄漏產(chǎn)生的原因
一個對象的生命周期已經(jīng)結(jié)束了绪抛,但是有其他對象持有了它的實例導(dǎo)致無法在GC時被回收,在Android中通常是Activity在finish之后依然有對象引用它導(dǎo)致內(nèi)存泄漏
內(nèi)存泄漏的常見場景
- 異步操作中異步邏輯未結(jié)束电禀,而Activity結(jié)束或者重建了
- Thread/Handler/AsyncTask/Rxjava/Timer等
- 使用靜態(tài)變量或者單例直接或者間接的保存Activty實例但是未及時釋放
- 注冊廣播未注銷
- ObjectAnimator未調(diào)用cancel
- I/O操作等完成后未及時關(guān)閉或者釋放
- WebView造成的內(nèi)存泄漏 Android 5.1 WebView內(nèi)存泄漏分析
內(nèi)存泄漏在分析工具上的表現(xiàn)
每次activity的重建都會造成內(nèi)存上升且gc不會使內(nèi)存使用降低
內(nèi)存泄漏的避免
- LeakCanary
- StrictMode
- 沒有必要使用Activity作為Context的地方全部使用ApplicationContext
- 使用WeakReference
- 使用ViewModel+LiveData/RxJava+Rxlifecycle等工具實現(xiàn)異步邏輯避免內(nèi)存泄漏
- 對需要銷毀時進(jìn)行處理的操作進(jìn)行檢查幢码,如xxx.cancel()/xxx.close()/xxx.unregister()/xxx.remove()等操作
內(nèi)存抖動
內(nèi)存抖動的原因
內(nèi)存抖動一般是瞬間創(chuàng)建了大量對象,會在短時間內(nèi)觸發(fā)多次GC尖飞,產(chǎn)生卡頓
內(nèi)存抖動的場景
- IM通知需要轉(zhuǎn)發(fā)到所有WebView界面症副,當(dāng)剛打開APP時多個通知同時到達(dá),或者在群聊中消息很多的場景下葫松,會造成短時間內(nèi)頻繁GC瓦糕,同時伴隨界面卡頓
內(nèi)存抖動的在分析工具上的表現(xiàn)
制造了一個內(nèi)存抖動的場景
public void testThrashing(boolean needLog) {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
//優(yōu)化以前
for (int i = 0; i < lotsOfInts.length; i++) {
String rowAsStr = "";
int[] sorted = getSorted(lotsOfInts[i]);
for (int j = 0; j < lotsOfInts[i].length; j++) {
rowAsStr += sorted[j];
if (j < (lotsOfInts[i].length - 1)) {
rowAsStr += ", ";
}
}
if (needLog) {
Log.i(TAG, "Row " + i + ": " + rowAsStr);
}
}
}
public void optimizeThrashing() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
//優(yōu)化以后
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lotsOfInts.length; i++) {
sb.delete(0, sb.length());
int[] sorted = getSorted(lotsOfInts[i]);
for (int j = 0; j < lotsOfInts[i].length; j++) {
sb.append(sorted[j]);
if (j < (lotsOfInts[i].length - 1)) {
sb.append(", ");
}
}
Log.e(TAG, "Row " + i + ": " + sb);
}
}
- 自己試驗的感受底洗,gc帶來的卡頓其實并不明顯(也可能是demo不太復(fù)雜腋么,GC耗時不長)
- 個人感覺卡頓主要是因為內(nèi)存抖動大多出現(xiàn)在一些復(fù)雜場景,通常伴隨著主線程的大量操作已經(jīng)出現(xiàn)了卡頓亥揖,而內(nèi)存抖動引起的頻繁GC會加劇卡頓的程度
解決方案
- 最簡單的做法就是把之前的主線程操作放到子線程去珊擂,雖然內(nèi)存抖動依然存在圣勒,但是卡頓問題可以大大緩解
- 對于內(nèi)存抖動本身
- 盡量避免在循環(huán)體內(nèi)創(chuàng)建對象,應(yīng)該把對象創(chuàng)建移到循環(huán)體外
- 需要大量使用Bitmap和其他大型對象時摧扇,盡量嘗試復(fù)用之前創(chuàng)建的對象
- 對于黑盒子(例如之前例子中im通知造成的webview的內(nèi)存抖動和主線程耗時操作)
- 控制觸發(fā)頻率圣贸,減輕卡頓程度
- 添加注冊機(jī)制,需要接收通知的頁面才發(fā)送通知
圖片加載的內(nèi)存占用
不同dpi文件夾對圖片內(nèi)存的影響
不同dpi限定符對應(yīng)的dpi
xxxhdpi-640
xxhdpi-480
xhdpi-320
mdpi-160通過resId加載的Bitmap的寬高計算
bitmap寬高=圖片實際寬高*屏幕dpi/文件夾對應(yīng)的dpinodpi
從這個文件夾中加載的圖片資源生成的Bitmap會保持圖片本身的尺寸1920*1080圖片資源放在不同的文件夾下加載的Bitmap大小計算
使用設(shè)備小米note扛稽,設(shè)備dpi為440
文件夾 | 對應(yīng)dpi | bitmap width | height | size | 倍數(shù) |
---|---|---|---|---|---|
nodpi | 1920 | 1080 | 8294400 | 1 | |
xxxhdpi | 640 | 1320 | 743 | 3923040 | 0.47 |
xxhdpi | 480 | 1760 | 990 | 6969600 | 0.84 |
xhdpi | 320 | 2640 | 1485 | 15681600 | 1.89 |
mdpi | 160 | 5280 | 2970 | 62726400 | 7.56 |
- 使用圖片的建議
- 盡量使用1080p的尺寸下的切圖
- 圖片盡量放xxhdpi以上的文件夾下
- 大圖如Splash頁和引導(dǎo)頁的圖片放在nodpi文件夾下,通過控制ImageView大小來限制圖片大小
- 按照上面操作會導(dǎo)致apk大小增加吁峻,可以將圖片轉(zhuǎn)成webp并進(jìn)行壓縮
RGB565
除了圖片資源的文件夾,加載圖片時使用的色彩模式也影響了Bitmap大小在张。ARGB8888使用了32bit用含,所以一個像素需要4byte;RGB565使用了16bit帮匾,一個像素只需要2byte
但是因為RGB565少了alpha通道啄骇,對有透明度的圖片顯示有問題,而且顯示效果上還是有些區(qū)別瘟斜,所以并不建議修改這個屬性缸夹,只是在對內(nèi)存有嚴(yán)格要求的場景下可以作為特殊手段進(jìn)行優(yōu)化
ProGuard對內(nèi)存的影響
- ProGuard可以對類、方法和變量重命名螺句,剔除無用代碼和資源虽惭,減小dex大小,除了減小了apk的大小壹蔓,同時也減小了加載dex所需的內(nèi)存
- 因為虛擬機(jī)加載dex文件是按需加載的趟妥,而內(nèi)存分配的最小單位是頁,所以加載一個功能的代碼時同一個內(nèi)存頁中也會加載dex文件中該功能前后不相關(guān)的代碼佣蓉,ProGuard可以重新排序類的字節(jié)碼在dex文件中位置披摄,使得有相互調(diào)用關(guān)系的類在dex中更加緊湊,加載相同功能所需的內(nèi)存更小
內(nèi)存碎片
Davik的內(nèi)存回收算法不能移動對象勇凭,所以會造成一個小對象占據(jù)整個內(nèi)存頁疚膊,產(chǎn)生內(nèi)存碎片
而ART虛擬機(jī)的可以在GC時對內(nèi)存空間進(jìn)行整理,隨著5.0以上系統(tǒng)的占有率逐漸提升虾标,內(nèi)存碎片造成的內(nèi)存消耗可以不必過于關(guān)心
其他內(nèi)存問題
- 頁面不可見收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)時釋放UI資源
- 通過getMemoryInfo()獲取內(nèi)存信息寓盗,保證自己不開辟大內(nèi)存導(dǎo)致oom
- 謹(jǐn)慎的使用Service
- 使用IntentService
- 使用JobScheduler進(jìn)行后臺調(diào)度
- 使用優(yōu)化的容器如SparseArray等
- 代碼抽象會帶來額外的內(nèi)存消耗
- 使用@IntDef、@StringDef代替枚舉
...
to be continue...
- 流暢度
- 耗電
- 網(wǎng)絡(luò)
- apk瘦身
- Matrix