您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

Android Camera 内存问题剖析

时间:2020-08-20 10:05:16  来源:  作者:

本文通过一类 Android 机型上相机拍摄过程中的 native 内存 OOM 的问题展开,借助内存快照裁剪回捞和 Native 内存监控工具的赋能,来深入剖析此类问题。

背景

Raphael 是西瓜视频 Android 团队开发的一款 native 内存监控工具,在字节跳动内部产品(如西瓜、抖音、头条等)上广泛用于监控 native 内存泄漏问题。在抖音 7.8.0-8.3.0 上搜集到大量因虚拟内存触顶而 crash 的内存日志现场(如 pthread_create、GL error、EGL_BAD_ALLOC),其中 60%以上都是 camera 相关的内存泄漏,占整体 crash 的 15%以上(JAVA & Native)。同时也收到 OPPO 等厂商反馈抖音 App 在其新机型上 native crash 比其他机型高了 3 倍以上,分析厂商提供的日志发现基本都是虚拟内存触顶导致的 carsh,这其中 80%以上都有 camera 相关的内存分配失败的日志。

问题

通过对 native 内存监控搜集到的日志进行堆栈聚合和 so 级的内存占用统计,可以发现截止到 OOM 时工具拦截到的 native 内存总量已经达到了 1.3G 左右(32 位下应用可直接使用的 native 内存上限约 2G),这其中占比最大的是 CameraMetaData 对象间接引用的内存,native 内存泄漏十分严重。

Android Camera 内存问题剖析

 

由于 native 内存分配的频率过高,获取 Java 层堆栈又比较耗时,在拦截 native 内存分配时并不适合直接频繁抓取 Java 堆栈。Native 内存不同于 Java 内存,单从拦截到的数据很难直观给出结论。通常对于内存等资源不合理使用导致的资源不足而引发的问题都很难归因,从拦截到的数据来看,CameraMetaData 所引用的内存最大,嫌疑也最大,基于此决定剖析一下这个问题

初步分析

分析 native 内存的分配和释放

通过拦截到的堆栈可以看出,CameraMetaData 的创建堆栈的上层是 Java 调用,最终在 native 层进行的内存分配(boot-framework.oat & libandroid_runtime.so)。CameraMetaData 对象有两部分内存,对象本身 & mBuffer 指向的 camera_metadata_t 所引用的内存;通过源码可知,每个 CameraMetadata 对象的 mBuffer 所指向的 camera_metadata_t 是独立的,彼此是不重叠的。

Android Camera 内存问题剖析

 


Android Camera 内存问题剖析

 

既然工具能拦截到这么多的未释放的内存分配,一定是因为这些内存的释放逻辑出问题导致的,我们需要优先调查清楚 CameraMetadata.mBuffer 的释放逻辑。通过分析 CameraMetadata.cpp 的源码可知,CameraMetadata::release()并未释放 mBuffer 所指向的内存,而是把 mBuffer 所指向的内存赋值给了另一个 CameraMetadata 对象;CameraMetadata::clear()是真释放,而 clear 的调用有两个场景:一个是在 camera_metadata_t 复用时,另一个是 CameraMetadata 对象析构时。

Android Camera 内存问题剖析

 

前述结论可知 CameraMetadata.mBuffer 所指向的 camera_metadata_t 是彼此独立的。通过工具拦截到的堆栈和分配数量猜测,Native OOM 时内存中一定存在大量的 CameraMetadata 实例。C++对象的析构通常是调用 delete 来实现的,AOSP 里想搜索哪里 delete 了一个 CameraMetaData 对象是很难的,因为很难知道 delete 时的变量名。根据一个基本的 C++编程规范,内存通常在哪里创建的,应该就在那里释放,我们全局搜索 new CameraMetaData 字符串就可以很轻松的发现 CameraMetaData 对象的创建和释放均是在/frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp里实现的。

Android Camera 内存问题剖析

 


Android Camera 内存问题剖析

 


Android Camera 内存问题剖析

 

通过 android_hardware_camera2_CameraMetadata.cpp 里的注册清单可以看到与这些函数关联的 Java 层 class 是android/hardware/camera2/impl/CameraMetadataNative,CameraMetadata_close 函数在 Java 对应的是 nativeClose 函数。可以进一步发现 CameraMetaDataNative 里 nativeClose 函数是在 close 函数里调用的,而 close 函数又是在 finalize 函数调用的。

