android性能評(píng)測(cè)與優(yōu)化-內(nèi)存

內(nèi)存性能分析及優(yōu)化的意義

Overview of memory management
內(nèi)存管理介紹

OOM

  • 系統(tǒng)分配給app的堆內(nèi)存是有上限的,不是系統(tǒng)空閑多少內(nèi)存app就可以用多少,getMemoryClass()可以獲取到這個(gè)值

  • 可以在manifest文件中設(shè)置largeHeap為true,這樣會(huì)增大堆內(nèi)存上限,getLargeMemoryClass()可以獲取到這個(gè)值

  • 超出虛擬機(jī)堆內(nèi)存上限會(huì)造成OOM

Low Memory Killer

  • android內(nèi)存管理使用了分頁(yè)(paging)和內(nèi)存映射(memory-mapping)技術(shù),但是沒(méi)有使用swap,而是使用Low Memory Killer策略來(lái)提升物理內(nèi)存的利用率 ,導(dǎo)致除了gc和殺死進(jìn)程回收物理內(nèi)存之外沒(méi)有其他方式來(lái)利用已經(jīng)被占用的內(nèi)存

  • 當(dāng)前臺(tái)應(yīng)用切換到后臺(tái)后,系統(tǒng)并不結(jié)束它的進(jìn)程,而是把它緩存起來(lái),供下次啟動(dòng)。當(dāng)系統(tǒng)內(nèi)存不足時(shí),按最近最少使用+優(yōu)先釋放內(nèi)存使用密集的策略釋放緩存進(jìn)程。

GC

  • 內(nèi)存使用的多也會(huì)造成GC速度變慢,造成卡頓

  • 內(nèi)存占用過(guò)高,在創(chuàng)建對(duì)象時(shí)內(nèi)存不足,很容易造成觸發(fā)GC影響APP性能

綜上

關(guān)注并減少應(yīng)用的內(nèi)存消耗可以減少oom的概率,在內(nèi)存緊張的場(chǎng)景下獲得更好的用戶體驗(yàn),也可以增加應(yīng)用的后臺(tái)存活時(shí)間

工具介紹

調(diào)查 RAM 使用情況

  • GC-LOG

  • dumpsys meminfo

  • Profiler

  • jhat

dumpsys procstats

用來(lái)衡量一段時(shí)間內(nèi)應(yīng)用消耗內(nèi)存的情況

  • PSS:Proportional Set Size按比例分配共享內(nèi)存的實(shí)際內(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)

procstats

LeakCanary

檢測(cè)內(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)建的對(duì)象

使用場(chǎng)景

  • 總體性找出內(nèi)存優(yōu)化的瓶頸

  • 只有dump文件的現(xiàn)實(shí)場(chǎng)景,或者無(wú)法定位具體問(wèn)題等只有現(xiàn)場(chǎng)而沒(méi)有線索的情況下庖丁解牛的工具

  • 對(duì)于專項(xiàng)功能的內(nèi)存優(yōu)化感覺(jué)不如代碼調(diào)試+profiler

分析場(chǎng)景構(gòu)建

性能測(cè)試的一些注意點(diǎn)

  • 需要考慮盡量真實(shí)的場(chǎng)景

  • 需要關(guān)閉log等調(diào)試組件避免干擾

常見(jiàn)的性能測(cè)試方式

  • 切換到后臺(tái)

  • 反復(fù)執(zhí)行功能

  • 長(zhǎng)時(shí)間執(zhí)行功能

  • 多個(gè)場(chǎng)景來(lái)回切換

容易出現(xiàn)內(nèi)存問(wèn)題的場(chǎng)景

  • 包含了圖片顯示的界面

  • 網(wǎng)絡(luò)傳輸大量數(shù)據(jù)的場(chǎng)景

  • 需要緩存數(shù)據(jù)的場(chǎng)景

常見(jiàn)的內(nèi)存問(wèn)題

內(nèi)存泄漏

內(nèi)存泄漏產(chǎn)生的原因

