当前位置: 首页 > news >正文

Android内存调优学习总结(OOM与ANR)

OOM​

面试官: "请谈谈你对 Android 中 OOM 的理解。"

你: "OOM,即 Out of Memory Error,指的是当应用程序向系统申请内存,而系统无法分配足够的可用内存时抛出的错误,这通常会导致应用崩溃。

发生机制: Android 系统为每个应用分配了有限的堆内存(Heap Size)。当应用占用的内存(包括 Java 对象、Bitmap、Native 内存等)持续增长,超出了这个限制,并且垃圾回收器(GC)也无法回收足够的空间时,就会发生 OOM。

常见原因主要有以下几点:

  • 内存泄漏 (Memory Leak): 这是最常见的原因。当不再需要的对象仍然被其他活动对象引用,导致 GC 无法回收它们,内存占用持续累积。例如,静态变量持有 Activity 引用、未及时关闭的资源(如 Cursor、FileStream)、内部类持有外部类引用等。
  • 大量加载或处理大对象: 例如,一次性加载高清大图而未进行压缩或缩放、加载大量数据到内存中而未进行分页或流式处理。
  • 内存抖动 (Memory Churn): 短时间内频繁创建和销毁大量对象,会导致堆内存碎片化,即使总内存未超限,也可能因为找不到连续的内存空间而导致 OOM。同时,频繁 GC 也会影响应用性能。
  • 资源未正确释放: 例如,BroadcastReceiver、Service 等组件在不需要时未及时注销或停止,或者一些需要手动释放的 Native 资源未被释放。
  • 不合理的内存缓存设计: 缓存策略不当,缓存了过多或过大的对象,并且没有有效的淘汰机制。"

面试官: "如果你的应用发生了 OOM,你会如何进行分析和定位?"

你: "当应用发生 OOM 时,我会遵循一套系统性的排查流程:

  1. 复现问题: 首先,我会尝试稳定复现 OOM 的场景,这有助于缩小问题范围。我会关注 OOM 发生时的具体操作路径和用户行为。

  2. 查看 Logcat 日志: OOM 发生时,Logcat 会输出关键的错误信息,包括 java.lang.OutOfMemoryError 以及堆栈跟踪(Stack Trace)。通过堆栈跟踪,可以初步定位到发生 OOM 的代码位置。有时还会提示是哪种类型的内存溢出,比如 Bitmap 相关的或者分配大对象失败。

  3. 使用 Android Studio Profiler:

    • Memory Profiler: 这是定位 OOM 的核心工具。我会使用 Memory Profiler 来监控应用的内存使用情况,包括 Java Heap、Native Heap、Graphics 等。
      • 实时监控: 观察内存分配的趋势,是否有快速增长或持续不降的情况。
      • Heap Dump (.hprof 文件): 在 OOM 发生前或内存占用较高时,我会手动触发或在 OOM 发生时自动生成 Heap Dump 文件。然后使用 Android Studio 自带的分析器或 MAT (Memory Analyzer Tool) 打开 .hprof 文件。
      • 分析 Heap Dump:
        • Shallow Heap 和 Retained Heap: 分析对象的 Shallow Heap(对象自身占用内存)和 Retained Heap(对象被回收后能释放的总内存),找出占用内存最大的对象。
        • Dominator Tree (支配树): 查看哪些对象是内存泄漏的根源。
        • Paths to GC Roots (到 GC Roots 的路径): 分析对象为什么没有被回收,找到持有其引用的路径。
        • 查找重复对象或大对象实例。
    • Allocation Tracking (内存分配跟踪): 可以查看在一段时间内,哪些对象被频繁创建,有助于发现内存抖动问题。
  4. 使用 LeakCanary: 在开发阶段,我会集成 LeakCanary。它能够在发生内存泄漏时自动检测并给出通知,显示泄漏对象的引用链,帮助快速定位泄漏点。

  5. 代码审查 (Code Review): 结合 Profiler 的分析结果,我会重点审查相关模块的代码,特别是涉及静态变量、内部类、生命周期管理、资源关闭、Bitmap 处理等部分。

  6. 灰度测试和用户反馈: 对于难以复现的 OOM,可以通过灰度发布,收集线上用户的 OOM 日志(例如通过 Bugly、Firebase Crashlytics 等 APM 工具)进行分析。

面试官: "有哪些常见的优化 OOM 的策略?"

