Android 性能優化入門(二)—— 內存優化

1、概述

1.1 Java 對象的生命周期

各狀態含義:

  • 創建:分配內存空間并調用構造方法
  • 應用:使用中,處于被強引用持有(至少一個)的狀態
  • 不可見:不被強引用持有,應用程序已經不再使用該對象了,但是它仍然保存在內存中
  • 不可達:GC 運行時檢測到(可達性分析)了該對象不再被任何強引用持有,即根不可達
  • 收集:被 GC 標記收集準備回收
  • 終結:調用該對象的 finalized(),如果 finalized() 內部沒有拯救該對象的措施(即便拯救也只能躲過一次 GC),就會執行回收過程
  • 對象空間重新分配:對象已經被回收,其占用空間會被重新分配

1.2 JVM 的堆區內存劃分示意圖

對象在內存區域中流動的大致步驟:

  1. 對象創建后在 Eden 區
  2. 執行 GC 后,如果對象仍然存活,則復制到 S0 區
  3. 當 S0 區滿時,該區域存活對象將復制到 S1 區,然后 S0 清空,接下來 S0 和 S1 角色互換
  4. 當第 3 步達到一定次數(系統版本不同會有差異)后,存活對象將被復制到 Old Generation
  5. 當這個對象在 Old Generation 區域停留的時間達到一定程度時,最后會移動到 Permanent Generation 區域

Android 系統使用的虛擬機在 JVM 的基礎上又會多出幾個區域。Dalvik 虛擬機多出:

  • Linear Alloc:匿名共享內存
  • Zygote Space:Zygote 相關信息
  • Alloc Space:每個進程獨占

而 ART 虛擬機多出:

  • NonMoving Space
  • Zygote Space
  • Alloc Space
  • Image Space:預加載的類信息(預加載是 Zygote 啟動過程中執行的任務)
  • Large Obj Space:分配大對象的區域,如 Bitmap。

此外還需回憶在 Java 專題中講過的:

  1. 可回收對象的判定:不被 GC roots 直接或間接持有的對象是可回收的,GC roots 包括靜態變量、線程棧變量、常量池和 JNI(指針)
  2. Java 的四種引用:強>軟(內存不足時回收)>弱(GC 時回收)>虛
  3. 垃圾回收算法(面試必問):
    • 標記清除算法:位置不連續(有內存碎片)、效率略低、兩次掃描(第一次標記,第二次回收)
    • 復制算法:實現簡單、運行高效、沒有內存碎片但空間利用率只有一半
    • 標記整理算法:沒有內存碎片、效率偏低、兩次掃描(第一次標記,第二次整理)、指針需要調整
    • 分代收集算法:未創建新的算法,只是在不同的內存區域使用以上不同的算法

1.3 app 內存組成與限制

Android 系統給每個 app 分配一個虛擬機 Dalvik/ART,讓 app 運行在虛擬機上,即便 app 崩潰也不會影響到系統。系統給虛擬機分配了一定的內存大小,app 可以申請使用的內存大小不能超過此硬性邏輯限制,就算物理內存富余,如果應用超出虛擬機的最大內存就會發生內存溢出。

由程序控制操作的內存空間在堆上,分為 java heapsize 和 native heapsize。Java 申請的內存在 java heapsize 上,如果超過虛擬機的邏輯內存大小就會發生內存溢出的異常;而 native 層的內存申請不受到這個虛擬機的邏輯大小限制,而是受 native process 對內存大小的限制。

通常手機的 RAM 為 4G、8G 甚至 12G,但是每個 app 并不會有太大的內存,通過 adb shell cat /system/build.prop 命令可以看到,虛擬機堆的初始大小為 16M,最大堆內存為 128M:

這個初始大小和最大值,各個手機廠商會自行修改,不同的系統和機型都有可能不同。只不過 Android 系統源碼設置的是 16M,在 AndroidRuntime.cpp 中:

    int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote){/** The default starting and maximum size of the heap.  Larger* values should be specified in a product property override.*/parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m"); //修改這里}

可以看到給 “dalvik.vm.heapsize” 設置的大小為 16M,可以通過修改這個值改變初始的虛擬機堆大小,也可以通過修改 platform/dalvik/+/eclair-release/vm/Init.c 文件:

    gDvm.heapSizeStart = 2 * 1024 * 1024;   // Spec says 16MB; too big for us.gDvm.heapSizeMax = 16 * 1024 * 1024;    // Spec says 75% physical mem

要獲取這個值的話,可以在代碼中通過 AMS 獲取:

    ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)activityManager.getMemoryClass(); // 以 m 為單位

