内存优化与启动优化深入详解
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. 内存管理基础
1.1 Android 进程内存结构
进程内存
├── Java Heap(Dalvik/ART 管理)
│ ├── 对象实例
│ ├── 数组
│ └── 类信息
├── Native Heap(malloc/free)
│ ├── Bitmap 像素数据(Android 8.0+)
│ ├── so 库分配的内存
│ └── 硬件缓冲区(HardwareBuffer)
├── Code(代码段)
│ ├── dex/oat 文件映射
│ └── so 库代码
├── Stack(线程栈)
│ └── 每个线程默认 1MB
├── Graphics(GPU 内存)
│ ├── Surface 缓冲区
│ └── 纹理
└── mmap(内存映射文件)
├── APK 资源
└── 字体文件1.2 Java Heap 限制
kotlin
// 获取堆内存限制
val maxMemory = Runtime.getRuntime().maxMemory() // 通常 256MB-512MB
val totalMemory = Runtime.getRuntime().totalMemory() // 当前已分配
val freeMemory = Runtime.getRuntime().freeMemory() // 当前空闲
// ActivityManager 获取
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.memoryClass // 普通应用堆上限(MB),通常 256
am.largeMemoryClass // largeHeap 应用堆上限(MB),通常 5121.3 Bitmap 内存变迁
| Android 版本 | Bitmap 像素存储位置 | 回收方式 |
|---|---|---|
| 2.x | Native Heap | 必须手动 recycle() |
| 3.0 - 7.x | Java Heap | GC 自动回收 |
| 8.0+ | Native Heap(NativeAllocationRegistry) | GC 自动回收 |
Android 8.0+ 将 Bitmap 像素数据移回 Native Heap,但通过 NativeAllocationRegistry 注册到 Java GC,GC 回收 Bitmap 对象时自动释放 Native 内存。好处是不占用 Java Heap 配额。
2. 内存泄漏深入
2.1 LeakCanary 原理详解
Activity.onDestroy()
→ ActivityLifecycleCallbacks 监听到
→ 创建 KeyedWeakReference(activity, key, referenceQueue)
→ 等待 5 秒
→ 检查 referenceQueue
→ WeakReference 入队了 → 对象已被回收,正常
→ WeakReference 没入队 → 对象还活着,可能泄漏
→ 手动触发 GC(Runtime.getRuntime().gc())
→ 再等 100ms
→ 再次检查 referenceQueue
→ 入队了 → 正常(GC 延迟回收)
→ 还没入队 → 确认泄漏!
→ Debug.dumpHprofData() 导出堆快照
→ Shark 库分析 hprof 文件
→ 从 GC Root 到泄漏对象的最短引用链
→ 展示通知核心源码:
kotlin
// ObjectWatcher.kt
fun expectWeaklyReachable(watchedObject: Any, description: String) {
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
// 创建弱引用,关联 ReferenceQueue
val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
watchedObjects[key] = reference
// 5 秒后检查
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
private fun moveToRetained(key: String) {
// 先清理已入队的引用
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
// 还在 watchedObjects 中,说明没被回收
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
// 引用入队了,说明对象被回收了,从监控列表移除
watchedObjects.remove(ref.key)
}
} while (ref != null)
}2.2 常见泄漏场景源码级分析
Handler 泄漏:
java
// ❌ 非静态内部类持有外部 Activity 引用
public class MainActivity extends Activity {
private Handler handler = new Handler() { // 匿名内部类持有 MainActivity.this
@Override
public void handleMessage(Message msg) {
// 处理消息
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
handler.postDelayed(new Runnable() { // Runnable 也持有 MainActivity.this
@Override
public void run() { /* ... */ }
}, 60_000); // 60 秒后执行
}
}
// 泄漏链:
// MessageQueue → Message → Handler(内部类)→ MainActivity
// 60 秒内 Activity 无法被回收java
// ✅ 正确做法
public class MainActivity extends Activity {
// 静态内部类不持有外部引用
private static class SafeHandler extends Handler {
private final WeakReference<MainActivity> activityRef;
SafeHandler(MainActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityRef.get();
if (activity != null && !activity.isFinishing()) {
// 安全处理
}
}
}
private final SafeHandler handler = new SafeHandler(this);
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null); // 清除所有消息
}
}单例持有 Context 泄漏:
kotlin
// ❌ 单例持有 Activity Context
object ImageLoader {
private lateinit var context: Context
fun init(context: Context) {
this.context = context // 如果传入 Activity,Activity 永远无法回收
}
}
// ✅ 使用 Application Context
object ImageLoader {
private lateinit var context: Context
fun init(context: Context) {
this.context = context.applicationContext // Application 生命周期与进程一致
}
}匿名内部类/Lambda 泄漏:
kotlin
// ❌ 注册监听后忘记注销
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 传感器监听器持有 Activity 引用
sensorManager.registerListener(object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
updateUI(event) // 引用了 Activity 的方法
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
}, sensor, SensorManager.SENSOR_DELAY_NORMAL)
}
// 忘记在 onDestroy 中 unregisterListener → 泄漏
}2.3 Bitmap 优化详解
kotlin
// 1. 采样加载:先获取图片尺寸,再按需采样
fun decodeSampledBitmap(res: Resources, resId: Int, reqWidth: Int, reqHeight: Int): Bitmap {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true // 只读取尺寸,不分配内存
}
BitmapFactory.decodeResource(res, resId, options)
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(res, resId, options)
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (height, width) = options.outHeight to options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// inSampleSize 必须是 2 的幂
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
// 2. inBitmap 复用
val reusableBitmap: Bitmap = ... // 之前用过的 Bitmap
val options = BitmapFactory.Options().apply {
inMutable = true
inBitmap = reusableBitmap // 复用内存块,避免重新分配
// Android 4.4+ inBitmap 只需要 >= 目标大小即可
// Android 4.4 以下需要完全相同的尺寸和配置
}
// 3. 像素格式选择
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565 // 2 字节/像素,无透明通道
// 默认 ARGB_8888 是 4 字节/像素
// 一张 1920x1080 的图:
// ARGB_8888: 1920 * 1080 * 4 = 8.3MB
// RGB_565: 1920 * 1080 * 2 = 4.1MB
}2.4 OOM 排查流程
1. 获取 hprof 文件
- Android Studio Profiler → Dump Java Heap
- 代码中:Debug.dumpHprofData(filePath)
- LeakCanary 自动 dump
2. 分析 hprof(使用 MAT 或 Android Studio)
- Dominator Tree:按 Retained Size 排序,找到占内存最大的对象
- Histogram:按类统计对象数量和大小
- GC Root 引用链:找到为什么对象无法被回收
3. 常见 OOM 原因
- Bitmap 过大:加载原图到内存
- 内存泄漏积累:Activity/Fragment 泄漏
- 大量对象创建:内存抖动导致碎片化
- Native 内存泄漏:JNI 层 malloc 未 free
- WebView 内存:WebView 本身占用大量内存
4. 线上 OOM 监控
- 注册 Thread.setDefaultUncaughtExceptionHandler 捕获 OOM
- 定期检查内存使用率,超过阈值时 dump hprof 上报
- 使用 KOOM(快手开源)自动检测和分析3. 启动优化实战
3.1 冷启动全链路
用户点击图标
→ Launcher 调用 startActivity(Binder → AMS)
→ AMS 检查进程是否存在
→ 不存在 → Zygote fork 新进程(~200ms)
→ ActivityThread.main()
→ Looper.prepareMainLooper()
→ ActivityThread.attach()(Binder → AMS 注册)
→ AMS 回调 bindApplication
→ Application.attachBaseContext()
→ ContentProvider.onCreate()(所有 Provider!)
→ Application.onCreate()
→ AMS 回调 scheduleLaunchActivity
→ Activity.onCreate()
→ setContentView() → inflate XML → 创建 View 树
→ Activity.onStart()
→ Activity.onResume()
→ ViewRootImpl.performTraversals()
→ measure → layout → draw
→ 首帧渲染完成(TTID)
→ 用户看到内容(TTFD)3.2 各阶段耗时分析
典型冷启动耗时分布(总计 ~1500ms):
├── Zygote fork: ~200ms(系统层面,无法优化)
├── Application 初始化: ~400ms(重点优化)
│ ├── ContentProvider: ~150ms(合并 Provider)
│ ├── SDK 初始化: ~200ms(异步/延迟)
│ └── 其他: ~50ms
├── Activity 创建: ~300ms(重点优化)
│ ├── inflate 布局: ~150ms(减少层级/异步 inflate)
│ ├── 数据加载: ~100ms(预加载/缓存)
│ └── 其他: ~50ms
└── 首帧绘制: ~100ms3.3 任务编排(DAG)
kotlin
// 定义初始化任务
abstract class InitTask {
abstract fun run()
open fun dependencies(): List<Class<out InitTask>> = emptyList()
open fun runOnMainThread(): Boolean = false
}
// 具体任务
class NetworkInitTask : InitTask() {
override fun run() {
OkHttpClient.Builder().build() // 初始化网络库
}
// 无依赖,可以并行
}
class ImageLoaderInitTask : InitTask() {
override fun run() {
Glide.init(app, config)
}
override fun dependencies() = listOf(NetworkInitTask::class.java) // 依赖网络初始化
}
class PushInitTask : InitTask() {
override fun run() {
PushManager.init(app)
}
override fun runOnMainThread() = true // 必须在主线程
}
// DAG 调度器
class TaskScheduler(private val tasks: List<InitTask>) {
fun start() {
// 1. 拓扑排序
val sorted = topologicalSort(tasks)
// 2. 按依赖关系分层
// 第一层:无依赖的任务(并行执行)
// 第二层:依赖第一层的任务(第一层完成后执行)
// ...
// 3. 使用 CountDownLatch 等待依赖完成
val latch = CountDownLatch(mainThreadTasks.size)
for (task in sorted) {
if (task.runOnMainThread()) {
mainHandler.post {
task.run()
latch.countDown()
}
} else {
executor.execute {
// 等待依赖任务完成
task.dependencies().forEach { dep ->
dependencyLatches[dep]?.await()
}
task.run()
taskLatches[task::class.java]?.countDown()
}
}
}
// 4. 等待所有主线程任务完成(保证 Activity 启动前必要的初始化已完成)
latch.await()
}
}3.4 延迟初始化策略
kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 第一优先级:必须同步初始化(影响首屏)
CrashReporter.init(this)
// 第二优先级:异步初始化(不影响首屏)
thread {
NetworkClient.init(this)
ImageLoader.init(this)
Database.init(this)
}
// 第三优先级:延迟到首帧后
Handler(Looper.getMainLooper()).post {
// 首帧绘制完成后执行
Analytics.init(this)
PushService.init(this)
}
// 第四优先级:延迟到空闲时
Looper.myQueue().addIdleHandler {
AdSDK.init(this)
false // 返回 false,只执行一次
}
}
}3.5 ContentProvider 优化
kotlin
// 问题:每个 ContentProvider 都在 Application.onCreate 之前初始化
// 很多第三方 SDK 用 ContentProvider 做自动初始化(如 Firebase、WorkManager)
// 解决:使用 App Startup 合并多个 Provider 为一个
class MyInitializer : Initializer<Unit> {
override fun create(context: Context) {
// 在这里初始化
WorkManager.initialize(context, config)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
// AndroidManifest.xml
// 移除第三方 SDK 的 ContentProvider
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" /> <!-- 移除自动初始化 -->
// 添加 App Startup 的统一 Provider
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup">
<meta-data
android:name="com.example.MyInitializer"
android:value="androidx.startup" />
</provider>3.6 启动监控指标
kotlin
// TTID(Time To Initial Display):首帧显示时间
// 方法1:adb
// adb shell am start -W com.example/.MainActivity
// TotalTime 就是 TTID
// 方法2:代码埋点
class MyApplication : Application() {
companion object {
val startTime = SystemClock.elapsedRealtime() // 进程创建时间
}
}
class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// 首帧回调
window.decorView.post {
val ttid = SystemClock.elapsedRealtime() - MyApplication.startTime
Log.d("Startup", "TTID: ${ttid}ms")
}
}
}
// TTFD(Time To Full Display):完整内容显示时间
class MainActivity : AppCompatActivity() {
fun onDataLoaded() {
// 数据加载完成,内容展示后
reportFullyDrawn() // 系统 API,会在 Logcat 输出 Fully drawn 时间
}
}3.7 Systrace / Perfetto 使用
bash
# 抓取 5 秒的 trace
python systrace.py -t 5 -o trace.html \
sched freq idle am wm gfx view binder_driver hal dalvik input res
# 或使用 Perfetto(推荐,更强大)
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace \
<<EOF
buffers: { size_kb: 63488 fill_policy: RING_BUFFER }
data_sources: { config { name: "linux.ftrace" ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "power/suspend_resume"
atrace_categories: "am" atrace_categories: "wm"
atrace_categories: "view" atrace_categories: "gfx"
atrace_apps: "com.example.myapp"
}}}
duration_ms: 5000
EOF在 trace 中关注:
bindApplication:Application 初始化耗时activityStart:Activity 启动耗时inflate:布局加载耗时measure/layout/draw:首帧绘制耗时- 主线程上的长条方法:耗时操作