你: "优化 OOM 主要从以下几个方面入手:

  1. 避免内存泄漏:

    • Context 使用: 谨慎使用 Activity Context,对于生命周期长的对象,尽量使用 ApplicationContext
    • 静态变量: 避免静态变量持有生命周期较短的对象的引用(如 Activity、View)。如果必须使用,务必在合适的时机(如 onDestroy())将其置空。
    • 内部类/匿名内部类: 非静态内部类会隐式持有外部类的引用。如果不需要访问外部类成员,应将其声明为静态内部类。对于需要回调的场景,可以使用弱引用(WeakReference)来持有外部类引用。
    • 资源及时关闭: CursorInputStreamOutputStreamBitmapMediaPlayerFile 等资源在使用完毕后,务必在 finally 块中调用 close()release() 方法。
    • 取消注册: BroadcastReceiverEventBus 订阅、HandlerCallbackMessage 等,在组件销毁时(如 onDestroy(), onDetachedFromWindow())要及时取消注册或移除。
    • 线程管理: 异步任务或线程执行完毕后,要确保能正常结束,并且不再持有 Activity 等组件的引用。
  2. 优化 Bitmap 处理:

    • 按需加载: 不要加载原始尺寸的大图。根据目标 ImageView 的大小,使用 BitmapFactory.OptionsinSampleSize 属性进行采样压缩。
    • 选择合适的图片格式: WebP 格式通常比 JPEG 和 PNG 占用更少的空间,且支持透明度和有损/无损压缩。对于不需要透明度的场景,考虑使用 JPEG。
    • 使用 ARGB_565 配置: 对于不需要 Alpha 通道的图片,可以将 Bitmap.Config 设置为 ARGB_565,它比默认的 ARGB_8888 占用内存少一半。
    • 及时回收: 对于不再使用的 Bitmap,显式调用 bitmap.recycle() 并将其置为 null,尤其是在低版本 Android 系统上。高版本系统虽然对 Bitmap 回收做了优化,但良好习惯依然重要。
    • 使用图片加载库: Glide、Picasso 等成熟的图片加载库内置了完善的内存缓存、磁盘缓存、Bitmap 复用、生命周期管理等机制,能有效避免 OOM。
    • Bitmap 复用: 使用 inBitmap 属性可以复用旧 Bitmap 的内存空间来解码新的 Bitmap,前提是新旧 Bitmap 的尺寸和配置兼容。
  3. 使用优化的数据结构:

    • SparseArray 替代 HashMap: 当 Key 为 Integer 类型时,SparseArraySparseIntArraySparseLongArray 等比 HashMap 更节省内存,因为它们避免了 Key 和 Value 的自动装箱拆箱。
    • ArrayMap 替代 HashMap: 对于小数据量(通常几百以内)的场景,ArrayMapHashMap 更节省内存,因为它内部使用数组存储,查找效率稍低但内存占用更优。
  4. 减少内存抖动:

    • 避免在循环中创建对象: 尤其是在 onDraw()getView() 等频繁调用的方法中,尽量复用对象,避免创建大量临时对象。
    • 对象池技术 (Object Pool): 对于需要频繁创建和销毁的对象(如 Message、自定义 View 的绘制对象),可以使用对象池来复用,减少 GC 压力。
  5. 合理使用内存缓存:

    • LruCache: 基于 LRU (Least Recently Used) 算法实现内存缓存,当缓存达到设定大小时,会自动移除最近最少使用的对象。适合缓存 Bitmap 等占用内存较大的对象。
    • 控制缓存大小: 根据应用的实际内存情况,合理设置缓存的最大值,通常为应用可用内存的 1/8 到 1/4。
  6. 谨慎使用大型第三方库: 评估第三方库的内存占用和潜在泄漏风险。

  7. 针对大内存需求使用独立进程: 如果应用中有某些模块确实需要消耗大量内存(例如图片编辑、视频处理),可以考虑将其放在独立的进程中,避免主进程 OOM 影响核心功能。通过 android:process 属性指定。

场景一:Adj内存管理机制

面试官​:“后台服务保活容易被LMK杀进程,你们是怎么处理的?”
​:“我们项目里对核心服务会通过startForeground()绑定通知栏来提升优先级,比如直播间的推流服务。不过这里有个平衡点——如果过多服务设为前台,用户通知栏会被占满,反而影响体验。所以我们只对音频播放、IM长连接这类关键服务做保活,其他像日志上传这类任务会拆到独立进程,并且监听onTrimMemory()及时释放资源。”

追问​:“独立进程会不会增加内存开销?”
​:“确实会有额外消耗,所以需要评估必要性。比如之前我们有个图片预加载模块放在主进程导致OOM,拆分到子进程后通过Messenger通信,主进程崩溃时子进程还能保留缓存。但像WebView多进程这种场景,就得权衡内存和稳定性了。”


