贝博恩创新科技网

Android内存优化核心技巧有哪些?

Android 内存管理终极指南

目录

  1. 第一部分:基础概念

    Android内存优化核心技巧有哪些?-图1
    (图片来源网络,侵删)
    • 1 为什么内存管理如此重要?
    • 2 Android 内存模型概览
    • 3 关键术语:堆、栈、内存分配
    • 4 Java/Kotlin 对象生命周期
  2. 第二部分:核心机制

    • 1 垃圾回收
      • 1.1 GC 是什么?
      • 1.2 GC 算法(了解即可)
      • 1.3 GC 对性能的影响
    • 2 内存限制与进程生命周期
      • 2.1 Android 的内存分配机制
      • 2.2 LRU (Least Recently Used) 缓存策略
      • 2.3 onTrimMemory() 回调
  3. 第三部分:内存泄漏

    • 1 什么是内存泄漏?
    • 2 常见的内存泄漏场景及解决方案
      • 2.1 静态 Context/View 引用
      • 2.2 静态集合类
      • 2.3 匿名内部类/非静态内部类
      • 2.4 未取消注册的监听器
      • 2.5 未关闭的资源
      • 2.6 单例模式持有 Activity/View 上下文
    • 3 如何检测内存泄漏
      • 3.1 Android Studio Profiler
      • 3.2 LeakCanary (强烈推荐)
  4. 第四部分:性能优化最佳实践

    • 1 优化对象创建
      • 1.1 使用对象池
      • 1.2 避免在循环中创建对象
    • 2 优化数据结构
    • 3 优化图片加载
      • 3.1 使用 inSampleSize 进行采样
      • 3.2 使用 WebP 格式
      • 3.3 使用成熟的图片加载库 (Glide, Picasso)
    • 4 使用原生代码
    • 5 使用 ProGuard/R8 优化代码
  5. 第五部分:高级工具与监控

    Android内存优化核心技巧有哪些?-图2
    (图片来源网络,侵删)
    • 1 Android Studio Profiler 深度解析
    • 2 ADB 命令行工具
      • adb shell dumpsys meminfo
      • adb shell procrank
      • adb shell top

第一部分:基础概念

1 为什么内存管理如此重要?

  • 避免 ANR (Application Not Responding):当应用消耗过多内存,导致系统频繁进行垃圾回收时,UI 线程可能会被阻塞,造成应用卡顿甚至无响应。
  • 防止 OOM (OutOfMemoryError):当应用尝试分配超过可用内存的块时,会抛出 OutOfMemoryError,导致应用崩溃。
  • 提升用户体验:流畅、不卡顿的应用是优秀用户体验的基础,内存优化直接关系到应用的响应速度和稳定性。
  • 延长电池续航:内存管理不当会增加 CPU 的负担,从而消耗更多电量。

2 Android 内存模型概览

Android 应用运行在 Linux 内核之上,每个应用都运行在自己的进程中,每个进程拥有自己独立的虚拟地址空间,这个空间被划分为几个关键区域:

  • 代码段:存放应用的代码。
  • 数据段:存放静态变量和全局变量。
  • 动态内存分配的区域,所有通过 new (Java) 或 val/var (Kotlin) 创建的对象实例都存放在这里,这是我们关注的重点。
  • :存放局部变量方法调用信息,每个线程都有自己的栈,生命周期与方法调用一致,速度快,空间小。
  • 其他:如内存映射文件、共享库等。

3 关键术语:堆、栈、内存分配

  • :大小不固定,生命周期不确定,GC 的主要工作区域,所有对象都从这里分配。
  • :大小固定(由线程栈大小决定),生命周期确定(方法出栈时销毁),基本数据类型和对象引用存放在这里。
  • 内存分配:创建对象时,JVM/Kotlin Runtime 会在堆上寻找足够的连续空间来存放对象数据,并将对象的引用存放在栈的局部变量表中。

4 Java/Kotlin 对象生命周期

  1. 创建:通过 new 或构造函数创建,内存分配在堆上。
  2. 引用:栈中的引用变量指向堆中的对象。
  3. 使用:通过引用变量访问对象的成员和方法。
  4. 不可达:当没有任何引用指向该对象时,它就变成了“垃圾”。
  5. 回收:GC 在未来的某个时刻发现并回收这些垃圾对象,释放其占用的内存。

第二部分:核心机制

1 垃圾回收

1.1 GC 是什么?

