android效能評測與優化-記憶體

Android心路歷程發表於2019-04-12

記憶體效能分析及優化的意義

Overview of memory management
記憶體管理介紹

OOM

  • 系統分配給app的堆記憶體是有上限的,不是系統空閒多少記憶體app就可以用多少,getMemoryClass()可以獲取到這個值

  • 可以在manifest檔案中設定largeHeap為true,這樣會增大堆記憶體上限,getLargeMemoryClass()可以獲取到這個值

  • 超出虛擬機器堆記憶體上限會造成OOM

Low Memory Killer

  • android記憶體管理使用了分頁(paging)和記憶體對映(memory-mapping)技術,但是沒有使用swap,而是使用Low Memory Killer策略來提升實體記憶體的利用率 ,導致除了gc和殺死程式回收實體記憶體之外沒有其他方式來利用已經被佔用的記憶體

  • 當前臺應用切換到後臺後,系統並不結束它的程式,而是把它快取起來,供下次啟動。當系統記憶體不足時,按最近最少使用+優先釋放記憶體使用密集的策略釋放快取程式。

GC

  • 記憶體使用的多也會造成GC速度變慢,造成卡頓

  • 記憶體佔用過高,在建立物件時記憶體不足,很容易造成觸發GC影響APP效能

綜上

關注並減少應用的記憶體消耗可以減少oom的概率,在記憶體緊張的場景下獲得更好的使用者體驗,也可以增加應用的後臺存活時間

工具介紹

調查 RAM 使用情況

  • GC-LOG

  • dumpsys meminfo

  • Profiler

  • jhat

dumpsys procstats

用來衡量一段時間內應用消耗記憶體的情況

  • PSS:Proportional Set Size按比例分配共享記憶體的實際記憶體

  • USS:Unique Set Size程式私有記憶體
    PSS=USS+共享記憶體/共享記憶體的程式數

(最小PSS-平均PSS-最大PSS/最小USS-平均USS-最大USS)

android效能評測與優化-記憶體
procstats

LeakCanary

檢測記憶體洩漏的工具

MAT

比較常用的記憶體dump檔案分析工具

使用方法

  • 使用Memory Profiler Dump記憶體資料

  • 匯出的hprof檔案不是MAT的標準檔案,需要使用sdk帶的hprof-conv轉換

hprof-conv -z src dst //-z可以排除android框架建立的物件
複製程式碼

使用場景

  • 總體性找出記憶體優化的瓶頸

  • 只有dump檔案的現實場景,或者無法定位具體問題等只有現場而沒有線索的情況下庖丁解牛的工具

  • 對於專項功能的記憶體優化感覺不如程式碼除錯+profiler

分析場景構建

效能測試的一些注意點

  • 需要考慮儘量真實的場景

  • 需要關閉log等除錯元件避免干擾

常見的效能測試方式

  • 切換到後臺

  • 反覆執行功能

  • 長時間執行功能

  • 多個場景來回切換

容易出現記憶體問題的場景

  • 包含了圖片顯示的介面

  • 網路傳輸大量資料的場景

  • 需要快取資料的場景

常見的記憶體問題

記憶體洩漏

記憶體洩漏產生的原因

一個物件的生命週期已經結束了,但是有其他物件持有了它的例項導致無法在GC時被回收,在Android中通常是Activity在finish之後依然有物件引用它導致記憶體洩漏

記憶體洩漏的常見場景

  • 非同步操作中非同步邏輯未結束,而Activity結束或者重建了

  • Thread/Handler/AsyncTask/Rxjava/Timer等

  • 使用靜態變數或者單例直接或者間接的儲存Activty例項但是未及時釋放

  • 註冊廣播未登出

  • ObjectAnimator未呼叫cancel

  • I/O操作等完成後未及時關閉或者釋放

  • WebView造成的記憶體洩漏 Android 5.1 WebView記憶體洩漏分析

記憶體洩漏在分析工具上的表現

android效能評測與優化-記憶體
記憶體洩漏

每次activity的重建都會造成記憶體上升且gc不會使記憶體使用降低

記憶體洩漏的避免

  • LeakCanary

  • StrictMode

  • 沒有必要使用Activity作為Context的地方全部使用ApplicationContext

  • 使用WeakReference

  • 使用ViewModel+LiveData/RxJava+Rxlifecycle等工具實現非同步邏輯避免記憶體洩漏

  • 對需要銷燬時進行處理的操作進行檢查,如xxx.cancel()/xxx.close()/xxx.unregister()/xxx.remove()等操作

記憶體抖動

記憶體抖動的原因

記憶體抖動一般是瞬間建立了大量物件,會在短時間內觸發多次GC,產生卡頓