场景二:JVM与GC机制​

面试官​:“G1回收器比CMS强在哪?怎么选型?”
​:“上个月我们刚把线上APK的GC策略从CMS切到G1。最大的改进是STW时间可控了,特别是大内存机型设置MaxGCPauseMillis=50ms后,卡顿率降了15%。不过G1的Region划分需要预热,像冷启动阶段如果瞬间分配大数组,还是可能触发Full GC。所以我们会在初始化时预加载部分内存池。”

追问​:“GC日志里频繁Young GC说明什么?”
​:“这可能是内存抖动的信号。之前我们RecyclerView快速滑动时,每次加载图片都new Bitmap,Young GC每秒触发3次以上。后来改用对象池复用ByteBuffer,GC频率降到0.5次/秒。这时候要看MAT报告的Allocation Stack,找到高频分配点。”


场景三:内存泄漏排查

面试官​:“LeakCanary报单例持有Activity,你们怎么解决的?”
​:“这问题我们踩过坑!比如全局工具类里缓存了Activity的Context,导致旋转屏幕后旧Activity无法回收。后来强制要求所有单例必须用ApplicationContext,并且用WeakReference包装外部引用。但弱引用有个坑——如果异步回调时Activity已经被销毁,需要加空判断防止NPE。”

追问​:“线上OOM怎么定位不是泄漏导致的?”
​:“上周刚处理一个案例:用户上传9张10MB的高清图,即使没有泄漏,Bitmap堆内存直接爆了。我们通过adb shell dumpsys meminfo发现Graphics(显存)和Native堆占用异常,最后改用SubsamplingScaleImageView实现区域加载,内存从120MB降到20MB。”


场景四:Bitmap优化

面试官​:“加载百张2K图不OOM,具体怎么操作?”
​:“这要分四步走:1)先用inSampleSize=2把分辨率降到1/4,内存直接省75%;2)格式切到RGB_565,虽然不支持透明通道,但用户头像场景够用了;3)Android 8.0以上用HARDWARE配置,让Bitmap走Graphic内存而不是Java堆;4)LruCache设动态上限——比如根据Runtime.getRuntime().maxMemory()实时计算,避免固定值在高配机浪费,在低配机又OOM。”

追问​:“inBitmap复用有什么注意点?”
​:“去年我们适配Android 4.4时踩过坑——必须保证新旧Bitmap的像素格式完全一致。比如列表里先加载了一张ARGB_8888的图,后面复用时要先decode成同样格式。现在我们在Glide层统一封装,通过BitmapPool自动管理复用队列,同时加try-catch防止不兼容机型崩溃。”


场景五:架构设计

面试官​:“低内存时怎么优雅降级?”
​:“我们设计了三级降级策略:1)收到TRIM_MEMORY_UI_HIDDEN时,清空非当前页的图片缓存;2)TRIM_MEMORY_COMPLETE时,杀掉所有非核心进程,跳转回首页;3)onLowMemory()时,连核心进程的缓存都释放,只保留用户数据。同时监控MemoryInfo.totalMem动态调整策略——比如6GB以上手机可以保留更多后台缓存。”

追问​:“内存监控体系怎么搭建?”
​:“线下用AS Profiler + MAT分析堆转储,重点看Dominator Tree里的大对象;线上通过埋点上报每次OOM前的内存快照,包括Activity堆栈和Bitmap尺寸。我们还接入了LeakCanary的远程兜底——当连续两次启动发生泄漏时,自动关闭相关模块并弹窗引导用户重启。”


面试官:"如何通过LeakCanary定位Activity泄漏?请描述从集成到分析的完整流程。"

高分回答​:
"在项目中集成LeakCanary时,我们会在Debug环境的build.gradle中添加leakcanary-android依赖

当Activity执行onDestroy()后,LeakCanary通过RefWatcher将其包装成KeyedWeakReference,并关联ReferenceQueue

若两次GC后对象仍未回收,则触发Heap Dump生成.hprof文件

比如我们直播间模块曾因静态Handler导致Activity泄漏,通过LeakCanary的引用链溯源,发现是未及时调用removeCallbacks(),修复后OOM率下降60%

延伸考点​:

  • 追问​:"为什么LeakCanary要在独立进程分析Heap Dump?"
    答:避免主进程内存占用过高导致二次崩溃,同时防止分析过程阻塞UI线程

 ANR

