您的位置:首页 > 娱乐 > 明星 > 如何应对 Android 面试官 -> 内存如何进行优化?玩转 LeakCanary

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

2024/12/23 14:33:14 来源:https://blog.csdn.net/IT_Android/article/details/141859270  浏览:    关键词:如何应对 Android 面试官 -> 内存如何进行优化?玩转 LeakCanary

前言


在这里插入图片描述

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

内存分配


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

常用 adb 命令

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 topadb shell top |grep your app nameadb 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 内存分配模型

在这里插入图片描述

内存分配模型,详细的讲解可以看我之前的文章(https://juejin.cn/post/7314365534107238438)

java 对象生命周期

在这里插入图片描述

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

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 注册

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

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

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 {// 获取 Applicationval application = context!!.applicationContext as Application// 加载 LeakCanaryInternalAppWatcher.install(application)return true}
}

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

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

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

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 监视 ActivityobjectWatcher.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 是如何被监视的;

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

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

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 任务是如何创建的;

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

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

@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 方法看下;

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

我们进入 scheduleRetainedObjectCheck 方法看下;

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

我们进入 checkRetainedObjects 方法看下;

private fun checkRetainedObjects(reason: String) {...if (retainedReferenceCount > 0) {// 执行一次 GC,确认所有存在对象都是泄露对象;gcTrigger.runGc()retainedReferenceCount = objectWatcher.retainedObjectCount}// 检查当前所有存在对象的个数if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) returnif (!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 = heapDumpUptimeMillisdismissRetainedCountNotification()// 执行dump Heap操作val heapDumpFile = heapDumper.dumpHeap()lastDisplayedRetainedObjectCount = 0objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)HeapAnalyzerService.runAnalysis(application, heapDumpFile)}

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

 private fun checkRetainedCount(retainedKeysCount: Int,retainedVisibleThreshold: Int): Boolean {val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCountlastDisplayedRetainedObjectCount = retainedKeysCountif (retainedKeysCount == 0) {// 存在对象为 0return true}if (retainedKeysCount < retainedVisibleThreshold) {// 存在对象低于阈值5个if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold)// 当前应用可见,或者不可见时间间隔少于 5s,重新安排到 2s 后执行 checkRetainedObjectsscheduleRetainedObjectCheck("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 有没有泄露;

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

下一章预告


继续搞内存

欢迎三连


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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com