其實 AMS 是通過在 AMS.setSystemProcess() 內注冊的 meminfobinder 獲取到內存信息的,另外 ActivityManager 中還有 MemoryInfo 這個成員。

此外還可以通過 adb shell cat /proc/meminfo 命令查看內存信息:

1.4 Android 的低內存殺進程機制

oom_adj 在講 AMS 源碼時有講過,可以去復習一下。

AMS 中的 oom_adj 會對應用分級,值為 [-16,15],值越小越不容易被殺,這是一個粗粒度的。此外還有一個 oom_score_adj 評分 [-1000,1000],分越高越容易被殺掉。

通過 adb shell cat /proc/pid/oom_adj 查看 pid 對應的 app 的值,在前臺時為 0,按 home 鍵讓其退到后臺這個值就會變成 11。如果有兩個應用都是 11,那么誰占用的內存大就殺誰,因此【降低應用進入后臺后所占用的內存】也是一種保活方法。

1.5 內存三大問題

內存抖動:內存波動圖形呈鋸齒狀,頻繁 GC 導致卡頓。

內存泄漏:在當前應用周期內不再使用的對象被 GC Roots 引用,導致不能回收,使實際可使用內存變小。

內存溢出:即 OOM,OOM 時會導致程序異常。Android 設備出廠以后,Java 虛擬機對單個應用的最大內存分配就確定下來了,超出這個值就會 OOM。OOM 可以分為如下基幾類:

  • Java 堆內存溢出
  • 無足夠連續內存
  • FD 數量超出限制
  • 線程數量超出限制
  • 虛擬內存不足

2、常見分析內存的命令

2.1 adb shell dumpsys meminfo

輸出系統內各個應用占用內存信息,以及分類的內存信息:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

按照 oom_adj 排序的信息:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

按照文件類型分類的信息:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

Total RAM 是總的運行內存,Free RAM 是當前可用內存,Used RAM 是當前已使用的內存。

上圖的 PSS 是內存指標概念,與之類似的還有幾個,如下表:

Item全稱含義等價
USSUnique Set Size物理內存進程獨占的內存
PSSProportional Set Size物理內存PSS= USS+ 按比例包含共享庫
RSSResident Set Size物理內存RSS= USS+ 包含共享庫
VSSVirtual Set Size虛擬內存VSS= RSS+ 未分配實際物理內存

其中 VSS >= RSS >= PSS >= USS,但 /dev/kgsl-3d0 部份必須考慮 VSS。

此外還可以通過加 --package 參數查詢某個應用的內存情況,如 adb shell dumpsys meminfo --package packageName:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

內存優化時可能會用到這個命令(只是大概判斷,不是精準判斷),比如說在復現前先打印一次內存信息 -> 復現可能的 OOM 操作 -> 再打印一次。

各種其他命令和參數含義去看預習資料 【內存OOM】。

3、常見分析工具

3.1 MAT

按包分類,然后右擊選擇一個類的 incoming references 和 outgoing references,前者表示持有該類實例的對象,后者表示該類持有哪些類的對象。

淺堆(Shallow Heap)與深堆(Retained Heap):前者只計算自身占用的空間,后者則計算本身以及它所引用的對象那一條鏈上的所有對象占用的空間。

比如說下圖:

A~G 每個對象都占 10 個內存單位,它們的淺堆都是 10,但是 B、C 的深堆就要計算上各自分支上的對象總和,即 30,而 A 要計算分支上所有對象的內存總和,即 70。

此時假如新來一個 H 引用 B,那么這時 A 的深堆變為 40,因為假如把 A 干掉的話,能釋放的是 A、C、F、G 這 4 個。新加入的 H 深堆為 10,因為上圖中將 H 干掉就只能釋放它自己一個對象,不會連帶其他對象一起被回收。但是假如在 A 釋放后再看 H 的深堆,那么就是 40,因為這個時候釋放 H 會實際釋放 H、B、D、E 這 4 個對象。

3.2 AS memory profile

看官網連接,介紹的十分詳細:

Inspect your app’s memory usage with Memory Profiler

3.3 LeakCanary

LeakCanary 會找出有泄漏嫌疑的對象,并通過 haha 這個開源庫進行可達性分析確定是否發生了泄漏。它的使用非常簡單,僅需要添加如下依賴:

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

然后如果在應用運行時發生了內存泄漏就會在 UI 上提示我們,還可以保存成 hprof 文件交給 MAT 作進一步分析。

LeakCanary 是如何做到僅添加了一個依賴就幫助應用定位內存泄漏問題的呢?

初始化