面试官: "请解释一下什么是 Android 中的 ANR,以及它通常是如何发生的?"

你: "ANR,即 Application Not Responding,指的是应用程序的 UI 线程(主线程)在一定时间内未能响应用户输入事件(如点击、触摸)或未能完成关键的系统回调(如 Activity 的生命周期方法、BroadcastReceiver 的 onReceive()),导致系统认为应用卡死,从而向用户显示一个“应用无响应”的对话框。

发生机制: Android 系统通过消息队列来处理 UI 事件和系统消息。所有这些操作都在主线程中执行。如果主线程被某个耗时操作阻塞,例如:

  • 耗时的网络请求或文件 I/O 操作。
  • 复杂的计算或大量数据处理。
  • 主线程与其他线程发生死锁。
  • BroadcastReceiveronReceive() 方法执行时间过长。
  • Service 的关键生命周期方法(如 onCreate(), onStartCommand())执行时间过长。

当这些耗时操作阻塞了主线程,使其无法在规定时间内处理新的消息或完成当前任务,系统就会触发 ANR。

超时的阈值通常是:

  • 前台 Activity: 5 秒内无法响应输入事件。
  • BroadcastReceiver: onReceive() 方法在前台运行时超过 10 秒(后台可能更长,但一般也是这个量级)未执行完毕。
  • Service: 前台 Service 的关键方法在 20 秒内未执行完毕。"

面试官: "如果你的应用发生了 ANR,你会如何分析和定位?"

你: "定位 ANR 问题,我会采取以下步骤:

  1. 复现问题: 尝试稳定复现 ANR 场景,记录操作路径。

  2. 分析 /data/anr/traces.txt 文件: 这是定位 ANR 最核心的文件。

    • 获取文件: 当 ANR 发生时,系统会将当时的线程堆栈信息输出到 /data/anr/traces.txt 文件中。可以通过 ADB (adb pull /data/anr/traces.txt) 将其导出。对于没有 root 权限的设备,某些情况下在 Android Studio 的 Device File Explorer 中也可能找到,或者通过 Bug 报告 (adb bugreport) 间接获取。
    • 分析 traces.txt
      • 主线程堆栈: 首先找到 main 线程(通常是第一个列出的线程或者明确标明 tid=1Cmd line: com.example.app)。仔细分析主线程当前的堆栈信息,看它阻塞在哪个方法调用上。
      • CPU 占用率: traces.txt 文件开头会显示 ANR 发生时 CPU 的负载情况,以及各个进程的 CPU 占用。如果 CPU 负载很高,可能是系统整体繁忙,或者是某些线程占用了过多 CPU 资源。
      • 锁信息: 关注主线程是否在等待某个锁 (waiting to lock <0x لاک_address>held by threadid=X)。如果是,再找到持有该锁的线程(threadid=X),看它在做什么,判断是否发生了死锁或者锁争用导致的长时间等待。
      • 其他线程状态: 观察其他线程的状态,看是否有其他线程影响了主线程的执行,例如后台线程持有主线程需要的资源。
  3. 查看 Logcat 日志: 在 ANR 发生前后,Logcat 中可能会有一些相关的警告或错误信息,例如 Choreographer 的跳帧信息 (Skipped X frames! The application may be doing too much work on its main thread.),或者其他与耗时操作相关的日志。

  4. 使用 Android Studio Profiler:

    • CPU Profiler:
      • Trace System Calls / Sample Java Methods: 可以录制 ANR 发生前后的 CPU 活动。通过分析主线程的火焰图 (Flame Chart) 或调用栈 (Call Chart),可以清晰地看到哪些方法耗时较长,占用了主线程时间。
      • 检查主线程的执行状态: 查看主线程是否长时间处于 Running 状态执行某个方法,或者处于 Blocked 状态等待资源。
    • Debug 模式和断点: 如果大致能定位到可疑代码段,可以使用 Debug 模式,在主线程的关键路径上设置断点,逐步排查是哪个环节耗时过长。

 

场景一:ANR 的根本原因与常见类型

面试官​:
“你之前项目里遇到过 ANR 吗?一般是什么原因导致的?怎么快速判断是哪种类型的 ANR?”