Android Camera 内存问题剖析

 


Android Camera 内存问题剖析

 

通过上述分析可知只有在 CameraMetaDataNative 对象执行 finalize 方法时才会回收与之对应的 native 内存,而 finalize 方法又是在 FinalizerDaemon 线程里执行的,猜测到如果发生了上述堆栈的 native OOM,Java 层一定存在大量还没有执行 finalize 方法的 CameraMetaDataNative 对象。

排查 Java 堆现场

幸运的是我们通过内存快照裁剪工具(Tailor)轻松拿到了大量这类 native OOM 时对应的 Java 堆内存快照文件。这些内存快照文件完美证实了之前的猜想,当发生这类 native OOM 时 Java 层的确存在大量的 CameraMetadataNative 对象。以下图为例,这些 CameraMetadataNative 对象里除 6 个被其他代码引用外,其余对象全部在 FinalizerDaemon 线程的队列里,等待执行 finalize 方法。同时,快照里有 6658 个对象,只有大约 600+对象的 mMetadataPtr 是等于 0 的,说明这部分对象对应的 Native 内存需要在 finalize 时释放,这跟工具拦截的数据是完全匹配的,也间接验证了 Native 内存监控的正确性和可靠性

Android Camera 内存问题剖析

 

深入分析

排查 Finalize 执行

虽然上述分析验证了问题,也证实了之前的猜想,但仍未找到导致此类问题的深层次原因,对于最终解决此类问题也仍然束手无策。为什么会有这么多的 CameraMetadataNative 对象等待执行 finalize 方法或许是下一步的调查方向。做过 Java 稳定性治理的同学应该都知道一类很有名的 TimeoutException 异常,这类异常的根本原因是 finalize 执行超时导致的,这个 case 会不会是某个对象的 finalize 执行超时导致的?

Android Camera 内存问题剖析

 

结合 FinalizerDaemon 的源码可以看到,每执行一个对象的 finalize 方法时,都会通过finalizingObject属性记录当前的对象。如果真的是 finalize 超时导致的,一定存在 finalizingObject 属性不为空的现场。我们在遍历完所有相关内存快照里的 FinalizerDaemon 线程状态后发现,这些现场的 finalizingObject 属性均为空。这个结果很意外,似乎并不是某个对象的 finalize 方法执行超时导致的。

Android Camera 内存问题剖析

 

通过分析finalizingReference = (FinalizerReference<?>)queue.remove() 发现这行代码后面的逻辑并没有对 finalizingReference 判空,说明这个地方一定不会返回空。既然不为空, queue.remove() 只能 block 等待,这个 ReferenceQueue.java 的源码也证实了猜想。

Android Camera 内存问题剖析

 

源码显示 goToSleep 是个同步方法,可能会 block。但遍历所有相关快照发现所有的 needToWork 属性均是 false,证明已经走过(只有FinalizerWatchdogDaemon.INSTANCE.goToSleep() 会置为 false,而且这个函数是 private 的,只在 FinalizerDaemon 线程里调用),所以 block 在这里的可能性几乎没有。

Android Camera 内存问题剖析

 


Android Camera 内存问题剖析

 

其实 block 在这里的原因通常是因为只有在 GC 时才会将需要执行 finalize 的对象加入到 FinalizerDaemon 的队列里。如果一段时间内没有 GC,且队列就为空时,上面的 remove 会一直 block,直到 GC 后才有对象加入到这个队列里。巧合的是我们在发生这类 native OOM 时会通过 Tailor 主动 dump Java 堆的内存快照,而 dump 快照时会触发 GC & suspend,这个最终导致大量的 CameraMetadataNative 对象被同时加入到 FinalizerDaemon.queue 的队列里。

分析 GC 策略

通过上述分析可知如果不是 GC,这些对象是不会被被加入到 FinalizerDaemon.queue 里的,这说明这类 native OOM 发生前的一段时间内一直没有 GC,才导致大量 CameraMetadataNative 对象没有及时执行 finalize,进而发生 native OOM。以上分析也在线下进入到拍摄页后静置观察实验中得到验证,这其中大概每隔 30s-40s 甚至更长时间 Java 堆才会主动触发一次 GC,在这期间 native 内存会不断增长,直到 GC 后才会大幅下降,Java & Native 内存才会恢复到正常水平。虽然问题不是 block 在 finalize 环节,但最终这个问题的原因被锁定在了 GC 逻辑上!

