如何应对 Android 面试官 -> 内存如何进行优化(上)?玩转 LeakCanary

前言


本章主要围绕内存相关的知识点讲解;

内存分配


在内存优化中,我们通常需要借助一些常用的 adb 命令,方便我们快速定位,下面是一些常用的 adb 命令总结

常用 adb 命令

 

arduino

代码解读

复制代码

adb shell getprop ro.product.model // 手机型号 adb shell dumpsys battery //电池信息 adb shell wm size //屏幕尺寸 adb shell wm density //屏幕像素密度 adb shell dumpsys window displays // 查看总得设备信息,包含了显示屏编号,像素密度,分辨率等等 adb shell settings get secure android_id // android id 一般用来标识不同的手机 adb shell service call iphonesubinfo 1 // IMEI adb shell getprop ro.build.version.release // 版本 adb shell cat /proc/cpuinfo // cpu 信息 adb shell cat /system/build.prop // 查看构建文件信息 adb shell cat /proc/meminfo // 查看内存信息 adb shell dumpsys meminfo //获取内存信息 adb shell procrank //查看进程内存排名 adb shell top adb shell top |grep your app name adb shell vmstat // adb shell vmstat 2 // adb shell top -d 20 > meminfo

内存指标概念

RSS(共享库、so 动态链接库) USS 进程独占内存空间

procs(进程)

r: Running队列中进程数量

b: IO wait的进程数量

memory(内存)

free: 可用内存大小

mapped:mmap映射的内存大小

anon: 匿名内存大小

slab: slab的内存大小

system(系统)

in: 每秒的中断次数(包括时钟中断)

cs: 每秒上下文切换的次数

cpu(处理器)

us: user time (用户时间)

ni: nice time

sy: system time(系统时间)

id: idle time

wa: iowait time

ir: interrupt time

用户时间 + 系统时间 = 进程时间;

内存分配


Java 内存分配模型

java 对象生命周期

  • 创建
    • 为对象分配内存空间,调用构造方法构造对象
  • 应用
    • 此时对象至少被一个强引用持有
  • 不可见
    • 对象还存在,但是没有被强引用了,如果被GC扫描到了,就会进行可达性分析
  • 不可达
    • 可达性分析,发现不可达(也就是没有任何强应用了)
  • 收集
    • GC准备对该对象内存空间进行重新分配
    • 如果重写了finalize方法,这个方法就会被调用
  • 终结
    • 被垃圾回收器 回收
  • 对象空间重新分配
    • 对象被回收之后,这块空间重新分配

      这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

       需要全套面试笔记的【点击此处即可】免费获取

java 对象的内存布局

  • 对象头
    • 存储对象自身的运行时数据
      • 哈希码
      • GC 分代年龄
      • 锁状态标识
      • 线程持有的锁
      • 偏向线程 ID
      • 偏向时间戳
    • 类型指针
    • 若为对象数据,还应该记录数组长度的数据
  • 实例数据
  • 对齐填充

本地方法栈

线程私有,每个线程不一样

程序计数器

线程切换使用,也是线程私有;

垃圾回收算法

标记清除

位置不连续,产生内存碎片,效率略低,两遍扫描。多次进行标记清除算法,内存就会千疮百孔;先标记存活对象,然后清除回收对象,产生内存碎片;

复制算法

实现简单,运行高效,没有内存碎片,但是利用率只有一半;将内存一分为二,将存活对象复制到剩余的一半,其余的回收

标记整理算法

没有内存碎片,效率偏低,两边扫描、指针需要调整;先标记,然后将存活对象整理到一起。剩下的回收掉

分代收集算法

综合应用上面的算法;

四种引用

Android 内存回收机制

Android Kill 机制

  • 在 Android 的 lowmemroykiller 机制中,会对于所有进程进行分类,对于每一类别的进程会有其 oom_adj 值的取值范围,oom_adj值越高则代表进程越不重要,在系统执行低杀操作时,会从 oom_adj 值越高的开始杀;
  • 对于期望较长时间留在后台的服务,应该将服务运行在单独的进程里,即是 UI 进程与 Servie 进程分离,这样期望长时间留在后台的 Serivce 会存在与一个被 lowmemorykiller 分类为 Service 进程的服务而获得较小的Adj 值,而占有大量内存的UI进程则会分类为 Cached 进程,能够在需要的时候更快地被回收;