候选人​:
“碰到过几次,印象最深的一次是用户反馈在商品详情页滑动时会弹 ANR 弹窗。我们排查后发现是因为主线程里直接调用了 Glide 加载高清大图,而且图片尺寸没做压缩,导致 UI 线程卡死。”
“常见的 ANR 类型主要是四种:

  1. 输入事件超时​:比如点击按钮后主线程卡住 5 秒没反应,用户就会看到弹窗。
  2. 广播超时​:比如在 BroadcastReceiver 里同步写数据库,前台广播超过 10 秒就崩了。
  3. 服务超时​:后台服务如果没及时完成 onStartCommand,Android 8.0 之后超过 200 秒会触发 ANR。
  4. 内容提供者超时​:ContentProviderquery 方法里做复杂计算,超过 10 秒也会挂。”

“我一般先看 traces.txt 里的 Reason 字段。比如看到 Input dispatching timed out 就知道是用户操作卡死,如果是 Timeout executing service 就是服务没处理好。”


场景二:锁竞争导致的 ANR

面试官​:
“主线程明明没有耗时操作,为什么还会 ANR?遇到过这种反直觉的情况吗?”

候选人​:
“还真遇到过!有一次线上报了个 ANR,主线程堆栈显示在等一个锁,但代码里压根没写 synchronized。后来发现是用了三方库的 SharedPreferences,它内部有个全局锁,我们在主线程调了 commit(),而另一个线程的 apply() 卡在 IO 上,结果主线程等了 5 秒直接 ANR。”
SharedPreferencesEditor 实现类有个 mLock 对象,写操作会加锁。如果子线程的 apply() 正在写磁盘(尤其是低端机),主线程调 commit() 就会阻塞。后来我们全部改用 apply() + 协程 delay 规避,或者换 DataStore。”

面试官追问​:
“那怎么避免这种三方库的坑?”

候选人​:

  1. 源码审查​:遇到性能敏感的三方库,直接看关键方法是否有同步锁。
  2. 线程隔离​:把所有三方库调用封装到子线程,比如用 CoroutineWorker
  3. 监控工具​:线上集成 BlockCanary,捕获锁等待超过 2 秒的 case。”

场景三:系统资源不足导致的 ANR(腾讯/拼多多真题)​

面试官​:
“有没有遇到过 ANR 不是代码问题,而是手机太卡导致的?怎么区分责任?”

候选人​:
“有的!我们有个海外项目,低端机用户经常报 ANR。查日志发现主线程堆栈很正常,但 CPU usage 显示系统级负载 90% 以上。后来发现是竞品 APP 常驻后台疯狂吃 CPU,导致我们的主线程抢不到时间片。”

  1. 看 CPU 使用率​:如果 user + kernel 超过 80%,大概率是系统问题。
  2. 看内存状态​:adb shell dumpsys meminfo 发现 Total PSS 过高,OOM 风险会导致进程频繁挂起。
  3. 看线程数​:有些手机会限制进程最大线程数(比如 500 个),超出后 Thread.start() 直接阻塞主线程。”

“我们做了两件事:

  1. 降级策略​:检测到低内存设备时,关闭动画和预加载功能。
  2. 进程保活​:和手机厂商合作,加入白名单减少被杀概率(不过 Android 11 之后很难了)。”

场景四:线上 ANR 监控与定位

面试官​:
“你们线上怎么监控 ANR?如何快速定位到具体代码?”

候选人​:
“我们用的是 ​Sentry + 自定义埋点​ 的组合:

  1. Sentry 自动捕获​:集成 sentry-android 后,ANR 日志会自动上传,附带设备信息和堆栈。
  2. 关键路径埋点​:在核心流程(比如下单、支付)插入 Trace.beginSection(),ANR 时通过 traces.txt 看卡在哪段代码。
  3. 性能火焰图​:线上抽样用户开启 Debug.startMethodTracing(),复现问题时生成 .trace 文件分析热点函数。”

“比如上周有个 ANR 发生在支付结果页,通过 Sentry 发现堆栈卡在 Gson.fromJson() 解析一个 500KB 的响应数据。优化方案是用 JsonReader 流式解析,主线程耗时从 2 秒降到 200 毫秒。”


场景五:广播导致的 ANR

面试官​:
“广播超时 ANR 怎么处理?onReceive 里能不能启动线程?”

候选人​:
“绝对不能在 onReceive 里直接 new Thread()!我们之前有个广播监听网络变化,在 onReceive 里启线程做数据同步,结果广播结束线程还没跑完,系统直接把进程杀了,导致数据丢失。”
“现在我们的标准做法是:

  1. 快速响应​:onReceive 只做轻量操作,比如更新全局状态变量。
  2. 转交 Service​:用 JobIntentService(兼容 Android 8.0+)处理耗时逻辑,保证即使进程被杀也能重启任务。
  3. 超时控制​:给后台任务加 Timeout 机制,比如用 ThreadPoolExecutorawaitTermination。”

