草庐IT

一次不讲武德的 Android 线上 OOM 的排查过程

像程序那样去思考 2023-03-28 原文

作者:王晨彦

开篇

一天,后台统计到线上有大量 OOM 崩溃,小王收到老板的紧急指令,立即排查!

小王心想,这还不简单,待我看看崩溃堆栈,分分钟解决。

于是小王不慌不忙的打开崩溃后台,一看傻眼了,同样的 OOM,却有几十种不同的堆栈,大到创建 View,小到 new 一个 String。

小王差点骂了出来:这 OOM 不讲武德啊!

骂完之后,还是得解决问题啊,否则怎么面对老板啊。

心路历程

正郁闷着,小王突然想起曾经看过性能优化的文章,里面介绍了 Android Studio 中集成的 Profiler 可以分析 APP 内存。

既然堆栈看不出什么问题,那就只能照着文章的方法,碰碰运气了。

于是小王点开了 IDE 底部那个毫不起眼的「Profiler」面板,映入眼帘的是:

小王一眼就看到了 MEMORY 栏,这不就是内存使用嘛。

嗯,数据倒是挺全,可是怎么知道哪里导致 OOM 了啊,小王又开始怀疑人生了…

“放着不动肯定看不出什么啊,内存是动态申请的嘛。”

小王心想,既然这么多 OOM,那么肯定是 APP 内的常用页面导致的,于是小王开始一边来回切换常用页面,一边观察内存走势。

经过多次尝试,小王发现应用的内存占用确实在不断升高,即使手动 GC 之后,仍然居高不下。

小王想起面试宝典中「无法被 GC 回收的对象,会导致内存泄露」,于是手动点了下 GC,避免数据不准确。

Java 堆从 15.7MB 涨到 19.3MB,好像问题不大,而 Native 就离谱了,好家伙,竟然从 56.1MB 涨到了 97.5MB,分分钟就涨了 40MB+。

小王喜出望外,终于发现内存问题了!看来平时摸鱼的时候多看看文章真是没坏处啊。

可是,就算知道内存不正常,但还是不能定位是哪段代码导致了…

小王平复了一下心情,继续观察规律,终于发现,每次从A页面跳转出去,内存都会增加几M,而且 GC 无法回收,那肯定是这个页面有问题了!

于是小王骂骂咧咧的开始阅读这个页面的代码,希望能够发现内存泄露的元凶。心里嘀咕着,让我看看是哪个 ** 写出了内存泄露的代码。

小王逐字逐句看完了代码:可是并没有什么问题啊,就是一个普通的列表页,还是用 RecyclerView 实现的,没啥毛病啊。

这下又把小王难住了,小王心想,不能在黎明前倒下啊,于是又想起文章中关于 Profiler 的介绍,可以使用 Dump 功能方便的查看当前的内存快照,兴许能发现什么端倪呢。

好家伙,原来是 Bitmap 占了这么大内存,于是小王又想起面试宝典。

Android 2.3.3(API level 10) 和更早的版本,Bitmap 对象和对象里对应的像素数据是分开存储的,Bitmap 存在虚拟机的堆里,而像素数据存储在 Native 内存里。

从 Android 3.0(API level 11) 到 Android 7.1(API level 25),Bitmap 对象及其像素数据都存储在虚拟机的堆里。

从 Android 8.0(API level 26) 开始,Bitmap 对象存储在虚拟机的堆里,而对应的像素数据存储在 Native 堆里。

小王测试的手机是 Android 10,Bitmap 数据存储在 Native 堆,所以基本上可以确定就是 Bitmap 导致内存泄露了。虽然又前进了一大步,但还是找不到原因。

小王发现,点击对象,可以查看所有实例的引用链,这下可把小王高兴坏了,而且小王还发现了一个非常可疑的引用链。

这不是 Coil 的 Memory Cache 嘛,可是这里明明是有缓存的嘛,怎么还会泄露,难不成是这个开源库有 bug?

https://github.com/coil-kt/coil

小王怀着忐忑的心情打开了 RealMemoryCache 这个类。

这不就是一个基于 LRU 实现的内存缓存嘛,乍一看好像没什么毛病。

没时间仔细研究了,小王心想,先看看开源社区有没有人反馈过这个问题,小王过滤了一下包含 "memory leak" 关键字的 issue。

果然有一个 PR 的标题非常接近 Fix memory leak if request is started on detached view.

https://github.com/coil-kt/coil/pull/518

看起来问题已经被修复且已经发布了新版本,于是小王立马升级版本再次测试,果然没有泄露了。

于是立马提交代码,兴冲冲的去找老板炫耀了!!!

追根溯源

回过头来,小王心想,作为一个“有上进心”的程序员,我得看看是什么原因导致的泄露啊。

于是再次打开 PR,在诸多改动中,终于找到一个真正的代码改动,其他都是测试用例。

小王不禁感慨,歪果仁就是专业呀,改了两行代码就要写一堆测试用例。