垃圾回收是自动内存管理机制,GC 会自动扫描堆,找出不再被任何引用指向的对象(即“垃圾”),并回收它们占用的内存,以便新的对象可以使用这部分空间,开发者无需手动释放内存(如 C/C++ 中的 free/delete),大大减少了内存泄漏的风险。

1.2 GC 算法(了解即可)
  • 标记-清除:标记所有存活对象,清除未被标记的对象,缺点是会产生内存碎片。
  • 复制算法:将内存分为两块,每次只使用其中一块,当一块用满时,将存活对象复制到另一块,然后清空原块,没有碎片,但空间利用率低。
  • 标记-整理:标记存活对象,然后将所有存活对象移动到内存的一端,直接清理端边界以外的内存,结合了前两者的优点。
  • 分代回收:现代 JVM/Kotlin Runtime 采用的策略,它将堆分为新生代老年代
    • 新生代:存放新创建的对象,GC 频繁,速度快(使用复制算法)。
    • 老年代:存放生命周期长的对象,GC 不频繁,速度慢(使用标记-整理或标记-清除算法)。
1.3 GC 对性能的影响

GC 是一件昂贵的操作,它会暂停应用的所有线程(Stop-The-World, STW),在暂停期间,应用无法响应用户输入,GC 频繁发生或单次暂停时间过长,就会导致卡顿,优化的目标就是减少 GC 次数和缩短 GC 暂停时间

2 内存限制与进程生命周期

2.1 Android 的内存分配机制

Android 系统不会为每个进程分配固定的内存,它使用一种软限制硬限制的机制。

Android内存优化核心技巧有哪些?-图3
(图片来源网络,侵删)
  • 软限制:系统期望进程使用的内存,如果进程超过软限制,系统会尝试回收内存(如杀死后台进程)。
  • 硬限制:进程可以使用的绝对最大值,如果进程尝试超过硬限制,系统会直接杀死它并抛出 OOM 错误。

这些限制取决于设备的物理内存、屏幕大小、Android 版本以及应用在 AndroidManifest.xml 中声明的 <uses-permission android:name="android.permission largeHeap" />largeHeap 只是一个请求,系统不一定会满足,并且滥用它可能导致系统不稳定。

2.2 LRU (Least Recently Used) 缓存策略

当系统内存不足时,它会根据 LRU 策法来决定杀死哪些后台进程,一个进程的“最近使用时间”由它包含的组件(Activity, Service, BroadcastReceiver, ContentProvider)的活跃程度决定。

  • 前台进程:正在与用户交互的进程,不会被杀死。
  • 可见进程:UI 可见但没有焦点的进程(如对话框),只有在极端情况下才会被杀死。
  • 服务进程:正在运行服务的进程。
  • 缓存进程:没有运行任何组件,但界面仍然对用户可见的进程,LRU 策法主要作用于这类进程,最久未使用的会被最先杀死。
2.3 onTrimMemory() 回调

当系统内存紧张时,会回调应用的 ComponentCallbacks 中的 onTrimMemory(level) 方法,我们应该重写这个方法来释放不必要的资源。

  • TRIM_MEMORY_RUNNING_MODERATE: 系统内存紧张,进程在 LRU 列表靠前,但仍有压力。
  • TRIM_MEMORY_RUNNING_LOW: 系统内存非常紧张,进程在 LRU 列表靠后。
  • TRIM_MEMORY_UI_HIDDEN: Activity 的 onStop() 被调用。这是释放与 UI 相关资源的最佳时机
  • TRIM_MEMORY_BACKGROUND: 进程即将被杀死,是 LRU 列表的最后一个。
  • TRIM_MEMORY_COMPLETE: 进程即将被杀死。

示例:

class MyApplication : Application(), ComponentCallbacks2 {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            TRIM_MEMORY_UI_HIDDEN -> {
                // Activity 不再可见,释放图片缓存等大内存资源
                imageLoader.clearCache()
            }
            TRIM_MEMORY_BACKGROUND -> {
                // 进程在后台,释放更多资源
                bitmapCache.evictAll()
            }
        }
    }
}

第三部分:内存泄漏

1 什么是内存泄漏?

内存泄漏是指程序中已不再使用的对象,由于仍然被其他活动对象所引用,导致 GC 无法回收它们,随着泄漏的累积,可用内存越来越少,最终导致 OOM 崩溃。

2 常见的内存泄漏场景及解决方案

2.1 静态 Context/View 引用

