Android 性能优化系列:抖音字节跳动技术团队教你Java 内存优化

整体流程分为:

  1. Hprof 收集

  2. 分析时机

  3. 分析策略

Hprof 收集

收集过程我们设置了多种策略可以自由组合,主要有 OOM、内存触顶、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值时触发,线下线上策略配置不同。

为了解决 dump 挂起进程问题,我们采用了子进程 dump+fileObsever 的方式完成 dump 采集和监听。

在 fork 子进程之前先 Suspend 获取主进程中的线程拷贝,通过 fork 系统调用创建子进程让子进程拥有父进程的拷贝,然后 fork 出的子进程中调用 Hprof 的 DumpHeap 函数即可完成把耗时的 dump 操作在放在子进程。由于 suspend 和 resume 是系统函数,我们这里通过自研的 native hook 工具对 libart.so hook 获取系统调用。由于写入是在子进程完成的,我们通过 Android 提供的 fileObsever 文件写入进行监控获取 dump 完成时机。

图片

图 5.子进程 dump 流程图

Hprof 分析时机

为了达到分析过程对于用户无感,我们在线上、线下配置了不同的分析时机策略,线下在 dump 分析完成后根据内存状态主动触发分析,线上当用户下次冷启退出应用后台且内存充足的情况下触发分析。

分析策略

分析策略我们提供了两种,一种在 Android 客户端分析,一种回传至 Server 端分析,均通过 MAT 分析引擎进行分析。

端上分析
分析引擎

端上分析引擎的性能很重要,这里我们主要对比了 LeakCanary 的分析引擎 Shark 和 Haha 库的 MAT。

图片

图 6. Shark VS MAT

我们在相同客户端环境对 160M 的 HPROF 多次分析对比发现 MAT 分析速度明显优于 Shark,另外针对 MAT 分析后仍持有统治者树占用内存我们也做了主动释放,对比性能收益后采用基于 MAT 库的分析引擎进行分析,对内存泄漏引用链路自动归并、大对象小对象引用链自动分析、大图线下自动还原线上过滤无用链路,分析结果如下:

内存泄漏

图片

图 7. 内存泄漏链路

对泄漏的 Activity 的引用链进行了聚合分析,方便一次性解决该 Activity 的泄漏链释放内存。

大对象

图片

图 8. 大对象链路

大对象不止分析了引用链路,还递归分析了内部 top 持有对象(InRefrenrece)的 RetainedSize。

小对象

图片

图 9. 小对象链路

小对象我们对 top 的外部持有对象(OutRefrenrece)进行聚合得到占有小对象最多的链路。

图片

图片

图 10. 图片链路

图片我们过滤了图片库等无效引用且对 Android 8.0 以下的大图在线下进行了还原。

图片

回传分析

为了最大限度的节省用户流量且规避隐私风险,我们通过自研 HPROF 裁剪工具 Tailor 在 dump 过程对 HPROF 进行了裁剪。

裁剪过程

图片

图 11. Tailor 裁剪流程

去除了无用信息

  • 跳过 header

  • 分 tag 裁剪

  • 裁剪无用信息:char[]; byte[]; timestamp; stack trace serial number; class serial number;

  • 压缩数据信息

同时对数据进行 zlib 压缩,在 server 端数据还原,整体裁剪效果:180M—>50M---->13M

优化实践

=======================================================================

内存泄漏


除了通过后台根据 GCROOT+ 引用链自动分配研发跟进解决我们常见的内存泄漏外,我们还对系统导致一些内存泄漏进行了分析和修复。

系统异步 UI 泄漏

根据上传聚合的引用链我们发现在 Android 6.0 以下有一个 HandlerThread 作为 GCROOT 持有大量 Activity 导致内存泄漏,根据引用发现这些泄漏的 Activity 都被一个 Runnable(这里是 Runnable 是一个系统事件 SendViewStateChangedAccessibilityEvent)持有,这些 Runnable 被添加到一个 RunQueuel 中,这个队列本身被 TheadLocal 持有。

图片

图 12. HandlerThread 泄露链路

我们从 SendViewStateChangedAccessibilityEvent 入手对源码进行了分析发现它在 notifyViewAccessibilityStateChangedIfNeeded 中被抛出,系统的大量 view 都会在自身的一些 UI 方法(eg: setChecked)中触发该函数。

图片

SendViewStateChangedAccessibilityEvent 的 runOrPost 方法会走到我们常用的 View 的 postDelay 方法中,这个方法在当 view 还未被 attched 到根 view 的时候会加入到一个 runQueue 中。

图片

这个 runQueue 会在主线程下一次的 performTraversals() 中消费掉。

图片

如果这个 runQueue 不在主线程那就没有消费的机会。

根据上面的分析发现造成这种内存泄漏需要满足一些条件:

  1. view 调用了 postDelay 方法 (这里是 notifyViewAccessisbilityStateChangeIfNeeded 触发)

  2. view 处于 detached 状态

  3. 上述过程是在非主线程里面操作的,ThreadLocal 非 UIThread,持有的 runQueue 不会走 performTraversals 消费掉。