Android Camera 内存问题剖析

 


Android Camera 内存问题剖析

 

了解 GC 的同学可能会知道 ART 虚拟机的 GC cause 有很多种,kGcCauseForAlloc/kGcCauseBackground 是虚拟机最易频繁触发的。当停留在拍摄页不做任何操作时,程序逻辑相对简单,这期间只有相机服务周期(>=30 次/s)地通过 binder 在应用端触发创建 CameraMetadataNative 对象,并在拍摄页显示一张相机采集到的图像。这个过程 Java 堆只有 CameraMetadataNative 对象创建,而 CameraMetadataNative 自身占用内存比较小,一次 GC 之后 Java 堆内存比较富裕的情况下,虚拟机很长一段时间内不会主动触发 GC。如果这期间 native 内存的增幅过大,在下次 GC 之前触顶就发生 native OOM

Android Camera 内存问题剖析

 

综上,这类 native OOM 的根本原因是:当应用自身的 native 内存本身已处于高水位时,开启相机后,相机服务会持续通过 binder 通信在应用侧创建 CameraMetadataNative 对象,创建 CameraMetadataNative 对象的同时也会在应用侧通过 jni 接口在 native 层创建/复用一块存放 camera_metadata_t 的相对比较大的内存。由于 Java 层的 CameraMetadataNative 对象本身比较小,这种连续创建小对象的行为一定时间内很难触发 Java 层的 GC,导致其间接引用的 native 内存不断上涨,最终触发虚拟内存上限而 crash。

解决思路

问题的原因虽然相对比较简单,但如何解决这类问题还是比较难抉择的。既然是 GC 不及时导致的,一种简单的方案就是在拍摄页周期性触发 GC。但如果 GC 间隔比较小,GC 毕竟是耗时的,GC 过于频繁会严重影响拍摄体验;如果 GC 间隔时间比较长,还是会有大概率重蹈这类 native OOM 的覆辙。

主动触发 GC 的方案很难平衡对性能的影响。其实问题的重点不是 Java 层,而是 Java 对象引用的 native 内存,如果及时主动释放这部分内存就可以从根本上彻底解决此类问题。通过前面的分析可以知道,这部分内存原本是在 GC 时的 finalize 环节回收,但如果提前发现 CameraMetadataNative 不再使用时,主动触发来释放这部分内存就可以一劳永逸。通过分析源码可以发现 CameraMetadataNative 传递到应用层之后后续并未再使用,在应用层使用完 CameraMetadataNative 对象之后,通过反射调用 close 函数即可释放其所引用的 native 内存。

Android Camera 内存问题剖析

 

线下实验也可以发现,开启主动回收策略后,Native 内存的增长速度比之前大幅降低。这期间 Java 堆& native 层仍有持续增加的小对象,但 native 的增长速度远小于 Java 层了,这种场景下 Java 内存会在 native 内存触顶之前先触发 GC,而大幅降低了发生 native OOM 的可能

Android Camera 内存问题剖析

 

最终该方案上线后,效果十分明显,此类 crash(Java & Native 总占比>15%)基本清零。后续搜集到的内存监控日志里 CameraMetadata 相关的内存基本都在 2M 以内,效果立竿见影!

总结

此类问题存在时间很久,至少从 Android 4.4 开始都是通过 CameraMetadataNative 的 finalize 函数来释放 native 内存。过去拍摄的需求比较简单,绝大多数时候都是使用 ROM 自带的相机应用来拍照,因为这类 app 比较简单,native 内存水位本身很低,很难触发到虚拟内存的上限,所以此类问题并没暴露出来。随着小视频等 app 的兴起,拍摄需求越来越重(特效&美颜等),app 也越来越复杂,应用自身的 native 内存水位不断上涨,加上 native 内存泄漏等原因,当长时间停留在拍摄页时,这类问题就很容易触发。

此外,CameraMetadata 的内存分配失败时,并不会直接 crash,这个时候有其他内存分配请求时才会触发 crash(如线程创建、GL 内存分配等),这也是很多拍摄过程中相机黑屏问题的根本原因。该方案也不经意间解决了长期存在的拍摄时相机黑屏的疑难问题。