oom_adj
  • 当所有应用退后台之后并不会立马被杀掉,而是通过 oom_adj 进行了一个分级[-16, 15] 从 -16 到 15 的这样的一个区间,应用对应的数字越小 越不容易被 OOM Killer 杀到
    • ams 又对齐进行了一个更高等级的区分 oom_score_adj,将应用优先级的区间划分为[-1000, 1000],从 -1000 到 1000,score 的值越低越不容易被杀掉;
  • 如果两个应用的 oom_adj 值一样,那么哪个 app 占用内存多,哪个就优先被杀掉。所以要尽可能的降低应用进入后台后的内存,才能保证尽可能的不被系统杀掉;

Android App内存组成以及限制

Android 给每一个 App 都分配一个 VM,让 App 运行在 Dalvik 上,这样即使 App 崩溃了也不会影响到系统,系统给 VM 分配了一定的内存大小,App 可以申请使用的内存大小不会超过此硬性逻辑限制,就算物理内存富余,如果应用超出 VM 最大内存,就会出现内存溢出 crash;

由程序控制操作的内存空间在 heap 上,分为 java heapsize 和 native heapsize;

native 层内存申请不受其限制,native 层受 native process 对内存大小的限制;

修改系统为每个App分配的内存大小;默认为每个app分配 16m 的内存大小;

LeakCanary 源码解析


从垃圾回收机制、源码分析、优缺点中分析;

总结的详细流程如下:

  • 通过 provider 进行 Leakcanary 的初始化逻辑
    • provider 的 onCreate 方法中,获取 Application
    • 加载 Leakcanary
      • 检查当前线程是否在主线程
      • 检查是否已经加载了 Leakcanary,如果加载了,则直接返回
      • 创建 Activity 的监视
        • 创建 Activity 的 onDestory 回调监视
          • 将可达的 Activity 删除
          • 根据 Activity 创建对应的弱引用,并绑定 ReferenceQueue
          • 将 reference 保存到 watchedObjects 数组中
          • 启动延时 5s 任务
          • 获取 GC 无法回收的 Activity
          • 通知内存泄露
        • 同 Application 绑定
      • 创建 Fragment 的监视
      • 调用上层模块 InternalLeakCanary.invoke

本质就是:监听 Activity 的 onDestory 方法回调,过 5s 之后进行一次 GC,通过 WeakReference 引用链看它有没有销毁;

LK 中使用的 垃圾回收算法

可达性分析算法、引用计算算法;

可以作为 GC Root 的对象有哪些?

  • 在线程栈中的局部变量(即正在被调用的方法里面的参数和局部变量)
  • 存活的线程对象
  • JNI的引用
  • Class对象(在Android中Class被加载后是不会被卸载的)
  • 引用类型的静态变量

模块层级(LK都有哪些模块,以及对应的模块都是做什么的?)

leakcanary-android

集成入口模块,提供 LeakCanary 安装,公开 API 等能力

leakcanary-android-core

核心模块

lealcanary-object-watcher、lealcanary-object-watcher-android、lealcanary-object-watcher-android-androidx、lealcanary-object-watcher-android-support-fragment

对象实例观察模块,在 Activity,Fragment 等对象的生命周期中,注册对指定对象实例的观察,有 Activity,Fragment,Fragment View,ViewModel 等;

leakcanary-android-process

和 leakcanary-android 一样,区别是会在单独的进程进行分析;

shark

hprof 文件解析与分析的入口模块;

shark-android

提供特定于 Android 平台的分析能力。例如设备的信息,Android 版本,已知的内存泄露问题等;

shark-graph

分析堆中对象的关系图模块;

shark-hprof

解析 hprof 文件模块;

share-log

日志模块;

LeakCanary 注册

 

ini

代码解读

复制代码

<application> <provider android:name="leakcanary.internal.AppWatcherInstaller$MainProcess" android:authorities="${applicationId}.leakcanary-installer" android:exported="false"/> </application>

这是通过注册 provider 进行 Leakcanary 的初始化逻辑,我们进入 AppWatcherInstaller 中看下

 

kotlin

代码解读

复制代码