场景:将 ActivityView 的引用赋给一个静态变量,静态变量的生命周期与应用进程一样长,这会导致 Activity 无法被回收。

// 错误示例
object Holder {
    var context: Context? = null
}
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Holder.context = this // 泄漏了 MyActivity
    }
}

解决方案:如果必须使用 Context,优先使用 Application Context,它的生命周期与进程相同,且不持有任何 UI 组件的引用。

// 正确示例
object Holder {
    var context: Context? = null
}
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Holder.context = applicationContext // 使用 Application Context
    }
}
2.2 静态集合类

场景:静态集合(如 HashMap, ArrayList)会一直持有其添加的元素,如果元素是对象,这些对象就不会被回收。

// 错误示例
object Cache {
    val data = ArrayList<String>()
}
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        for (i in 0..1000) {
            Cache.data.add("some data $i") // 持有大量数据
        }
    }
}

解决方案:在使用完集合后,将其清空,或者在不再需要时,将静态引用置为 null

// 正确示例
Cache.data.clear()
2.3 匿名内部类/非静态内部类

场景:匿名内部类(如 OnClickListener, Runnable, AsyncTask)和非静态内部类会隐式持有其外部类的引用,如果这个外部类是 Activity,而 Runnable 又被一个静态的 Handler 持有,就会导致 Activity 泄漏。

// 错误示例
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handler.post(object : Runnable { // 匿名 Runnable 持有 MyActivity 的引用
            override fun run() {
                // ...
            }
        })
    }
}

解决方案

  1. 将内部类改为静态内部类,这样它就不会持有外部类的引用。
  2. 如果必须在内部类中访问外部类成员,使用 WeakReference
// 正确示例:静态内部类
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handler.post(MyRunnable(this))
    }
    private static class MyRunnable implements Runnable {
        private WeakReference<MyActivity> activityRef;
        MyRunnable(MyActivity activity) {
            this.activityRef = new WeakReference<>(activity);
        }
        @Override
        public void run() {
            MyActivity activity = activityRef.get();
            if (activity != null) {
                // 安全地访问 activity
            }
        }
    }
}
2.4 未取消注册的监听器

场景:为 BroadcastReceiver, EventBus, RxJava 订阅等注册了监听器,但在 Activity 销毁时没有取消注册。

解决方案:在 ActivityonDestroy()onStop() 中取消所有注册。

class MyActivity : AppCompatActivity() {
    private lateinit var receiver: BroadcastReceiver
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        receiver = MyReceiver()
        registerReceiver(receiver, IntentFilter("MY_ACTION"))
    }
    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(receiver) // 必须取消注册
    }
}
2.5 未关闭的资源

场景Cursor, FileInputStream, Bitmap 等资源需要手动关闭,如果持有它们的对象没有被回收,资源也会一直被占用。

解决方案:使用 try-finallytry-with-resources (Kotlin 的 use 函数) 来确保资源被关闭。

// Kotlin use 函数 (推荐)
val inputStream: InputStream = ...
inputStream.use { stream ->
    // 使用 stream
} // stream 在这里会被自动关闭
2.6 单例模式持有 Activity/View 上下文

场景:单例的生命周期是整个应用进程,如果单例的构造函数或方法中传入了 ActivityContext,那么这个 Activity 就永远不会被销毁。

解决方案:与 3.2.1 相同,单例中如果需要 Context,务必使用 Application Context

3 如何检测内存泄漏

3.1 Android Studio Profiler
  1. 打开 Profiler (View > Tool Windows > Profiler)。
  2. 选择你的设备和应用。
  3. 点击 MEMORY 标签页。
  4. 强制垃圾回收:点击工具栏的垃圾桶图标。
  5. 记录 Heap:点击红色的录制按钮。
  6. 操作应用:触发你认为可能泄漏的场景(进入一个页面,然后按返回键退出)。
  7. 停止录制
  8. 分析:在时间轴上找到你退出页面的时间点,然后查看 Heap 的变化,如果内存没有显著下降,或者下降后又迅速回升,可能存在泄漏。
  9. 查看对象:在 Captures 视图中,选择一个快照,使用 Class 视图查看哪些类的实例数量异常多,特别是查看 Activity 实例,如果退出后数量没有减少,就是泄漏。
3.2 LeakCanary (强烈推荐)