首先,在 LeakCanary 的部分模塊的 AndroidManifest.xml 中會聲明一些 Provider,這些 Provider 在 apk 打包時會匯入 mergeAndroidManifest.xml,最后體現在 app 的 AndroidManifest.xml 文件中。

由于在 AMS 啟動過程中,會先執行 ContentProvider 的 onCreate(),后執行 Application 的 onCreate():

LeakCanary 正是利用這一點,在 MainProcessAppWatcherInstaller 中初始化:

/*** Content providers are loaded before the application class is created. [MainProcessAppWatcherInstaller] is* used to install [leakcanary.AppWatcher] on application start.** [MainProcessAppWatcherInstaller] automatically sets up the LeakCanary code that runs in the main* app process.*/
internal class MainProcessAppWatcherInstaller : ContentProvider() {override fun onCreate(): Boolean {val application = context!!.applicationContext as ApplicationAppWatcher.manualInstall(application)return true}
}

監聽生命周期

注冊 Application.ActivityLifecycleCallbacks 這個生命周期回調來監聽 Activity 何時被銷毀:

class ActivityWatcher(private val application: Application,private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {private val lifecycleCallbacks =object : Application.ActivityLifecycleCallbacks by noOpDelegate() {override fun onActivityDestroyed(activity: Activity) {reachabilityWatcher.expectWeaklyReachable(activity, "${activity::class.java.name} received Activity#onDestroy() callback")}}override fun install() {application.registerActivityLifecycleCallbacks(lifecycleCallbacks)}override fun uninstall() {application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)}
}

ReferenceQueue