抖音这边大量使用了异步 UI 框架来优化渲染性能,框架内部由一个 HandlerThread 驱动,完全符合上述条件。针对该问题,我们通过反射获取非主线程的 ThreadLocal,在每次异步渲染完主动清理内部的 RunQueue。

图片

图 13. 反射清理流程

另外,Google 在 6.0 上也修复了 notifyViewAccessisbilityStateChangeIfNeeded 的判断不严谨问题。

图片

内存泄漏兜底

大量的内存泄漏,如果我们都靠推进研发解决,经常会出现生产大于消费的情况,针对这些未被消费的内存泄漏我们在客户端做了监控和止损,将 onDestory 的 Activity 添加到 WeakRerefrence 中,延迟 60s 监控是否回收,未回收则主动释放泄漏的 Activity 持有的 ViewTree 的背景图和 ImageView 图片。

大对象


主要对三种类型的大对象进行优化

  • 全局缓存:针对全局缓存我们按需释放和降级了不需要的缓存,尽量使用弱引用代替强引用关系,比如针对频繁泄漏的 EventBus 我们将内部的订阅者关系改为弱引用解决了大量的 EventBus 泄漏。

  • 系统大对象:系统大对象如 PreloadDrawable、JarFile 我们通过源码分析确定主动释放并不干扰原有逻辑,在启动完成或在内存触顶时主动反射释放。

  • 动画:用原生动画代替了内存占用较大的帧动画,并对 Lottie 动画泄漏做了手动释放。

图片

图 14. 大对象优化点

小对象


小对象优化我们集中在字段优化、业务优化、缓存优化三个纬度,不同的纬度有不同的优化策略。

图片

图 15. 小对象优化思路

通用类优化

在抖音的业务中,视频是最核心且通用的 Model,抖音业务层的数据存储分散在各个业务维护了各自视频的 Model,Model 本身由于聚合了各个业务需要的属性很多导致单个实例内存占用就不低,随着用户使用过程实例增长内存占用越来越大。对 Model 本身我们可以从属性优化和拆分这两种思路来优化。

  • 字段优化:针对一次性的属性字段,在使用完之后及时清理掉缓存,比如在视频 Model 内部存在一个 Json 对象,在反序列完成之后 Json 对象就没有使用价值了,可以及时清理。

  • 类拆分:针对通用 Model 冗杂过多的业务属性,尝试对 Model 本身进行治理,将各个业务线需要用到的属性进行梳理,将 Model 拆分成多个业务 Model 和一个通用 Model,采用组合的方式让各个业务线最小化依赖自己的业务 Model,减少大杂烩 Model 不必要的内存浪费。

业务优化

  • 按需加载:抖音这边 IM 会全局保存会话,App 启动时会一次性 Load 所有会话,当用户的会话过多时相应全局占用的内存就会较大,为了解决该问题,会话列表分两次加载,首次只加载一定数量到内存,需要时再加载全部。

  • 内存缓存限制或清理:首页推荐列表的每一次 Loadmore 操作,都不会清理之前缓存起来的视频对象,导致用户长时间停留在推荐 Feed 时,缓存起来的视频对象过多会导致内存方面的压力。在通过实验验证不会对业务产生负面影响情况下对首页的缓存进行了一定数量的限制来减小内存压力。

缓存优化

上面提到的视频 Model,抖音最早使用 Manager 来管理通用的视频实例。Manager 使用 HashMap 存储了所有的视频对象,最初的方案里面没有对内存大小进行限制且没有清除逻辑,随着使用时间的增加而不断膨胀,最终出现 OOM 异常。为了解决视频 Model 无限膨胀的问题设计了一套缓存框架主要流程如下:

图片

图 16. 视频缓存框架

使用 LRU 缓存机制来缓存视频对象。在内存中缓存最近使用的 100 个视频对象,当视频对象从内存缓存中移除时,将其缓存至磁盘中。在获取视频对象时,首先从内存中获取,若内存中没有缓存该对象,则从磁盘缓存中获取。在退出 App 时,清除 Manager 的磁盘缓存,避免磁盘空间占用不断增长。

图片


关于图片优化,我们主要从图片库的管理和图片本身优化两个方面思考。同时对不合理的图片使用也做了兜底和监控。

图片库

针对应用内图片的使用状况对图片库设置了合理的缓存,同时在应用 or 系统内存吃紧的情况下主动释放图片缓存。

图片自身优化

我们知道图片内存大小公式 = 图片分辨率 * 每个像素点的大小

图片分辨率我们通过设置合理的采样来减少不必要的像素浪费。