internal sealed class AppWatcherInstaller : ContentProvider() { internal class LeakCanaryProcess : AppWatcherInstaller() { override fun onCreate(): Boolean { super.onCreate() AppWatcher.config = AppWatcher.config.copy(enabled = false) return true } } override fun onCreate(): Boolean { // 获取 Application val application = context!!.applicationContext as Application // 加载 LeakCanary InternalAppWatcher.install(application) return true } }

加载 Leakcanary,我们进入这个方法看下:

 

kotlin

代码解读

复制代码

internal object InternalAppWatcher { // 加载 fun install(application: Application) { // 检查当前线程是否在主线程 checkMainThread() if (this::application.isInitialized) { // 如果 Leakcanary 已经加载过了,直接返回 return } InternalAppWatcher.application = application val configProvider = { AppWatcher.config } // 监视 Activity ActivityDestroyWatcher.install(application, objectWatcher, configProvider) // 监视 Fragment FragmentDestroyWatcher.install(application, objectWatcher, configProvider) // 调用上层模块InternalLeakCanary.invoke onAppWatcherInstalled(application) } }

我们进入 ActivityDestroyWatcher 看下 Activity 是如何被监视的;

 

kotlin

代码解读

复制代码

internal class ActivityDestroyWatcher private constructor( private val objectWatcher: ObjectWatcher, private val configProvider: () -> Config ) { // 创建 Activity 的 destory 监听回调 private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() { override fun onActivityDestroyed(activity: Activity) { // Activity 的 onDestory 回调,触发存在对象检查 if (configProvider().watchActivities) { // 通过 objectWatcher 监视 Activity objectWatcher.watch(activity) } } } companion object { fun install( application: Application, objectWatcher: ObjectWatcher, configProvider: () -> Config ) { // 创建 Activity 的 onDestory 监听回调 val activityDestroyWatcher = ActivityDestroyWatcher(objectWatcher, configProvider) // 绑定 Application application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks) } } }

我们进入 ObjectWatcher 中看下 Acivity 是如何被监视的;

 

scss

代码解读

复制代码

@Synchronized fun watch( watchedObject: Any, name: String ) { if (!isEnabled()) { return } // 将可达的 activity 删除 removeWeaklyReachableObjects() val key = UUID.randomUUID() .toString() val watchUptimeMillis = clock.uptimeMillis() // 根据 activity 创建对应的弱引用,并绑定ReferenceQueue val reference = KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue) // 将 reference 保存到 watchedObjects 数组中 watchedObjects[key] = reference // 启动延时 5s 任务 checkRetainedExecutor.execute { // 获取 GC 无法回收的 Activity moveToRetained(key) } }

我们进入这个 removeWeaklyReachableObjects 方法看下,可达的 Activity 是怎么删除的;

 

csharp

代码解读

复制代码

private fun removeWeaklyReachableObjects() { var ref: KeyedWeakReference? do { // 重点,在 GC 或者 finalization 之前,在 WeakReferences 的被引用对象(这里是Activity)的可达性更改时,会把 WeakReferences 添加到创建时候指定的 ReferenceQueue 队列,这些可达性变更得对象,就是内存不泄露对象 ref = queue.poll() as KeyedWeakReference? if (ref != null) { // 在 watchedObjects 中删除不发送内存泄漏对象,剩下内存泄漏对象; watchedObjects.remove(ref.key) } } while (ref != null) }

我们接着看下这个延迟 5s 任务是如何创建的;

 

kotlin

代码解读

复制代码

val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5) private val checkRetainedExecutor = Executor { //在主线程延时五秒执行任务 mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis) }

我们接着看下无法回收的 Activity 是如何获取的,进入 moveToRetained 方法看下;

 

kotlin

代码解读

复制代码

@Synchronized private fun moveToRetained(key: String) { // 将可达activity删除 removeWeaklyReachableObjects() val retainedRef = watchedObjects[key] if (retainedRef != null) { // 保存当前时间作为泄漏时间 retainedRef.retainedUptimeMillis = clock.uptimeMillis() // 通知 InternalLeakCanary 发生内存泄漏 onObjectRetainedListeners.forEach { it.onObjectRetained() } } }

我们来看下内存泄露的场景是如何进行上报的,我们进入 HeapDumpTrigger 的 onObjectRetained 方法看下;

 

kotlin

代码解读

复制代码

fun onObjectRetained() { // 再次检查内存是否泄露 scheduleRetainedObjectCheck("found new object retained") }

我们进入 scheduleRetainedObjectCheck 方法看下;

 

kotlin

代码解读

复制代码

private fun scheduleRetainedObjectCheck( reason: String, delayMillis: Long ) { if (checkScheduled) { return } checkScheduled = true backgroundHandler.postDelayed({ checkScheduled = false // 检查泄漏对象 checkRetainedObjects(reason) }, delayMillis) }

我们进入 checkRetainedObjects 方法看下;

 

scss

代码解读

复制代码

private fun checkRetainedObjects(reason: String) { ... if (retainedReferenceCount > 0) { // 执行一次 GC,确认所有存在对象都是泄露对象; gcTrigger.runGc() retainedReferenceCount = objectWatcher.retainedObjectCount } // 检查当前所有存在对象的个数 if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) { showRetainedCountWithDebuggerAttached(retainedReferenceCount) // 如果配置了 debug 不使用 heap 且正在 debug,延时 20s 在执行checkRetainedObjects(reason) scheduleRetainedObjectCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS) return } val heapDumpUptimeMillis = SystemClock.uptimeMillis() KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis dismissRetainedCountNotification() // 执行dump Heap操作 val heapDumpFile = heapDumper.dumpHeap() lastDisplayedRetainedObjectCount = 0 objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) HeapAnalyzerService.runAnalysis(application, heapDumpFile) }

我们来看下当前所有存在对象的个数是如何检查的;

 

kotlin

代码解读

复制代码

private fun checkRetainedCount( retainedKeysCount: Int, retainedVisibleThreshold: Int ): Boolean { val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount lastDisplayedRetainedObjectCount = retainedKeysCount if (retainedKeysCount == 0) { // 存在对象为 0 return true } if (retainedKeysCount < retainedVisibleThreshold) { // 存在对象低于阈值5个 if (applicationVisible || applicationInvisibleLessThanWatchPeriod) { showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold) // 当前应用可见,或者不可见时间间隔少于 5s,重新安排到 2s 后执行 checkRetainedObjects scheduleRetainedObjectCheck( "Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS ) return true } } return false }

到此,Leakcanary 的流程就整体跑完了;

线上内存如何监控


在引入任何自动分析工具之前,对于 Activity 泄漏,一般都是在自动化测试阶段监控内存占用,一旦超过预期,则发起一次 GC 后进行 Dump Hprof 操作。分析人员将 Hprof 文件导入 MAT 中查看各个 Activity 的引用链,找出被静态成员或 Application 对象等长生命周期对象持有的 Activity,再进行修复。对于冗余的 Bitmap,也是将 Hprof 导入 Android Monitor 后通过 Android Monitor 自带的工具找出冗余的 Bitmap 对象。

自动化监测目标流程

  • 自动且较为准确地监测 Activity 泄漏,发现泄漏之后再触发 Dump Hprof 而不是根据预先设定的内存占用阈值盲目触发;
  • 自动获取泄漏的 Activity 和冗余 Bitmap 对象的引用链;
  • 能灵活地扩展 Hprof 的分析逻辑,必要时允许提取 Hprof 文件人工分析;

监测阶段

Activity 对象被生命周期更长的对象通过强引用持有,使 Activity 生命周期结束后仍无法被 GC 机制回收,导致其占用的内存空间无法得到释放

具体如何做?

  • Activity 在执行销毁的时候 我们如何得知?
  • 如何判断一个 Activity 无法被 GC 机制回收?

采用 ActivityLifeCycleCallbacks + WeakHashMap 的方式,我们可以不主动暴露 RefrenceQueue 这个对象,WeakHashMap 的 key 可以自动被弱引用,可以自动被回收,那么这个 key 就可以是 Activity,key 被回收了,那么 value 也就跟着被移除了,监听 Activity 的回收,也就达到监听泄露的目的了;

借助 ActivityLifeCycleCallbacks 的 onActivityDestoryed 回调,在回调中将传递过来的 Activity 放入 WeakHashMap 中,然后在 onStop 的回调中通过 GC 监测 Activity 有没有泄露;

好了,内存这块就讲到这里吧

下一章预告


继续搞内存

欢迎三连


来到来了,点个关注,点个赞吧,你的支持是我最大的动力~

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值