一個(gè)對(duì)象的生命周期已經(jīng)結(jié)束了,但是有其他對(duì)象持有了它的實(shí)例導(dǎo)致無(wú)法在GC時(shí)被回收,在Android中通常是Activity在finish之后依然有對(duì)象引用它導(dǎo)致內(nèi)存泄漏

內(nèi)存泄漏的常見(jiàn)場(chǎng)景

  • 異步操作中異步邏輯未結(jié)束,而Activity結(jié)束或者重建了

  • Thread/Handler/AsyncTask/Rxjava/Timer等

  • 使用靜態(tài)變量或者單例直接或者間接的保存Activty實(shí)例但是未及時(shí)釋放

  • 注冊(cè)廣播未注銷(xiāo)

  • ObjectAnimator未調(diào)用cancel

  • I/O操作等完成后未及時(shí)關(guān)閉或者釋放

  • WebView造成的內(nèi)存泄漏 Android 5.1 WebView內(nèi)存泄漏分析

內(nèi)存泄漏在分析工具上的表現(xiàn)

內(nèi)存泄漏

每次activity的重建都會(huì)造成內(nèi)存上升且gc不會(huì)使內(nèi)存使用降低

內(nèi)存泄漏的避免

  • LeakCanary

  • StrictMode

  • 沒(méi)有必要使用Activity作為Context的地方全部使用ApplicationContext

  • 使用WeakReference

  • 使用ViewModel+LiveData/RxJava+Rxlifecycle等工具實(shí)現(xiàn)異步邏輯避免內(nèi)存泄漏

  • 對(duì)需要銷(xiāo)毀時(shí)進(jìn)行處理的操作進(jìn)行檢查,如xxx.cancel()/xxx.close()/xxx.unregister()/xxx.remove()等操作

內(nèi)存抖動(dòng)

內(nèi)存抖動(dòng)的原因

內(nèi)存抖動(dòng)一般是瞬間創(chuàng)建了大量對(duì)象,會(huì)在短時(shí)間內(nèi)觸發(fā)多次GC,產(chǎn)生卡頓

內(nèi)存抖動(dòng)的場(chǎng)景

  • IM通知需要轉(zhuǎn)發(fā)到所有WebView界面,當(dāng)剛打開(kāi)APP時(shí)多個(gè)通知同時(shí)到達(dá),或者在群聊中消息很多的場(chǎng)景下,會(huì)造成短時(shí)間內(nèi)頻繁GC,同時(shí)伴隨界面卡頓

內(nèi)存抖動(dòng)的在分析工具上的表現(xiàn)

內(nèi)存抖動(dòng)

制造了一個(gè)內(nèi)存抖動(dòng)的場(chǎng)景

 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);
        }

    }
  • 自己試驗(yàn)的感受,gc帶來(lái)的卡頓其實(shí)并不明顯(也可能是demo不太復(fù)雜,GC耗時(shí)不長(zhǎng))

  • 個(gè)人感覺(jué)卡頓主要是因?yàn)閮?nèi)存抖動(dòng)大多出現(xiàn)在一些復(fù)雜場(chǎng)景,通常伴隨著主線程的大量操作已經(jīng)出現(xiàn)了卡頓,而內(nèi)存抖動(dòng)引起的頻繁GC會(huì)加劇卡頓的程度

解決方案

  • 最簡(jiǎn)單的做法就是把之前的主線程操作放到子線程去,雖然內(nèi)存抖動(dòng)依然存在,但是卡頓問(wèn)題可以大大緩解

  • 對(duì)于內(nèi)存抖動(dòng)本身

    • 盡量避免在循環(huán)體內(nèi)創(chuàng)建對(duì)象,應(yīng)該把對(duì)象創(chuàng)建移到循環(huán)體外

    • 需要大量使用Bitmap和其他大型對(duì)象時(shí),盡量嘗試復(fù)用之前創(chuàng)建的對(duì)象

  • 對(duì)于黑盒子(例如之前例子中im通知造成的webview的內(nèi)存抖動(dòng)和主線程耗時(shí)操作)

    • 控制觸發(fā)頻率,減輕卡頓程度

    • 添加注冊(cè)機(jī)制,需要接收通知的頁(yè)面才發(fā)送通知