小王终于弄清了导致泄露的原因,原来是在快速切换页面时,有时页面已经销毁了,才开始加载图片,此时 Coil 会把这个 View 对象保存起来,等待 View detach 的时候释放,然而此时 View 已经是 detach 的状态了,因此永远不会被释放了,而 Bitmap 又被 View 持有,而我们都知道 Bitmap 是内存占用大户,因此就出现了上面 Bitmap 占用大量内存的情况。

而解决方案就是再判断一下 View 是否已经 Detach,是的话就直接释放了,避免造成泄露。

最后小王高高兴兴的下班了。

有关一次不讲武德的 Android 线上 OOM 的排查过程的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  3. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  4. ruby-on-rails - Rake 任务仅调用一次时执行两次 - 2

    我写了一个非常简单的rake任务来尝试找到这个问题的根源。namespace:foodotaskbar::environmentdoputs'RUNNING'endend当在控制台中执行rakefoo:bar时,输出为:RUNNINGRUNNING当我执行任何rake任务时会发生这种情况。有没有人遇到过这样的事情?编辑上面的rake任务就是写在那个.rake文件中的所有内容。这是当前正在使用的Rakefile。requireFile.expand_path('../config/application',__FILE__)OurApp::Application.load_tasks这里

  5. ruby - 我怎样才能只写一次 "Text"并同时检查 path_info 是否包含 'A' ? - 2

    -if!request.path_info.include?'A'%{:id=>'A'}"Text"-else"Text"“文本”写了两次。我怎样才能只写一次并同时检查path_info是否包含“A”? 最佳答案 有两种方法可以做到这一点。使用部分,或使用content_forblock:如果“文本”较长,或者是一个重要的子树,您可以将其提取到一个部分。这会使您的代码变干一点。在给出的示例中,这似乎有点矫枉过正。在这种情况下更好的方法是使用content_forblock,如下所示:-if!request.path_info.inc

  6. ruby-on-rails - 使用 Ruby 正确处理 Stripe 错误和异常以实现一次性收费 - 2

    我查看了Stripedocumentationonerrors,但我仍然无法正确处理/重定向这些错误。基本上无论发生什么,我都希望他们返回到edit操作(通过edit_profile_path)并向他们显示一条消息(无论成功与否)。我在edit操作上有一个表单,它可以POST到update操作。使用有效的信用卡可以正常工作(费用在Stripe仪表板中)。我正在使用Stripe.js。classExtrasController5000,#amountincents:currency=>"usd",:card=>token,:description=>current_user.email)

  7. ruby - Ruby 中的 block 和过程 - 2

    我已经开始学习Ruby,我已经阅读了一些教程,甚至还买了一本书(“ProgrammingRuby1.9-ThePragmaticProgrammers'Guide”),我遇到了一些以前从未见过的新东西使用我知道的任何其他语言(我是一名PHP网络开发人员)。block和过程。我想我明白它们是什么,但我不明白的是为什么它们如此伟大,以及我应该在何时何地使用它们。我到处都看到他们说block和过程是Ruby中的一个很棒的特性,但我不理解它们。这里有人能给像我这样的Ruby新手一些解释吗? 最佳答案 block有很多好处。电梯演讲:bloc

  8. ruby - 如何在 Ruby 中只执行一次方法?有静态变量吗? - 2

    我写了一个脚本,其中包含一些方法定义,没有类和一些公共(public)代码。其中一些方法执行一些非常耗时的shell程序。然而,这些shell程序只需要在第一次调用该方法时执行。现在在C中,我会在每个方法中声明一个静态变量,以确保这些程序只执行一次。我怎么能在Ruby中做到这一点? 最佳答案 ruby中有一个成语:x||=y。defsomething@something||=calculate_somethingendprivatedefcalculate_something#somelongprocessend但是如果您的“长时间

  9. ruby - 格式化数字以每隔三位数拆分一次 - 2

    我想在格式化数字时每隔三个字符放置一个空格。根据这个规范:it"shouldformatanamount"dospaces_on(1202003).should=="1202003"end我想出了这段代码来完成这项工作defspaces_onamountthousands=amount/1000remainder=amount%1000ifthousands==0"#{remainder}"elsezero_padded_remainder='%03.f'%remainder"#{spaces_onthousands}#{zero_padded_remainder}"endend所以我

  10. 电脑启动后显示器黑屏怎么办?排查下面4个问题,快速解决 - 2

    电脑启动出现显示器黑屏是一个相当常见的问题。如果您遇到了这个问题,不要惊慌,因为它有很多可能的原因,可以采取一些简单的措施来解决它。在本文中,小编将介绍下面4种常见的电脑启动后显示器黑屏的原因,排查这些原因,快速解决! 演示机型:联想Ideapad700-15ISK-ISE系统版本:Windows10一、显示器问题如果出现电脑启动后显示器黑屏的情况。那么首先您需要检查一下显示器是否正常工作。您可以通过更换另一个显示器或将当前显示器连接到另一台计算机来检查显示器是否存在问题。如果问题仍然存在,那么您可以排除显示器故障的可能性。 二、显卡问题如果您的电脑配备了独立显卡,那么显卡故障也可能是导致电脑

随机推荐