面试官: "当你的应用出现 OOM 的时候,你通常会怎么去定位和分析这个问题呢?"

我: "如果应用出现了 OOM,我首先会尝试稳定复现这个问题的场景,这能帮我缩小排查范围。紧接着,我会第一时间查看 Logcat 日志,因为 OOM 发生时,系统会打印出 java.lang.OutOfMemoryError 的错误信息和关键的堆栈跟踪,这能让我初步判断是哪块代码区域可能出了问题,比如是 Bitmap 过大还是集合类不当使用等等。

但要深入定位,Android Studio 的 Memory Profiler 是我的主要武器。我会用它来实时监控应用的内存使用情况,观察 Java 堆、Native 堆的变化趋势。如果看到内存在某个操作后持续增长且居高不下,或者在GC后也没有明显下降,那很可能就有内存泄漏或者内存管理不当的问题。

最关键的一步是获取和分析 Heap Dump 文件 (.hprof 文件)。我会在内存占用较高或者即将 OOM 的时候,手动触发一次 Heap Dump,或者如果能稳定复现 OOM,也可以设置在 OOM 时自动生成。拿到这个文件后,我会用 Android Studio 自带的分析工具或者 MAT (Memory Analyzer Tool) 来打开它。

在分析 Heap Dump 时,我会重点关注几个方面:

  1. 查找大对象实例: 通过查看对象的 Shallow Heap(对象自身大小)和 Retained Heap(此对象被回收后能释放的总内存),快速定位那些占用内存特别多的“大户”。
  2. 分析支配树 (Dominator Tree): 这能帮我清晰地看到哪些对象是主要的内存持有者,以及它们之间的引用关系,从而找到内存无法被回收的根源。
  3. 检查到 GC Roots 的路径 (Paths to GC Roots): 对于可疑的泄漏对象,我会查看它到 GC Roots 的引用链。这条链会告诉我,究竟是哪个还存活的对象(比如静态变量、活动的线程、或者某些系统服务)持有了这个本该被回收的对象的引用,导致了内存泄漏。

当然,在开发阶段,我也会积极使用像 LeakCanary 这样的工具,它能在发生内存泄漏时主动报警并给出清晰的引用链,帮助我更早地发现和修复问题,而不是等到线上 OOM 爆发。"


面试官: "那如果遇到的是 ANR 问题呢?你的排查思路是怎样的?"

我: "对于 ANR 问题,我的排查思路和 OOM 有些不同,但同样需要系统性地进行。

首先,和 OOM 类似,我也会尝试复现 ANR 发生的场景,理解用户当时的具体操作。

ANR 排查最核心、最有价值的信息来源是 /data/anr/traces.txt 文件。当 ANR 发生时,系统会把所有线程当时的堆栈快照、CPU 使用情况等关键信息都记录在这个文件里。我会通过 ADB 或者 Android Studio 的 Device File Explorer (如果权限允许) 将这个文件导出来分析。

打开 traces.txt 文件后,我会重点关注:

  1. 主线程 (通常是 'main' 线程) 的堆栈信息: 这是判断 ANR 原因的关键。我会仔细看主线程在 ANR 发生时究竟卡在了哪个方法的调用上。是正在进行网络请求?还是在做复杂的文件读写?或者是在等待某个锁?
  2. CPU 负载和各线程状态: 文件开头会显示 ANR 时刻的 CPU 整体负载,以及各个进程、线程的 CPU 占用和状态。如果 CPU 负载很高,可能是后台有其他任务占用了大量资源。
  3. 锁信息: 如果主线程堆栈显示它在 waiting to lock某个锁,我就会去查找是哪个线程 held by threadid=X 持有了这个锁,并分析那个线程当时在做什么,判断是否存在死锁或者某个线程长时间持有锁导致主线程饿死。

除了 traces.txt,我也会结合 Logcat 日志进行分析。在 ANR 发生前后,Logcat 中可能会有一些有用的线索,比如 Choreographer 打印的跳帧信息("Skipped X frames! The application may be doing too much work on its main thread."),或者其他与耗时操作相关的警告。

如果 traces.txt 的信息还不够明确,或者我想更细致地了解主线程的耗时分布,我会使用 Android Studio 的 CPU Profiler。我会选择 "Trace System Calls" 或 "Sample Java Methods" 来录制一段 ANR 发生前后的 CPU 活动。通过分析生成的火焰图 (Flame Chart) 或调用栈 (Call Chart),我可以非常直观地看到主线程中哪些方法调用最为耗时,从而定位到性能瓶颈。