記憶體抖動的場景

  • IM通知需要轉發到所有WebView介面,當剛開啟APP時多個通知同時到達,或者在群聊中訊息很多的場景下,會造成短時間內頻繁GC,同時伴隨介面卡頓

記憶體抖動的在分析工具上的表現

android效能評測與優化-記憶體
記憶體抖動

製造了一個記憶體抖動的場景

 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();
            }
        }
        //優化以前
        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();
            }
        }
        //優化以後
        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不太複雜,GC耗時不長)

  • 個人感覺卡頓主要是因為記憶體抖動大多出現在一些複雜場景,通常伴隨著主執行緒的大量操作已經出現了卡頓,而記憶體抖動引起的頻繁GC會加劇卡頓的程度

解決方案

  • 最簡單的做法就是把之前的主執行緒操作放到子執行緒去,雖然記憶體抖動依然存在,但是卡頓問題可以大大緩解

  • 對於記憶體抖動本身

    • 儘量避免在迴圈體內建立物件,應該把物件建立移到迴圈體外

    • 需要大量使用Bitmap和其他大型物件時,儘量嘗試複用之前建立的物件

  • 對於黑盒子(例如之前例子中im通知造成的webview的記憶體抖動和主執行緒耗時操作)

    • 控制觸發頻率,減輕卡頓程度

    • 新增序號產生器制,需要接收通知的頁面才傳送通知

圖片載入的記憶體佔用

不同dpi資料夾對圖片記憶體的影響

  • 不同dpi限定符對應的dpi
    xxxhdpi-640

xxhdpi-480
xhdpi-320
mdpi-160

  • 通過resId載入的Bitmap的寬高計算
    bitmap寬高=圖片實際寬高*螢幕dpi/資料夾對應的dpi

  • nodpi
    從這個資料夾中載入的圖片資源生成的Bitmap會保持圖片本身的尺寸

  • 1920*1080圖片資源放在不同的資料夾下載入的Bitmap大小計算
    使用裝置小米note,裝置dpi為440

資料夾對應dpibitmap widthheightsize倍數
nodpi1920108082944001
xxxhdpi640132074339230400.47
xxhdpi480176099069696000.84
xhdpi32026401485156816001.89
mdpi16052802970627264007.56
android效能評測與優化-記憶體
圖片載入

使用圖片的建議

  • 儘量使用1080p的尺寸下的切圖

  • 圖片儘量放xxhdpi以上的資料夾下

  • 大圖如Splash頁和引導頁的圖片放在nodpi資料夾下,通過控制ImageView大小來限制圖片大小

  • 按照上面操作會導致apk大小增加,可以將圖片轉成webp並進行壓縮

RGB565

除了圖片資源的資料夾,載入圖片時使用的色彩模式也影響了Bitmap大小。ARGB8888使用了32bit,所以一個畫素需要4byte;RGB565使用了16bit,一個畫素只需要2byte
但是因為RGB565少了alpha通道,對有透明度的圖片顯示有問題,而且顯示效果上還是有些區別,所以並不建議修改這個屬性,只是在對記憶體有嚴格要求的場景下可以作為特殊手段進行優化

ProGuard對記憶體的影響

壓縮程式碼和資源

  • ProGuard可以對類、方法和變數重新命名,剔除無用程式碼和資源,減小dex大小,除了減小了apk的大小,同時也減小了載入dex所需的記憶體

  • 因為虛擬機器載入dex檔案是按需載入的,而記憶體分配的最小單位是頁,所以載入一個功能的程式碼時同一個記憶體頁中也會載入dex檔案中該功能前後不相關的程式碼,ProGuard可以重新排序類的位元組碼在dex檔案中位置,使得有相互呼叫關係的類在dex中更加緊湊,載入相同功能所需的記憶體更小

記憶體碎片

android效能評測與優化-記憶體
Overview of memory management
android效能評測與優化-記憶體
記憶體碎片

Davik的記憶體回收演算法不能移動物件,所以會造成一個小物件佔據整個記憶體頁,產生記憶體碎片
而ART虛擬機器的可以在GC時對記憶體空間進行整理,隨著5.0以上系統的佔有率逐漸提升,記憶體碎片造成的記憶體消耗可以不必過於關心

其他記憶體問題

Manage your app's memory

  • 頁面不可見收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)時釋放UI資源

  • 通過getMemoryInfo()獲取記憶體資訊,保證自己不開闢大記憶體導致oom

  • 謹慎的使用Service

    • 使用IntentService

    • 使用JobScheduler進行後臺排程

  • 使用優化的容器如SparseArray

  • 程式碼抽象會帶來額外的記憶體消耗

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


相關文章