//开启采样ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)    .setDownsampleEnabled(true)    .build();Fresco.initialize(context, config);//请求图片时,传入resize的大小,一般直接取View的宽高ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)    .setResizeOptions(new ResizeOptions(50, 50))    .build();mSimpleDraweeView.setController(    Fresco.newDraweeControllerBuilder()        .setOldController(mSimpleDraweeView.getController())        .setImageRequest(request)        .build());

而单个像素大小,我们通过替换系统 drawable 默认色彩通道,将部分没有透明通道的图片格式由 ARGB_8888 替换为 RGB565,在图片质量上的损失几乎肉眼不可见,而在内存上可以直接节省一半。

图片兜底

针对因 activity、fragment 泄漏导致的图片泄漏,我们在 onDetachedFromWindow 时机进行了监控和兜底,具体流程如下:

图片

图 17. 图片兜底流程

图片监控

关于对不合理的大图 or 图片使用我们在字节码层面进行了拦截和监控,在原生 Bitmap or 图片库创建时机记录图片信息,对不合理的大图进行上报;另外在 ImageView 的设置过程中针对 Bitmap 远超过 view 本身超过大小的场景也进行了记录和上报。

图片

图 18. 图片字节码监控方案

更多思考

=======================================================================

是不是解决了 OOM 内存问题就告一段落了呢?作为一只追求极致的团队,我们除了解决静态的内存占用外也自研了 Kenzo(Memory Insight)工具尝试解决动态内存分配造成的 GC 卡顿。

Kenzo 原理


Kenzo 采用 JVMTI 完成对内存监控工作,JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口。JVMTI 开发时,应用建立一个 Agent 使用 JVMTI,可以使用 JVMTI 函数,设置回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的业务判断。

图片

图 19. Agent 时序图

Jvmti SetEventCallbacks 方法可以设置目标虚拟机内部事件回调,可以根据 jvmtiCapabilities 支持的能力和我们关注的事件来定义需要 hook 的事件。

Kenzo 采用 Jvmti 完成如下事件回调:

  • 类加载准备事件 -> 监控类加载

  • ClassPrepare:某个类的准备阶段完成。

  • GC -> 监控 GC 事件与时间

  • GarbageCollectionStart:GC 启动时。

  • GarbageCollectionFinish:GC 结束后。

  • 对象事件 -> 监控内存分配

  • ObjectFree:GC 释放一个对象时。

  • VMObjectAlloc:虚拟机分配一个对象的时候。

框架设计

Kenzo 整体分为两个部分:

生产端

  • 采集内存数据

  • 以 sdk 形式集成到宿主 App

消费端

  • 处理生产端的数据

  • 输入 Kenzo 监控的内存数据

  • 输出可视化报表

图片

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

文末

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

小编将自己6年以来的面试经验和学习笔记都整理成了一个**937页的PDF,**以及我学习进阶过程中看过的一些优质视频教程。

其实看到身边很多朋友抱怨自己的工资很低,包括笔者也是一样的,其原因是在面试过程中没有给面试官一个很好的答案。所以笔者会持续更新面试过程中遇到的问题,也希望大家和笔者一起进步,一起学习。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

d开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

[外链图片转存中…(img-FrRidzp1-1713759741168)]

文末

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

小编将自己6年以来的面试经验和学习笔记都整理成了一个**937页的PDF,**以及我学习进阶过程中看过的一些优质视频教程。

[外链图片转存中…(img-UoqCTwhb-1713759741169)]

其实看到身边很多朋友抱怨自己的工资很低,包括笔者也是一样的,其原因是在面试过程中没有给面试官一个很好的答案。所以笔者会持续更新面试过程中遇到的问题,也希望大家和笔者一起进步,一起学习。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android系统性能优化对于解决卡顿、提升稳定性和优化续航方面起着重要的作用。 首先,在解决卡顿问题上,开发人员需要关注应用程序的UI线程。为了确保应用程序的流畅运行,可以采用以下优化措施:优化布局文件,减少层级嵌套;使用异步加载图片,避免在主线程中进行网络请求等耗时操作;合理利用缓存机制,避免重复加载数据。此外,还可以针对卡顿问题进行性能分析,通过工具查找耗时操作,并进行相应的优化。 其次,在提高系统稳定性方面,开发人员需要考虑异常崩溃的处理和内存管理。异常崩溃处理可通过捕获并记录崩溃异常来及时解决问题和改进代码。内存管理方面,应避免内存泄漏和过度分配内存,使用系统提供的工具来进行内存管理和优化。 最后,在续航优化上,需要考虑电源管理和资源使用的合理分配。通过使用省电模式、灵活控制后台任务和限制应用程序在后台运行等方式,最大程度地延长设备的电池寿命。另外,合理管理资源,避免过度使用CPU、网络和图形渲染等资源,有助于降低能耗并优化系统续航。 总之,Android系统性能优化是一个综合性的工作,需要开发人员关注卡顿问题、提升稳定性和优化续航方面的问题。通过合理使用工具和采取相应的优化措施,可以实现系统性能的有效提升。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值