圖片加載的內(nèi)存占用

不同dpi文件夾對(duì)圖片內(nèi)存的影響

  • 不同dpi限定符對(duì)應(yīng)的dpi
    xxxhdpi-640

xxhdpi-480
xhdpi-320
mdpi-160

  • 通過(guò)resId加載的Bitmap的寬高計(jì)算
    bitmap寬高=圖片實(shí)際寬高*屏幕dpi/文件夾對(duì)應(yīng)的dpi

  • nodpi
    從這個(gè)文件夾中加載的圖片資源生成的Bitmap會(huì)保持圖片本身的尺寸

  • 1920*1080圖片資源放在不同的文件夾下加載的Bitmap大小計(jì)算
    使用設(shè)備小米note,設(shè)備dpi為440

文件夾 對(duì)應(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頁(yè)和引導(dǎo)頁(yè)的圖片放在nodpi文件夾下,通過(guò)控制ImageView大小來(lái)限制圖片大小

  • 按照上面操作會(huì)導(dǎo)致apk大小增加,可以將圖片轉(zhuǎn)成webp并進(jìn)行壓縮

RGB565

除了圖片資源的文件夾,加載圖片時(shí)使用的色彩模式也影響了Bitmap大小。ARGB8888使用了32bit,所以一個(gè)像素需要4byte;RGB565使用了16bit,一個(gè)像素只需要2byte
但是因?yàn)镽GB565少了alpha通道,對(duì)有透明度的圖片顯示有問(wèn)題,而且顯示效果上還是有些區(qū)別,所以并不建議修改這個(gè)屬性,只是在對(duì)內(nèi)存有嚴(yán)格要求的場(chǎng)景下可以作為特殊手段進(jìn)行優(yōu)化

ProGuard對(duì)內(nèi)存的影響

壓縮代碼和資源

  • ProGuard可以對(duì)類、方法和變量重命名,剔除無(wú)用代碼和資源,減小dex大小,除了減小了apk的大小,同時(shí)也減小了加載dex所需的內(nèi)存

  • 因?yàn)樘摂M機(jī)加載dex文件是按需加載的,而內(nèi)存分配的最小單位是頁(yè),所以加載一個(gè)功能的代碼時(shí)同一個(gè)內(nèi)存頁(yè)中也會(huì)加載dex文件中該功能前后不相關(guān)的代碼,ProGuard可以重新排序類的字節(jié)碼在dex文件中位置,使得有相互調(diào)用關(guān)系的類在dex中更加緊湊,加載相同功能所需的內(nèi)存更小

內(nèi)存碎片

Overview of memory management
內(nèi)存碎片

Davik的內(nèi)存回收算法不能移動(dòng)對(duì)象,所以會(huì)造成一個(gè)小對(duì)象占據(jù)整個(gè)內(nèi)存頁(yè),產(chǎn)生內(nèi)存碎片
而ART虛擬機(jī)的可以在GC時(shí)對(duì)內(nèi)存空間進(jìn)行整理,隨著5.0以上系統(tǒng)的占有率逐漸提升,內(nèi)存碎片造成的內(nèi)存消耗可以不必過(guò)于關(guān)心

其他內(nèi)存問(wèn)題

Manage your app's memory

  • 頁(yè)面不可見(jiàn)收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)時(shí)釋放UI資源

  • 通過(guò)getMemoryInfo()獲取內(nèi)存信息,保證自己不開(kāi)辟大內(nèi)存導(dǎo)致oom

  • 謹(jǐn)慎的使用Service

    • 使用IntentService

    • 使用JobScheduler進(jìn)行后臺(tái)調(diào)度

  • 使用優(yōu)化的容器如SparseArray

  • 代碼抽象會(huì)帶來(lái)額外的內(nèi)存消耗

  • 使用@IntDef、@StringDef代替枚舉
    ...

  • image
image

+qq群457848807:。獲取以上高清技術(shù)思維圖,以及相關(guān)技術(shù)的免費(fèi)視頻學(xué)習(xí)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容