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.mzph.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 本身已自带高效回收复…