前言
这篇文章的内容是我回顾和再学习 Android 内存优化的过程中整理出来的,整理的目的是让我自己对 Android 内存优化相关知识的认识更全面一些,分享的目的是希望大家也能从这些知识中得到一些启发。
Android 应用运行在 Dalvik 虚拟机上,而 Dalvik 虚拟机是基于 JVM 优化而来的,因此只有了解了 JAVA 的内存管理机制,才能更好地理解 Android 的内存管理机制,如果你对这一块还不熟悉的话,可以看我的上一篇文章 探索 Java 内存管理机制。
本文的内容可分为两部分,第一部分讲的是 Android 内存管理机制相关的一些知识,第二部分讲的是内存问题的解决与内存优化方法,大家可以根据自己的需要选择性地阅读。
1. 为什么要做内存优化?
内存优化就是对内存问题的一个预防和解决,做内存优化能让应用挂得少、活得好和活得久。
2. 什么是 Dalvik?
要了解 Android 应用的内存管理机制,就要了解承载着 Android 应用的虚拟机 Dalvik,虽然 Android 现在是使用的 ART 来承载应用的执行,但是 ART 也是基于 Dalvik 优化而来的。
Dalvik 是 Dalvik Virtual machine(Dalvik 虚拟机)的简称,是 Android 平台的核心组成部分之一,Dalvik 与 JVM 的区别有如下几个。
2.1 Dalvik 与 JVM 的区别
2.2 Dalvik 堆大小
每一个手机厂商都可以设定设备中每一个进程能够使用的堆大小,设置进程堆大小的值有下面三个。
假如我们想看其中的一个值,我们可以通过命令查看,比如下面这条命令。
adb shell getprop dalvik.vm.heapsize
3. 什么是 ART?
ART 的全称是 Android Runtime,是从 Android 4.4 开始新增的应用运行时环境,用于替代 Dalvik 虚拟机。
Dalvik VM 和 ART 都可以支持已转换为 .dex(Dalvik Executable)格式的 Java 应用程序的运行。
ART 与 Dalvik 的区别有下面几个。
4. 什么是低杀?4.1 低杀简介
在 Android 中有一个心狠手辣的杀手,要想让我们的应用活下来,就要在开发应用时格外小心。
不过我们也不用太担心,因为它只杀“坏蛋”,只要我们不使坏,那它就不会对我们下手。
这个杀手叫低杀,它的全名是 Low Memory Killer。
低杀跟垃圾回收器 GC 很像,GC 的作用是保证应用有足够的内存可以使用,而低杀的作用是保证系统有足够的内存可以使用。
GC 会按照引用的强度来回收对象,而低杀会按照进程的优先级来回收资源,下面我们就来看看 Android 中的几种进程优先级。
4.2 进程优先级
在 Android 中不同的进程有着不同的优先级,当两个进程的优先级相同时,低杀会优先考虑干掉消耗内存更多的进程。
也就是如果我们应用占用的内存比其他应用少,并且处于后台时,我们的应用能在后台活下来,这也是内存优化为我们应用带来竞争力的一个直接体现。
当用户通过多次点击达到一个页面,然后又打开了其他应用时,这时我们的应用处于后台,如果我们的应用在后台能活下来,意味着当用户再次启动我们的应用时,不需要再次进行这个繁琐的操作。
4.2.1 前台进程
前台进程(Foreground Process)是优先级最高的进程,是正在于用户交互的进程,如果满足下面一种情况,则一个进程被认为是前台进程。
4.2.2 可见进程
可见进程(Visible Process)不含有任何前台组件,但用户还能再屏幕上看见它,当满足一下任一条件时,进程被认定是可见进程。
可见进程是非常重要的进程,除非前台进程已经把系统的可用内存耗光,否则系统不会终止可见进程。
4.2.3 服务进程
服务进程(Service Process)可能在播放音乐或在后台下载文件,除非系统内存不足,否则系统会尽量维持服务进程的运行。
当一个进程满足下面一个条件时,系统会认定它为服务进程。
4.2.4 后台进程
当一个进程满足下面条件时,系统会认定它为后台进程。
系统会把后台进程保存在一个 LruCache 列表中,因为终止后台进程对用户体验影响不大,所以系统会酌情清理部分后台进程。
你可以在 Activity 的 onSaveInstanceState 方法中保存一些数据,以免在应用被系统清理掉后,用户已输入的信息被清空,导致要重新输入。
4.2.5 空进程
当一个进程不包含任何活跃的应用组件,则被系统认定为是空进程。
系统保留空进程的目的是为了加快下次启动进程的速度。
5. 图片对内存有什么影响?
大部分 App 都免不了使用大量的图片,比如电商应用和外卖应用等。
图片在 Android 中对应的是 Bitmap 和 Drawable 类,我们从网络上加载下来的图片最终会转化为 Bitmap。
图片会消耗大量内存,如果使用图片不当,很容易就会造成 OOM。
下面我们来看下 Bitmap 与内存有关的一些内容。
5.1 获取 Bitmap 占用的内存大小
5.2 Bitmap 像素大小
一张图片中每一个像素的大小取决于它的解码选项,而 Android 中能够选择的 Bitmap 解码选项有四种。
下面四种解码选项中的的 ARGB 分别代表透明度和三原色 Alpha、Red、Green、Blue。
5.3 Glide
如果服务器返回给我们的图片是 200 * 200,但是我们的 ImageView 大小是 100 * 100,如果直接把图片加载到 ImageView 中,那就是一种内存浪费。
但是使用的 Glide 的话,那这个问题就不用担心了,因为 Glide 会根据 ImageView 的大小把图片大小调整成 ImageView 的大小加载图片,并且 Glide 有三级缓存,在内存缓存中,Glide 会根据屏幕大小选择合适的大小作为图片内存缓存区的大小。
6. 什么是内存泄漏?6.1 内存泄漏简介
内存泄漏指的是,当一块内存没有被使用,但无法被 GC 时的情况。
堆中一块泄漏的内存就像是地上一块扫不掉的口香糖,都很让人讨厌。
一个典型的例子就是匿名内部类持有外部类的引用,外部类应该被销毁时,GC 却无法回收它,比如在 Activity 中创建 Handler 就有可能出现这种情况。
内存泄漏的表现就是可用内存逐渐减少,比如下图中是一种比较严重的内存泄漏现象,无法被回收的内存逐渐累积,直到无更多可用内存可申请时,就会导致 OOM。
6.2 常见的内存泄漏原因
常见的造成内存泄漏的原因有如下几个。
6.2.1 非静态内部类
Activity activity;
publicMyHandler(Activity activity){
activity = newWeakReference<>(activity).get;
}
@Override
publicvoidhandleMessage(Message message){
// ...
}
}
6.2.2 静态变量
7. 什么是内存抖动?7.1 内存抖动简介
当我们在短时间内频繁创建大量临时对象时,就会引起内存抖动,比如在一个 for 循环中创建临时对象实例。
下面这张图就是内存抖动时的一个内存图表现,它的形状是锯齿形的,而中间的垃圾桶代表着一次 GC。
这个是 Memory Profiler 提供的内存实时图,后面会对 Memory Profiler 进行一个更详细的介绍。
7.2 预防内存抖动的方法
8. 什么是 Memory Profiler?8.1 Profiler8.1.1 Profiler 简介
Profiler 是 Android Studio 为我们提供的性能分析工具,它包含了 CPU、内存、网络以及电量的分析信息,而 Memory Profiler 则是 Profiler 中的其中一个版块。
打开 Profiler 有下面三种方式。
打开 Profiler 后,可以看到下面这样的面板,而在左边的 SESSIONS 面板的右上角,有一个加号,在这里可以选择我们想要进行分析的应用。
8.1.2 Profiler 高级选项
打开了高级选项后,我们在 Memory Profiler 中就能看到用一个白色垃圾桶表示的 GC 动作。
打开 Profiler 的方式:Run > Edit Configucation > Profiling > Enable advanced profiling
8.2 Memory Profiler 简介
Memory Profiler 是 Profiler 的其中一个功能,点击 Profiler 中蓝色的 Memory 面板,我们就进入了 Memory Profiler 界面。
8.3 堆转储
在堆转储(Dump Java Heap)面板中有 Instance View(实例视图)面板,Instance View 面板的下方有 References 和 Bitmap Preview 两个面板,通过 Bitmap Preview,我们能查看该 Bitmap 对应的图片是哪一张,通过这种方式,很容易就能找到图片导致的内存问题。
要注意的是,Bitmap Preview 功能只有在 7.1 及以下版本的设备中才能使用。
8.4 查看内存分配详情
在 7.1 及以下版本的设备中,可以通过 Record 按钮记录一段时间内的内存分配情况。
而在 8.0 及以上版本的设别中,可以通过拖动时间线来查看一段时间内的内存分配情况。
点击 Record 按钮后,Profiler 会为我们记录一段时间内的内存分配情况。在内存分配面板中,我们可以查看对象的分配的位置,比如下面的 Bitmap 就是在 onCreate 方法的 22 行创建的。
9. 什么是 MAT?9.1 MAT 介绍
对于内存泄漏问题,Memory Profiler 只能给我们提供一个简单的分析,不能够帮我们确认具体发生问题的地方。
而 MAT 就可以帮我们做到这一点,MAT 的全称是 Memory Analyzer Tool,它是一款功能强大的 Java 堆内存分析工具,可以用于查找内存泄漏以及查看内存消耗情况。
9.2 MAT 使用步骤
要想通过 MAT 分析内存泄漏,我们做下面几件事情。
9.3 注意事项
10. 怎么用 MAT 分析内存泄漏?
我在项目中定义了一个静态的回调列表 sCallbacks,并且把 MemoryLeakActivity 添加到了这个列表中,然后反复进出这个 Activity,我们可以看到这个 Activity 的实例有 8 个,这就属于内存泄漏现象,下面我们来看下怎么找出这个内存泄漏。
首先,按 8.3 小节的步骤打开我们的堆转储文件,打开后,我们可以看到 MAT 为我们分析的一个预览页。
打开左上角的直方图,我们可以看到一个类列表,输入我们想搜索的类,就可以看到它的实例数。
我们右键 MemoryLeakActivity 类,选择 List Objects > with incoming references 查看这个 Activity 的实例。
点击后,我们能看到一个实例列表,再右键其中一个实例,选择 Path to GC Roots > with all references 查看该实例被谁引用了,导致无法回收。
选择 with all references 后,我们可以看到该实例被静态对象 sCallbacks 持有,导致无法被释放。
这样就完成了一次内存泄漏的分析。
11. 什么是 LeakCanary?11.1 LeakCanary 简介
如果使用 MAT 来分析内存问题,会有一些难度,而且效率也不是很高。
为了能迅速发现内存泄漏,Square 公司基于 MAT 开源了 LeakCanary。
LeakCanary 是一个内存泄漏检测框架。
11.2 LeakCanary 原理
11.2 安装 LeakCanary11.2.1 AnroidX 项目
11.2.1 非 AndroidX 项目
当安装完成,并且重新安装了应用后,我们可以在桌面看到 LeakCanary 用于分析内存泄漏的应用。
下面这两张图中,第一个是 LeakCanary 为非 AndroidX 项目安装的应用,第二个是 LeakCanary 为 AndroidX 项目安装的应用。
11.4 使用 LeakCanary 分析内存泄漏
下面是一个静态变量持有 Activity 导致 Activity 无法被释放的一个例子。
publicclassMemoryLeakActivityextendsAppCompatActivity{
publicstaticList<Activity> activities = newArrayList<>;
@Override
protectedvoidonCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
activities.add( this);
}
}
我们可以在 Logcat 中看到泄漏实例的引用链。
除了 Logcat,我们还可以在 Leaks App 中看到引用链。
点击桌面上 LeakCanary 为我们安装的 Leaks 应用后,可以看到 activities 变量,之所以在这里会显示这个变量,是因为 LeakCanary 分析的结果是这个变量持有了某个实例,导致该实例无法被回收。
点击这一项泄漏信息,我们可以看到一个泄漏信息概览页。
我们点击第一项 MemoryActivity Leaked,可以看到泄漏引用链的详情。
通过上面这些步骤,很简单地就能找到 LeakCanary 为我们分析的导致内存泄漏的地方。
12. 怎么获取和监听系统内存状态?
Android 提供了两种方式让我们可以监听系统内存状态,下面我们就来看看这两种方式的用法。
12.1 ComponentCallback2
在 Android 4.0 后,Android 应用可以通过在 Activity 中实现 ComponentCallback2 接口获取系统内存的相关事件,这样就能在系统内存不足时提前知道这件事,提前做出释放内存的操作,避免我们自己的应用被系统干掉。
ComponentCallnback2 提供了 onTrimMemory(level) 回调方法,在这个方法里我们可以针对不同的事件做出不同的释放内存操作。
importandroid.content.ComponentCallbacks2
classMainActivity: AppCompatActivity, ComponentCallbacks2 {
/**
* 当应用处于后台或系统资源紧张时,我们可以在这里方法中释放资源,
* 避免被系统将我们的应用进行回收
* @paramlevel 内存相关事件
*/
overridefunonTrimMemory(level: Int){
// 根据不同的应用生命周期和系统事件进行不同的操作
when(level) {
// 应用界面处于后台
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
// 可以在这里释放 UI 对象
}
// 应用正常运行中,不会被杀掉,但是系统内存已经有点低了
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
// 应用正常运行中,不会被杀掉,但是系统内存已经非常低了,
// 这时候应该释放一些不必要的资源以提升系统性能
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
// 应用正常运行,但是系统内存非常紧张,
// 系统已经开始根据 LRU 缓存杀掉了大部分缓存的进程
// 这时候我们要释放所有不必要的资源,不然系统可能会继续杀掉所有缓存中的进程
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
// 释放资源
}
// 系统内存很低,系统准备开始根据 LRU 缓存清理进程,
// 这时我们的程序在 LRU 缓存列表的最近位置,不太可能被清理掉,
// 但是也要去释放一些比较容易恢复的资源,让系统内存变得充足
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
// 系统内存很低,并且我们的应用处于 LRU 列表的中间位置,
// 这时候如果还不释放一些不必要资源,那么我们的应用可能会被系统干掉
ComponentCallbacks2.TRIM_MEMORY_MODERATE,
// 系统内存非常低,并且我们的应用处于 LRU 列表的最边缘位置,
// 系统会有限考虑干掉我们的应用,如果想活下来,就要把所有能释放的资源都释放了
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
/*
* 把所有能释放的资源都释放了
*/
}
// 应用从系统接收到一个无法识别的内存等级值,
// 跟一般的低内存消息提醒一样对待这个事件
else-> {
// 释放所有不重要的数据结构。
}
}
}
}
12.2 ActivityManager.getMemoryInfo
Android 提供了一个 ActivityManager.getMemoryInfo 方法给我们查询内存信息,这个方法会返回一个 ActivityManager.MemoryInfo 对象,这个对象包含了系统当前内存状态,这些状态信息包括可用内存、总内存以及低杀内存阈值。
MemoryInfo 中包含了一个 lowMemory 布尔值,这个布尔值用于表明系统是否处于低内存状态。
fundoSomethingMemoryIntensive{
// 在做一些需要很多内存的任务前,
// 检查设备是否处于低内存状态、
if(!getAvailableMemory.lowMemory) {
// 做需要很多内存的任务
}
}
// 获取 MemoryInfo 对象
privatefungetAvailableMemory: ActivityManager.MemoryInfo {
valactivityManager = getSystemService(Context.ACTIVITY_SERVICE) asActivityManager
returnActivityManager.MemoryInfo.also { memoryInfo ->
activityManager.getMemoryInfo(memoryInfo)
}
}
13. 还有哪些内存优化技巧?13.1 使用更高效的代码结构13.1.1 谨慎使用 Service
(下面这些内容是我在 Andorid 官网上翻译的,从我们的应用角度来说,当然希望是应用一直运行,这样用户每次打开都不用重新走各种初始化流程,但是对于系统来说,我们的这种行为伤害挺大的。)
让一个没用的 Service 在后台运行对于一个应用的内存管理来说是一件最糟糕的事情。
要在 Service 的任务完成后停止它,不然 Service 占用的这块内存会泄漏。
当你的应用中运行着一个 Service,除非系统内存不足,否则它不会被干掉。
这就导致对于系统来说 Service 的运行成本很高,因为 Service 占用的内存其他的进程是不能使用的。
Android 有一个缓存进程列表,当可用内存减少时,这个列表也会随之缩小,这就会导致应用间的切换变得很慢。
如果我们是用 Service 监听一些系统广播,可以考虑使用 JobScheduler。
如果你真的要用 Service,可以考虑使用 IntentService,IntentService 是 Service 的一个子类,在它的内部有一个工作线程来处理耗时任务,当任务执行完后,IntentService 就会自动停止。
13.1.2 选择优化后的数据容器
Java 提供的部分数据容器并不适合 Android,比如 HashMap,HashMap 需要中存储每一个键值对都需要一个额外的 Entry 对象。
Android 提供了几个优化后的数据容器,包括 SparseArray、SparseBooleanArray 以及 LongSparseArray。
SparseArray 之所以更高效,是因为它的设计是只能使用整型作为 key,这样就避免了自动装箱的开销。
13.1.3 小心代码抽象
抽象可以优化代码的灵活性和可维护性,但是抽象也会带来其他成本。
抽象会导致更多的代码需要被执行,也就是需要更多的时间和把更多的代码映射到内存中。
如果某段抽象代码带来的好处不大,比如一个地方可以直接实现而不需要用到接口的,那就不用接口。
13.1.4 使用 protobuf 作为序列化数据
Protocol buffers 是 google 设计的,它可以对结构化的数据序列化,与 XML 类似,不过比 XML 更小,更快,而且更简单。
如果你决定使用 protobuf 作为序列化数据格式,那在客户端代码中应该使用轻量级的 protobuf。
因为一般的 protobuf 会生成冗长的代码,这样会导致内存增加、APK 大小增加,执行速度变慢等问题。
更多关于 protobuf 的信息可以查看 protobuf readme 中的 “轻量级版本” 。
13.2 删除内存消耗大的资源和第三方库
有些资源和第三方库会在我们不知情的情况下大量消耗内存。
APK 大小,第三方库和嵌入式资源,会影响我们应用的内存消耗,我们可以通过删除冗余和不必要的资源和第三方库来减少应用的内存消耗。
13.2.1 Apk 瘦身
Bitmap 大小、资源、动画以及第三方库会影响到 APK 的大小,Android Studio 提供了 R8 和 ProGuard 帮助我们缩小 Apk,去掉不必要的资源。
如果你使用的 Android Studio 版本是 3.3 以下的,可以使用 ProGuard,3.3 及以上版本的可以使用 R8。
13.2.2 使用 Dagger2 进行依赖注入
依赖注入框架不仅可以简化我们的代码,而且能让我们在测试代码的时候更方便。
如果我们想在应用中使用依赖注入,可以考虑使用 Dagger2。
Dagger2 是在编译期生成代码,而不是用反射实现的,这样就避免了反射带来的内存开销,而是在编译期生成代码,
13.2.3 谨慎使用第三方库
当你决定使用一个不是为移动平台设计的第三方库时,你需要对它进行优化,让它能更好地在移动设备上运行。
这些第三方库包括日志、分析、图片加载、缓存以及其他框架,都有可能带来性能问题。