在开发阶段,我也会开启 StrictMode,它可以帮助我检测到主线程中的一些不规范操作,比如磁盘 I/O 或网络访问,提前暴露潜在的 ANR 风险。


 基础知识扩展:

Heap Dump是Java虚拟机(JVM)在某一时刻对内存使用情况的完整快照,主要用于诊断内存泄漏、内存溢出(OOM)等性能问题。以下是结合搜索结果的深度解析:


一、核心概念与内容

  1. 定义
    Heap Dump以二进制文件形式(如.hprof)记录JVM堆内存中的对象信息,包括存活对象实例、类元数据、线程调用栈

    生成时通常伴随Full GC,因此文件内多为有效对象,垃圾对象较少
  2. 关键信息

    • 对象信息​:实例数据、成员变量、引用关系。
    • 类元数据​:类加载器、类名、父类、静态变量。
    • GC Roots​:可达性分析的起点(如线程栈中的局部变量、静态变量)。
    • 线程信息​:转储时刻的线程状态及局部变量

二、生成方式

  1. 自动触发

    • OOM时自动生成​:通过JVM参数-XX:+HeapDumpOnOutOfMemoryError配置,搭配-XX:HeapDumpPath指定路径
    • FullGC前后生成​:使用-XX:+HeapDumpBeforeFullGC-XX:+HeapDumpAfterFullGC
  2. 手动生成

    • 命令行工具​:
      • jmap -dump:format=b,file=heap.hprof <pid>(需进程暂停,影响性能)
      • jcmd <pid> GC.heap_dump filename=heap.hprof(推荐,对性能影响较小)
    • 图形化工具​:JVisualVM、MAT(Memory Analyzer)通过GUI操作生成
  3. 代码触发
    调用HotSpotDiagnosticMXBeandumpHeap方法,适用于需要特定条件触发的场景(如捕获异常时)


ANR 日志查找与分析实战

场景一:ANR 日志哪里找?​

面试官​:
“用户反馈应用卡死弹出了 ANR 弹窗,但测试环境复现不了,你会怎么排查?”

候选人​:
自然回忆
“首先我会让用户导出 /data/anr/traces.txt,这是 ANR 日志的默认存储位置。不过很多用户没 root 权限,这时候我会教他们用 adb bugreport 生成完整报告,里面会包含所有 ANR 信息。”

细节补充
“如果是线上问题,我们会在代码里集成 SentryFirebase Crashlytics,自动捕获 ANR 日志并上传。比如在 Application 类里加个监听:

class MyApp : Application() {override fun onCreate() {super.onCreate()val anrWatchDog = ANRWatchDog().setReportMainThreadOnly()anrWatchDog.start()}
}

“这样只要主线程卡顿超过 5 秒,就能自动触发日志记录,比等用户手动反馈快多了!”


场景二:解读 traces.txt 的核心技巧

面试官​:
“拿到 traces.txt 后,你会先看哪些关键信息?”

候选人​:
“我一般分三步走:

  1. 找 ANR 类型​:看 Reason 行,比如 Input dispatching timed out 是点击事件超时,Broadcast of Intent 是广播超时。
  2. 锁定主线程堆栈​:搜索 "main" prio=5 tid=1,这里显示主线程最后卡在哪个方法。比如:
    at com.example.MainActivity.loadData(MainActivity.kt:30)
    - waiting to lock <0x123> (a java.lang.Object)
    这里明显是主线程在等锁,锁被其他线程占用了。
  3. 查 CPU 和内存状态​:看日志末尾的 CPU usage,如果 total 超过 80%,可能是系统资源不足导致的卡顿。”

举反例
“之前有个案例,主线程堆栈显示在 TextView.setText(),看起来没问题。但继续往下翻发现有个 Binder 调用卡了 8 秒:

Binder:12345_1 (server) sysTid=456 blocking
at android.os.MessageQueue.nativePollOnce(Native method)

最后发现是跨进程调用 ContentProvider 时对方进程阻塞了,这种问题不翻完整日志根本想不到!”


场景三:锁竞争问题的深度分析

面试官​:
“如果日志显示主线程在 waiting to lock,该怎么进一步排查?”

候选人​:
“这时候就是侦探时间了!比如日志里写着:

"main" waiting to lock <0x123> (held by Thread-5)
"Thread-5" holding <0x123> at com.example.DataManager.save()