LeakCanary 的核心原理是:一個 Reference 對象(一般使用 WeakReference)所引用的對象被 GC 回收時,這個 Reference 對象會被添加到與之關聯的(一般通過構造方法關聯)引用隊列 ReferenceQueue 的隊列末尾。以下面代碼為例:

    public static void main(String[] args) {Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();// 與 ReferenceQueue 關聯WeakReference<Object> weakReference = new WeakReference<>(obj, queue);System.out.println("weakReference: " + weakReference);System.out.println("ReferenceQueue: " + queue.poll());obj = null;// Runtime.gc()一定會執行 GC;而 System.gc() 優先級低,調用后也不知何時執行,// 僅僅是通知系統在合適的時間 GC,并不保證一定執行Runtime.getRuntime().gc();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("weakReference: " + weakReference);System.out.println("ReferenceQueue: " + queue.poll());}

輸出為:

weakReference: java.lang.ref.WeakReference@15db9742
ReferenceQueue: null
weakReference: java.lang.ref.WeakReference@15db9742
ReferenceQueue: java.lang.ref.WeakReference@15db9742

可以證明 WeakReference 持有的對象被回收后,該 WeakReference 對象被添加到了與之關聯的 ReferenceQueue 的隊尾。

LeakCanary 利用這一點去檢測可能的內存泄漏,具體步驟為:

  1. 給每個對象生成一個 uuid,放到觀察列表 watchedReferences 中,觀察 5 秒
  2. 5 秒后,調用一次 GC,去 ReferenceQueue 中查找是否有 WeakReference,有說明對象已經被回收,從 watchedReferences 中將其移除。否則該對象沒有被回收,則有可能發生內存泄漏,將其添加到懷疑列表 retainedReferences 中
  3. 當懷疑列表 retainedReferences 中的元素數量大于 5 個時,就交給開源庫 haha 去做可達性分析

簡版的 LeakCanary 源碼分析看視頻

4、Bitmap 使用

Bitmap 的內存問題解決好就解決了 90% 的 OOM 問題。基本上加載圖片都用的 Glide。不用的話可以參考官網的資料:緩存位圖 等等。

Bitmap 解碼的 decodeXXX() 最終都會去 native 執行。看看預習資料【內存OOM】。

圖片在不同分辨率的設備上的內存中大小可能不一樣。當然這是圖片放在 xxx-xdpi 中的情況,需要根據公式去算 density。但如果圖片來源于網絡或者其他不是 xxx-xdpi 文件夾中的情況,density 就是 1。

gradle 可以控制只打包一個維度的 xxx-hdpi 包,不打其他密度的。

解析 Bitmap 的一個技巧:

    try {decode bitmap} catch(OutOfMemoryError e) {對 bitmap 進行質量壓縮}

加入解析 Bitmap 時發生了 OOM,可以在 catch 中通過質量壓縮的方式重新解析該 Bitmap。

第三方開源庫 epic 可以 hook ImageView 設置圖片的過程,檢測圖片的質量。

5、總體優化思路

來自于張紹文的 Android 開發高手課。

總體思想:

  1. 設備分級:
  2. Bitmap優化
    統一圖片庫
    線上線下監控 hook

盡量使用官方推薦的 glide

還可以做一些兜底操作,如果檢測到某個 Activity 發生了內存泄漏,那么可以在它的 onDestroy() 中遍歷所有成員變量和 View 樹并將他們置為 null。

此外在 Activity 中有兩個方法 onTrimMemory() 和 onLowMemory(),分別在 App 內存不足和整個設備內存不足時回調,可以在這兩個方法中主動釋放一些內存。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/81447.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/81447.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/81447.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

GCC 版本與C++ 標準對應關系

GCC 版本 與支持的 C 標準&#xff08;C11、C14、C17、C20、C23&#xff09; 的對應關系 GCC 版本與 C 標準支持對照表 GCC 版本默認 C 標準C11C14C17C20C23GCC 4.8C98? (部分支持)????GCC 4.9C98? (完整支持)????GCC 5.1C98?? (完整支持)???GCC 6.1C14??? …

5、事務和limit補充

一、事務【都是重點】 1、了解 一個事務其實就是一個完整的業務邏輯。 要么同時發生&#xff0c;要么同時結束。 是一個最小的工作單元。 不可再分。 看這個視頻&#xff0c;黑馬的&#xff0c;4分鐘多點就能理解到 可以理解成&#xff1a; 開始事務-----如果中間拋出異常…

一套基于 Bootstrap 和 .NET Blazor 的開源企業級組件庫

前言 今天大姚給大家分享一套基于 Bootstrap 和 .NET Blazor 的開源企業級組件庫&#xff1a;Bootstrap Blazor。 項目介紹 BootstrapBlazor 是一套基于 Bootstrap 和 Blazor 的開源&#xff08;Apache License&#xff09;、企業級組件庫&#xff0c;無縫整合了 Bootstrap …

mac-M系列芯片安裝軟件報錯:***已損壞,無法打開。推出磁盤問題

因為你安裝的軟件在Intel 或arm芯片的mac上沒有簽名導致。 首先打開任何來源操作 在系統設置中配置&#xff0c;如下圖&#xff1a; 2. 然后打開終端&#xff0c;輸入&#xff1a; sudo spctl --master-disable然后輸入電腦鎖屏密碼 打開了任何來源&#xff0c;還遇到已損壞…

RK3568-鴻蒙5.1與原生固件-扇區對比分析

編譯生成的固件目錄地址 ../openharmony/out/rk3568/packages/phone/images鴻蒙OS RK3568固件分析 通過查看提供的信息&#xff0c;分析RK3568開發板固件的各個組件及其用途&#xff1a; 主要固件組件 根據終端輸出的文件列表&#xff0c;RK3568固件包含以下關鍵組件&#x…

Java正則表達式:從基礎到高級應用全解析

Java正則表達式應用與知識點詳解 一、正則表達式基礎概念 正則表達式(Regular Expression)是通過特定語法規則描述字符串模式的工具&#xff0c;常用于&#xff1a; 數據格式驗證文本搜索與替換字符串分割模式匹配提取 Java通過java.util.regex包提供支持&#xff0c;核心類…

進程間通信--信號量【Linux操作系統】

文章目錄 并發編程相關基礎概念信號量深刻理解信號量使用共享資源的方式分塊使用共享資源的方式會出現的問題舉例子理解信號量的第二個特性---預定信號量要成為計數器面臨的問題 信號量相關操作接口--POSIX庫函數&#xff1a;sem_init庫函數&#xff1a;sem_destroy庫函數&…

謝賽寧團隊提出 BLIP3-o:融合自回歸與擴散模型的統一多模態架構,開創CLIP特征驅動的圖像理解與生成新范式

BLIP3-o 是一個統一的多模態模型&#xff0c;它將自回歸模型的推理和指令遵循優勢與擴散模型的生成能力相結合。與之前擴散 VAE 特征或原始像素的研究不同&#xff0c;BLIP3-o 擴散了語義豐富的CLIP 圖像特征&#xff0c;從而為圖像理解和生成構建了強大而高效的架構。 此外還…

HarmonyOs開發之——— ArkWeb 實戰指南

HarmonyOs開發之——— ArkWeb 實戰指南 謝謝關注!! 前言:上一篇文章主要介紹HarmonyOs開發之———合理使用動畫與轉場:CSDN 博客鏈接 一、ArkWeb 組件基礎與生命周期管理 1.1 Web 組件核心能力概述 ArkWeb 的Web組件支持加載本地或在線網頁,提供完整的生命周期回調體…

黑馬程序員C++2024版筆記 第0章 C++入門

1.C代碼的基礎結構 以hello_world代碼為例&#xff1a; 預處理指令 #include<iostream> using namespace std; 代碼前2行是預處理指令&#xff0c;即代碼編譯前的準備工作。&#xff08;編譯是將源代碼轉化為可執行程序.exe文件的過程&#xff09; 主函數 主函數是…

日語學習-日語知識點小記-構建基礎-JLPT-N4階段(22):復習

日語學習-日語知識點小記-構建基礎-JLPT-N4階段(22):復習 1、前言(1)情況說明(2)工程師的信仰2、知識點(1)復習(2)復習3、單詞(1)日語(2)日語片假名單詞4、對話練習5、單詞辨析記錄6、總結1、前言 (1)情況說明 自己在今年,在日本留學中,目前在語言學校,…

Docker配置SRS服務器 ,ffmpeg使用rtmp協議推流+vlc拉流

目錄 演示視頻 前期配置 Docker配置 ffmpeg配置 vlc配置 下載并運行 SRS 服務 推拉流流程實現 演示視頻 2025-05-18 21-48-01 前期配置 Docker配置 運行 SRS 建議使用 Docker 配置 Docker 請移步&#xff1a; 一篇就夠&#xff01;Windows上Docker Desktop安裝 漢化完整指…

Redis——緩存雪崩、擊穿、穿透

緩存雪崩 大量緩存數據在同一時間過期或者Redis故障宕機時&#xff0c;若此時有大量請求&#xff0c;都會直接訪問到數據庫&#xff0c;導致數據庫壓力倍增甚至宕機。 大量數據同時過期解決方案&#xff1a; 1、均勻設置過期時間&#xff1a; 設置過期時間的時候可以追加一…

開源GPU架構RISC-V VCIX的深度學習潛力測試:從RTL仿真到MNIST實戰

點擊 “AladdinEdu&#xff0c;同學們用得起的【H卡】算力平臺”&#xff0c;H卡級別算力&#xff0c;按量計費&#xff0c;靈活彈性&#xff0c;頂級配置&#xff0c;學生專屬優惠。 一、開篇&#xff1a;AI芯片架構演變的三重挑戰 &#xff08;引述TPUv4采用RISC-V的行業案…

字符串相乘(43)

43. 字符串相乘 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; class Solution { public:string multiply(string num1, string num2) {string res "0";for (int i 0; i < num2.size(); i) {string str multiplyOneNum(num1, num2[num2.size() -…

mathematics-2024《Graph Convolutional Network for Image Restoration: A Survey》

推薦深藍學院的《深度神經網絡加速&#xff1a;cuDNN 與 TensorRT》&#xff0c;課程面向就業&#xff0c;細致講解CUDA運算的理論支撐與實踐&#xff0c;學完可以系統化掌握CUDA基礎編程知識以及TensorRT實戰&#xff0c;并且能夠利用GPU開發高性能、高并發的軟件系統&#xf…

[LevelDB]LevelDB版本管理的黑魔法-為什么能在不鎖表的情況下管理數據?

文章摘要 LevelDB的日志管理系統是怎么通過雙鏈表來進行數據管理為什么LevelDB能夠在不鎖表的情況下進行日志新增 適用人群: 對版本管理機制有開發訴求&#xff0c;并且希望參考LevelDB的版本開發機制。數據庫相關從業者的專業人士。計算機狂熱愛好者&#xff0c;對計算機的…

【C++進階篇】C++容器完全指南:掌握set和map的使用,提升編碼效率

C容器的實踐與應用&#xff1a;輕松掌握set、map與multimap的區別與用法 一. 序列式容器與關聯式容器1.1 序列式容器 (Sequential Containers)1.2 關聯式容器 (Associative Containers) 二. set系列使用2.1 set的構造和迭代器2.2 set的增刪查2.2.1 插入2.2.2 查找2.2.3 刪除 2.…

2_Spring【IOC容器中獲取組件Bean】

Spring中IOC容器中獲取組件Bean 實體類 //接口 public interface TestDemo {public void doSomething(); } // 實現類 public class HappyComponent implements TestDemo {public void doSomething() {System.out.println("HappyComponent is doing something...")…

安卓開飯-ScrollView內嵌套了多個RecyclerView,只想與其中一個RecyclerView有聯動

在 Android 開發中&#xff0c;將 RecyclerView 嵌套在 ScrollView 內通常會導致性能問題和滾動沖突&#xff0c;應盡量避免這種設計。以下是原因和替代方案&#xff1a; 為什么不推薦 RecyclerView ScrollView&#xff1f;?? 性能損耗? RecyclerView 本身已自帶高效回收復…