Square 公司开源的内存泄漏检测库,使用极其简单。

  1. build.gradle 中添加依赖:
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9'
  2. Application 类中初始化:
    class MyApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            if (LeakCanary.isInAnalyzerProcess(this)) {
                // 这个进程是LeakCanary的分析进程,不做初始化
                return
            }
            LeakCanary.install(this)
        }
    }
  3. 使用:在你的 ActivityFragment 中,只需调用 RefWatcher 监控即可。
    class MyActivity : AppCompatActivity() {
        override fun onDestroy() {
            super.onDestroy()
            // (MyApplication.leakWatcher ?: RefWatcher.DISABLED).watch(this)
            // 更推荐的方式是使用自动监控
        }
    }

    LeakCanary 会自动监控 ActivityFragment,当 Activity 被销毁后,它会在后台线程检查该 Activity 是否被回收,如果没有,它会在通知栏显示一个警告,并提供一个 Hprof 文件供你分析。


第四部分:性能优化最佳实践

1 优化对象创建

  • 避免在循环中创建对象:在 forwhile 循环中创建对象会频繁触发 GC。

    // 差
    for (i in 0..1000) {
        val str = "String " + i // 每次循环都创建新对象
    }
    // 好
    val sb = StringBuilder()
    for (i in 0..1000) {
        sb.append("String ").append(i)
    }
    val result = sb.toString()
  • 使用对象池:对于创建和销毁成本高的对象(如 Bitmap, Drawables),可以使用对象池来复用它们,减少 GC 压力。

2 优化数据结构

  • 根据场景选择合适的数据结构,如果需要频繁的随机访问,ArrayListLinkedList 快得多,如果需要在中间插入/删除元素,LinkedList 更优。
  • 使用 SparseArrayLongSparseArray 代替 HashMap<Integer, Object>HashMap<Long, Object>,可以避免自动装箱,并且性能更好。

3 优化图片加载

图片是应用中最消耗内存的资源之一。

  • 使用 inSampleSize 进行采样:在加载大图时,先将其缩小到合适的尺寸再显示,可以极大地减少内存占用。
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeResource(resources, R.drawable.large_image, options)
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
    options.inJustDecodeBounds = false
    val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image, options)
  • 使用 WebP 格式:WebP 是一种现代的图片格式,在相同质量下,比 PNG 和 JPEG 更小,能显著节省流量和内存。
  • 使用成熟的图片加载库 (Glide, Picasso):这些库已经内置了缓存(内存缓存和磁盘缓存)、采样、线程池等功能,能很好地处理图片加载的各种场景。

4 使用原生代码

对于计算密集型任务,可以使用 C/C++ 或 Kotlin/Native 来实现,原生代码不受 Dalvik/ART 虚拟机的 GC 影响,可以更精细地控制内存。

5 使用 ProGuard/R8 优化代码

ProGuard/R8 不仅能混淆代码,还能移除未使用的代码和资源,减小 APK 体积,间接减少内存占用。


第五部分:高级工具与监控

1 Android Studio Profiler 深度解析

除了内存分析,Profiler 还提供 CPU、网络和电量的实时监控,结合使用可以全面定位性能瓶颈。

  • CPU Profiler:分析方法调用耗时,找出热点代码。
  • Network Profiler:监控网络请求,查看请求大小、耗时和状态码。

2 ADB 命令行工具

  • adb shell dumpsys meminfo <your_package_name>
    • 这是查看应用内存使用情况的“瑞士军刀”。
    • 它会显示应用的内存分配信息,包括 Pss (Proportional Set Size, 实际占用的物理内存)、Shared (与其他进程共享的内存)、Private (独占的内存) 等。
    • 还会列出该进程加载的所有库、 Activities, Services, Providers 等。
  • adb shell procrank
    • 以表格形式显示所有进程的内存占用情况,按总内存使用量排序。
    • VSS (Virtual Set Size), RSS (Resident Set Size), PSS, USS (Unique Set Size)。
  • adb shell top

    实时查看系统进程和 CPU/内存使用情况。


Android 内存管理是一个持续的过程,贯穿于应用的整个开发周期。

  1. 理解原理:深入理解堆、栈、GC 和进程生命周期是优化的基础。
  2. 预防胜于治疗:编写代码时时刻警惕内存泄漏的可能性,遵循最佳实践。
  3. 善用工具:熟练使用 Android Studio Profiler 和 LeakCanary 等工具来检测和分析问题。
  4. 持续监控:在开发、测试和上线后,持续监控应用的内存表现,不断迭代优化。

希望这份教程能帮助你成为一名更优秀的 Android 开发者!

分享:
扫描分享到社交APP
上一篇
下一篇