这类问题既有应用自身的原因,也有内存回收策略设计的原因。应用在尽可能减少泄漏的同时,也应该努力降低自身 native 内存水位。AOSP 里利用 Java 的 finalize 方法来释放其间接引用的 native 内存是个偷懒挖坑的设计,类似的案例在 AOSP 里比比皆是。我们在实际开发中,类似内存这种有限的资源应及时回收,甚至可以主动限定对象的生命周期,一旦完成使命就主动回收其占用的内存,避免使用 finalize 逻辑来释放 native 内存。

文中提高的两个工具(Native 内存监控工具 Raphael & Android 堆内存快照裁剪压缩工具)是西瓜视频 Android 团队在长期的内存优化治理中开发的两套高效实用的基础工具,在我司内部各大 app 中应用非常广泛,是内存优化&稳定性治理的绝对首选。这两套工具我们也会在后续的监控工具建设&优化治理实践等技术文章中介绍相关技术细节,敬请关注。

更多分享

字节跳动自研线上引流回放系统的架构演进

字节跳动表格存储中的事务

iOS大解密:玄之又玄的KVO

今日头条 Android '秒' 级编译速度优化

字节跳动-西瓜视频 Android 团队

字节跳动-西瓜视频 Android 团队是负责字节跳动旗下西瓜视频 App 研发的客户端团队,团队在满足业务高速迭代的同时,持续优化性能和体验,提升研发效率,探索 Flutter 等跨平台方案。我们长期招聘业务研发、架构师、Flutter 工程师、骨干工程师、实习生,在北京、杭州、上海三地均有职位。业务体量大,团队成长快,技术挑战大,欢迎各路人才加入!联系邮箱: tech@bytedance.com ;邮件标题:姓名-工作年限-西瓜-Android



Tags:Android   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  Tags: Android  点击:(7)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  Tags: Android  点击:(26)  评论:(0)  加入收藏
如果你是一名忠实的Android玩家,那么可能会知道,今年的Android 12系统在版本规划上与“往届”相比可以说是很有些特殊。具体来说,除了前段时间刚刚推出正式版的Android 12外,谷...【详细内容】
2021-11-10  Tags: Android  点击:(23)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  Tags: Android  点击:(36)  评论:(0)  加入收藏
今年5月,谷歌推出了Android 12,这是原生安卓系统史上最大的设计变化,10月4日,谷歌推出全新的Android12正式版本,并且宣布会在今年晚些时候应用于安卓设备,对比Android11的挤牙膏式...【详细内容】
2021-10-29  Tags: Android  点击:(125)  评论:(0)  加入收藏
今天凌晨,谷歌正式发布了全新一代安卓Android 12系统,拥有全新的UI,同时带来了六大新功能,除此以外还有10月的安全补丁,下面就给大家介绍这六大新功能以及安卓Android 12系统配置...【详细内容】
2021-10-22  Tags: Android  点击:(53)  评论:(0)  加入收藏
用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0...【详细内容】
2021-08-17  Tags: Android  点击:(76)  评论:(0)  加入收藏
前言耗电量指标 待机时间成关注目标 提升用户体验 通过不同的测试场景,找出app高耗电的场景并解决01需要的环境准备1、python2.7(必须是2.7,3.X版本是不支持的) 2、golang语言...【详细内容】
2021-08-04  Tags: Android  点击:(93)  评论:(0)  加入收藏
6 月 29 日,微软向 Windows 预览体验计划的 Dev 通道推送了 Windows 11 的第一个预览版本,我们也在第一时间升级到了最新系统,可以点击这里查看 APPSO 的抢先体验。 关于 Windo...【详细内容】
2021-07-14  Tags: Android  点击:(90)  评论:(0)  加入收藏
玩过王者荣耀的朋友,几乎无人不晓「鲁班七号」这个英雄。作为 Android 的应用程序包,「APK」对于资深 Android 用户来说,知名度并不亚于前者。 也正因如此,日前 Google 的一份声...【详细内容】
2021-07-08  Tags: Android  点击:(44)  评论:(0)  加入收藏
▌简易百科推荐
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(6)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(7)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(16)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(21)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(15)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(26)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(36)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(57)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(51)  评论:(0)  加入收藏
概述当Android应用程序需要访问设备上的敏感资源时,应用程序开发人员会使用权限模型。虽然该模型使用起来非常简单,但开发人员在使用权限时容易出错,从而导致安全漏洞。本文中,...【详细内容】
2021-09-07  SecTr安全团队    Tags:Android开发   点击:(66)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条