分步拆解

  1. 定位锁持有者​:全局搜索 0x123,找到 Thread-5 的堆栈。
  2. 分析锁内操作​:发现 DataManager.save() 内部有 Files.copy(),这是个同步的 IO 操作!
  3. 复现路径​:用户在保存数据时快速点击返回键,主线程调 onDestroy() 需要拿同一个锁,结果被 IO 卡住。”

解决方案
“后来我们做了两件事:

  • Files.copy() 改成 NIO 异步通道,锁内代码从 2 秒降到 50 毫秒。
  • tryLock(300ms) 设置超时,避免主线程无限等待。”

场景四:CPU 和内存的关联分析

面试官​:
“ANR 日志里 CPU usage 很高,怎么判断是应用问题还是系统问题?”

候选人​:
“我会重点看两个指标:

  1. 应用 CPU 占比​:如果 myapp_pkgUTIME 超过 30%,说明应用自身有耗 CPU 的操作,比如死循环或频繁 GC。
  2. 系统负载​:如果 TOTALuser + kernel 超过 80%,且 iowait 很高,可能是其他进程在疯狂读写磁盘。”

实战案例
“之前遇到一个 ANR,traces.txt 显示主线程正常,但 CPU usageiowait 占了 60%。用 adb shell dumpsys diskstats 发现微信的 com.tencent.mm 正在备份聊天记录,把磁盘 IO 打满了。最后我们只能在代码里加了个重试机制:

fun saveDataWithRetry() {var retryCount = 0while (retryCount < 3) {try {File(data).writeText(content)break} catch (e: IOException) {Thread.sleep(1000) // 等 IO 高峰过去retryCount++}}
}

场景五:工具链的灵活运用

面试官​:
“除了 traces.txt,还会用哪些工具分析 ANR?”

候选人​:
如数家珍
“我有三把斧头:

  1. Android Studio Profiler​:
    • 开启 Method Tracing,复现 ANR 后看火焰图,哪个方法占用了主线程时间。
    • 如果是 ChoreographerdoFrame 耗时高,说明是 UI 渲染问题,可能用了复杂的 Canvas.drawPath
  2. StrictMode​:
    • 在开发阶段开启 detectDiskReads(),提前捕获主线程 IO。
  3. 自定义监控​:
    • Looper.getMainLooper().setMessageLogging() 监听消息处理耗时:
    Looper.getMainLooper().setMessageLogging { msg ->if (msg.startsWith(">>>>>")) startTime = SystemClock.uptimeMillis()else if (msg.startsWith("<<<<<")) {val cost = SystemClock.uptimeMillis() - startTimeif (cost > 100) log("主线程消息处理耗时 ${cost}ms")}
    }

举一反三
“之前用 MessageLogging 发现有个 Handler 每隔 1 秒在主线程检查网络状态,直接导致低端机 ANR。后来改成 WorkManager 的周期性任务,问题就解决了!”

相关文章:

  • 【Qt开发】显示类控件——QLCDNumber
  • VRRP虚拟路由器协议的基本概述
  • 有两个Python脚本都在虚拟环境下运行,怎么打包成一个系统服务,按照顺序启动?
  • linux ptrace 图文详解(九) gdb如何判断被tracee唤醒的原因
  • 数字计数--数位dp
  • 文章记单词 | 第113篇(六级)
  • 反向海淘物流难题如何破解?
  • 文章记单词 | 第105篇(六级)
  • 动态库和静态库详解
  • 从数学融智学视域系统地理解《道德经》:前三十七章,道法自然
  • C语言中的文件I/O
  • 单目视觉测量及双目视觉测量
  • 【软件安装】Windows操作系统中安装mongodb数据库和mongo-shell工具
  • 【AUTOSAR网络管理】T_NM_Timeout参数测试指南
  • 10G SFP+ 双纤光模块选购避坑指南:从SFP-10G-LRM到SFP-10G-ZR的兼容性与应用
  • 八股--SSM(2)
  • 【通用智能体】smolagents/open_deep_research:面向开放式研究的智能体开发框架深度解析
  • STM32 CubeMX时钟配置PWM信号输出
  • SOC-ESP32S3部分:8-GPIO输出LED控制
  • 辐射发射RE测试
  • 哈尔滨市建设安全监察网站/查询网址域名ip地址
  • 邮箱注册163免费注册入口/兰州网络seo
  • 海口建网站公司/网站免费推广软件
  • 企业网络营销推广方案策划范文/seo网络排名优化方法
  • 程序员做网站如何赚钱/永久免费跨境浏览app
  • 仪征市建设发展有限公司网站/怎么建立网站平台