抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 方案

动手点关注 干货不迷路 👆

一、背景和目标

背景

作为 Android 开发者,相信大家都碰到过 Java OOM 问题,导致 OOM 的原因可能是应用存在内存泄漏,也可能是因为手机的 heapsize 比较小不能满足复杂应用对内存资源的大量需求。对于 Java 内存泄漏治理,业界已经有比较成熟的方案,这里不做介绍,本文主要针对第二点尝试进行分析和优化。

举个例子:我们在监控平台查看稳定性数据,发现 heapsize=256M 的设备发生的 OOM 崩溃最多,而 heapsize=512M 的设备很少发生 OOM 崩溃。且除此之外,还有一个特点:OOM 崩溃绝大多数发生在 Android 8.0 之前的设备。

对于这种 heapsize 较小难以满足业务复杂度的情况,可能有以下几种方式来解决:

1. 增加 heapsize

如果我们已经设置了 largeHeap,也就没有常规的提升 heapsize 的方式了;再想往前一步,可以尝试从虚拟机中突破这个限制,因为 heapsize 是虚拟机的配置,是否抛出 OOM 异常也是在虚拟机中决定的;修改虚拟机运行逻辑是有一定可能的,但是其难度和可行性与想要修改的内容相关性较大,修改方案的稳定性也需要非常深厚的功力才能保证,而如果运气不好,找不到好的切入点,甚至从理论上都无法保证其稳定性,那么达到上线的难度就更大了,本文不在这个方向深入。

2. 降低业务复杂度,裁剪应用功能

这个方案也不在我们的考虑范围之内,实际上很多应用都有推出极速版,但是功能都会有所裁剪,对于使用常规版本的用户,我们也不能推送极速版,因为使用体验会有很大变化。

3. 分析 Java Heap 里的内容都是什么,尝试发现主要矛盾进行优化,对症下药

实际上本文就是从这个方向经过调查后,找到了一个相对稳定的突破口。下面是结合 OOM 堆栈、android 版本、heapsize 维度对 OOM 整体概况的一个分析:

- 最常见 OOM 堆栈

ae4ec0d157027982ec81395d3dbe1a6c.png

出现最多的堆栈就是 Bitmap 创建时内存不足从而 OOM 崩溃,那么是不是已使用的内存大多都是 Bitmap 呢 ?不能 100%确定,因为直接触发 OOM 崩溃的原因是最后一次内存分配失败,而真正的原因是 OOM 之前的内存分配;但是仍然有一定可能性,因为总是出现同一个堆栈可能并不是巧合,可以在一定程度上说明这个堆栈执行的比较频繁,而且 Bitmap 一般占用内存较大。

这里先做一个不 100%确认的初步推断:OOM 时 Java heap 中占用内存较多的对象是 Bitmap。

- OOM 在不同 android 版本、heapsize 上的表现

继续对 OOM 数据做总结后发现了 OOM 的分布规律如下图:

7534e2114781e95435e3417e2be5b7fd.png

上图红色地雷代表 OOM,横坐标是 android 版本,纵坐标是 heapsize,原点是:(android8.0, 384M);可以看到:

  1. 第一、四象限,OOM 最少;对应 android 高版本,大 heapsize 和小 heapsize 都有

  2. 第二象限有一定 OOM;对应 android 低版本,大 heapsize

  3. 第三象限 OOM 最多;对应 android 低版本,小 heapsize

简单总结就是:

  1. heapsize 越大越不容易 OOM

  2. Android8.0 及之后的版本更不容易 OOM

第四象限的数据说明,即便在 heapsize 较小的情况下,在 android 8.0 之后的版本上也不容易发生 OOM,结合上面的初步推断信息“OOM 时 Java heap 中占用内存较多的对象是 Bitmap”,很容易想到,应该是 Bitmap 在 android 8.0 前后的实现变化导致了当前的 OOM 分布现象:

Bitmap 变化:

  • 在 Android 8.0 之前,Bitmap 像素占用的内存是在 Java heap 中分配的

  • Android 8.0 及之后,Bitmap 像素占用的内存分配到了 Native Heap

由于 Native heap 的内存分配上限很大,32 位应用的可用内存在 3~4G,64 位上更大,虚拟内存几乎很难耗尽,所以在前面的推测 “OOM 时 Java heap 中占用内存较多的对象是 Bitmap” 成立的情况下,应用更不容易 OOM。

而第三象限数据,则进一步佐证了前面的推测,Android 8.0 之前,Bitmap 像素内存在 Java heap 中分配时,即便 heap size 大到 512M,OOM 发生也比较多。

至此,得到了确定的结论:

  1. OOM 的分布主要在 Android 8.0 之前 heap size 较小的设备

  2. OOM 时 Java heap 中占用内存较多的是 Bitmap(确切的说是 Bitmap 的像素数据),当 Bitmap 像素占用内存在 Native Heap 分配时,即便 heap size 很小,应用也不容易 OOM

目标

根据上述结论,目标也就比较清晰了:

使 Android 8.0 之前 Bitmap 的像素内存也从 Native 层分配,从而减少 Java OOM 崩溃。

二、Bitmap 使用分析和方案调查

想要使得 Android 8.0 之前的设备 Bitmap 像素内存也分配在 Native heap,需要先把 Bitmap 的创建流程调查清楚。

Bitmap 创建流程

如下堆栈描述了 Bitmap 的创建:

61a4aa4ef3bd840dfd1bda6e11d55e16.png

Bitmap 的构造方法是不公开的,在使用 Bitmap 的时候,一般都是通过 Bitmap、BitmapFactory 提供的静态方法来创建 Bitmap 实例。下图中以 Bitmap.createBitmap 说明了 Bitmap 对象的主要创建过程:

580f0fb45f9494799e3c94fbec6c7ed8.png

从上图可以看到 Java Bitmap 对象是在 Native 层通过 NewObject 创建的。图中的两个函数:

  1. allocateJavaPixelRef,是 8.0 之前版本为 Bitmap 像素从 Java heap 申请内存

  2. allocateHeapBitmap,是 8.0 版本为 Bitmap 像素从 Native heap 申请内存

allocateJavaPixelRef 函数的实现
6b01db920ce361c937bb2c91b9f4c62a.png

allocateJavaPixelRef 通过 newNonMovableArray 从 Java 堆上为 Bitmap 像素分配内存,然后再构造 Native Bitmap 对象,对应的构造函数如下:

2f78d4941c8e0a8d666fa37803beaa1d.png

构造函数中发现 Native Bitmap 构造时对应的 mPixelStorageType 是 PixelStorageType::Java,表示 Bitmap 的像素是保存在 Java 堆上,所以尝试看下 PixelStorageType 总共有几种,是否可能有把 pixels 数据存储到 Native 层。查找代码发现 PixelStorageType 只有三类,如下:

4dad692199724f88fdfaa544652af0b3.png

这个信息可以作为一个切入点,在后面进行深入调查。

allocateHeapBitmap 实现
82ec1afa87e40e21eb83902d05d793bc.png

allocateHeapBitmap 主要是通过 calloc 为 Bitmap 的像素分配内存,这个分配就在 Native 堆上了。

通过初步的分析,初步有两个思路可以先进行尝试:

  1. 在创建 Bitmap 时,把对 allocateJavaPixelRef 的调用替换为调用 allocateHeapBitmap 来达到从 Native 层分配内存的目的

  2. 调查 PixelStorageType 共有哪些种类,是否可能从当前的保存到 Java 堆切换为保存到 Native 堆

思路 1:allocateJavaPixelRef 替换为 allocateHeapBitmap

这个思路看起来想要实现目标,做一下替换就可以了,但实际上没有这么简单,存在的问题如下:

  1. allocateHeapBitmap 返回的是 skBitmap,allocateJavaPixelRef 返回的是 android::Bitmap,类型并不匹配

  2. 并不是简单的插拔就可以把 allocateJavaPixelRef 替换为 allocateHeapBitmap,8.0 之前的 Android 版本上没有 allocateHeapBitmap 的实现。如果想要为 8.0 之前的系统写一个全新的实现,只是参数的获取就需要做很多适配,比如无法直接使用 skia 中的 SkBitmap、SkColorTab、SkImageInfo,就没有办法动态获取到要分配的内存 size

  3. Bitmap 内存的申请和释放要有匹配的逻辑和合适的时机

所以这个思路基本可以断定不可行。

思路 2:allocateJavaPixelRef 替换为 allocateAshmemPixelRef

前面的调查发现 PixelStorageType 只有三类,如下:

9d58ff7d8d82cf189ab40d098b594850.png

其中 External 方式存储 Bitmap 像素,在源码中没有看到相关使用,无法参考;Java 类型就是默认的 Bitmap 创建方式,像素内存分配的 Java 堆上;Ashmem 方式存储 Bitmap 像素的方式在源码中有使用,主要是在跨进程 Bitmap 传递时使用,对应的场景主要是 Notification 和截图场景:

52f2ebfd8da7edccd91ebfb07f257b97.png

查看其实现:

552256dbe608498758014a93bc14ec7f.png 691f1db74f6144d9c6bd4800b113e82a.png 1b010631f472c88b25c842860bc35055.png

从代码中看到 allocateAshmemPixelRef 这个函数是通过 mmap ashmem 内存来创建 native Bitmap 对象,且参数、返回值都与 allocateJavaPixelRef 相同,所以使用 Ashmem 方式存储 Bitmap 像素看起来有一定可行性,只需把 allocateJavaPixelRef 的调用替换为 allocateAshmemPixelRef 即可达到从 Native 层为 Bitmap 像素分配内存的目的。

但经过详细的源码分析以及实际验证,其可行性仍然很低,主要原因如下:

  1. allocateAshmemPixelRef 实现只在 android 6.0 ~ android7.1 上存在,所以这个方案即便能够实现,也只能覆盖 android 6.0 ~ android 7.1

实际情况中,6.0 系统的 OOM 占了非常大一部分,如果这个方案可行,也可以解决一部分问题,所以不会因为这个原因阻碍对这种方案的尝试,还可以继续尝试

  1. ashmem 方式存储 Bitmap 像素,每个 Bitmap 需要对应一个 fd,应用的 Bitmap 使用数量是能够达到 1000+ 的,这样可能会导致 fd 资源使用耗尽,从而发生崩溃

这个问题基本是无解的,但如果方案可行,可以尝试只给一定数量的 Bitmap 使用 ashmem 方式申请像素内存,比如 500 个;所以方案还可以继续尝试

  1. 最终尝试后发现这种方式影响 Bitmap 正常功能(一些视频动图不能正常展示),经分析主要原因是使用 ashmem 申请的 Bitmap 无法进行 reconfigure :

d42390879153931f0a22f40619905546.png

上图 Bitmap 的 reconfigure 代码中可以看到没有 mBuffer 的 Bitmap 不支持 reconfigure,Ashmem 方式创建的 Bitmap 没有从 Java 堆申请 mBuffer,所以一定是不支持 reconfigure 的。当然到这里之后还没有完全堵死这个方式,还可以继续尝试在 ashmem 方式申请 Bitmap 时给其一个假的 mBuffer 来绕过这个限制,但接下来要做的调查和改动势必很大,因为 ashmem 方式申请 Bitmap 本身不支持 mBuffer 的管理,新创建的 buffer 就难以找到合适的时机进行释放。

结合上述 3 个点综合判断,这个方案限制比较多,也有一定风险,所以暂时将当前的方案暂时挂起,作为备用方案。

上述的两种思路不成功其实有一定的必然性,毕竟对应代码的设计并不是为了给我们取巧做切换用的。既然没有办法这么容易实现,就深入调查清楚为 Bitmap 从 Java 堆申请内存的流程和这个内存的使用流程,再尝试从这些流程中找到切入点进行修改。

思路 3:剖析 Java 堆分配 Bitmap 内存的过程,再尝试找到方案

Bitmap 内存申请

调查思路:

实际就是查找 hook 点的思路,先分析内存是如何分配的,分配出来的内存是如何使用的(主要指分配出内存后,指针或者对象的传递路径),尝试把从 Java 堆分配内存的关键点替换为使用 malloc/calloc 函数从 Native 堆上进行分配,并把分配出来的内存指针构造成原流程中使用的数据结构,并保证其能够正常运行。

Android 8.0 之前 Bitmap 内存申请和使用如下图:

830cc9513f0fae1340b24d55e7003dd0.png

上图为简化后的核心内存分配流程,框起来的部分就是为 Bitmap 从 Java heap 申请像素内存的代码。其中:

  • arrayObj 是通过 newNonMovableArray 从 java heap 分配出来的 byte array 对象

  • addr 是 arrayObj 对象存放 byte 元素的首地址

这里需要先说明一下 java byte array 的内存布局(对应代码在 ART 虚拟机中):

c4a919c46afda83f135da1ed19b609c3.png

前面的 8 个字节是 Object 成员,length_ 是这个数组的长度,first_element_ 数组用来实际存放 byte 数据,数组的长度由 length_/4 来决定。addressOf(arrayObj) 获取到的就是 first_element_地址;arrayObj 和 addr 的传递在上图已经用分别用绿色和红色虚线箭头标记出来了。

想要把 Bitmap 内存分配改为在 Native 层分配,就需要从分配这里入手, 所以必须要把 arrayObj 和 addr 使用梳理清晰,为后续替换和适配做好铺垫。arrayObj 和 addr 使用如下:

arrayObj 的使用

1. 在 Native 层使用,即在 android::Bitmap 对象中使用

  • 在创建 Bitmap 时,把 arrayObj 添加到 weak global ref tab 中,并通过 Bitmap 的 mPixelStorage.jweakRef 引用 arrayObj:

958e57378f5b5a6ba52e86c03fbffd4a.png
  • 在 Bitmap 的 pinPixelsLocked 中,把 arrayObj 添加到 global ref tab 中,并保存在 Bitmap 的 mPixelStorage.java.jstrongRef 中:

7db80fd5456c832e504308098de3d2ff.png
  • 在 Bitmap 的 unpinPixelsLocked 中,从 global ref tab 中删除对 arrayObj 的引用:

d51b3fe2344d3f5dfff8fb333d6bd3e1.png
  • 在 Bitmap 的 doFreePixel 中(即释放像素内存),删除 arrayObj 对应的 weak ref:

77db4c9c5058b5206c7d3b358d9a2cda.png
  • 通过 Bitmap 的成员函数 javaByteArray() 向外部提供引用,即 mPixelStorage.jstrongRef (只在创建 Java Bitmap 对象时传递为参数,赋值给 Bitmap 的 mBuffer 成员进行使用)

091b366abcaba23d63ec741c724871cf.png

2. 在 Java Bitmap 对象中引用,对应 Bitmap 的 mBuffer 成员

  • 在创建 Java Bitmap 时通过 nativeBitmap->javaByteArray()获取对 arrayObj 的引用,并赋值给 Java Bitmap 的 成员:private byte[] mBuffer;

c8d10be8ac587af3efb056edbc434ccc.png
  • 在 Bitmap.reconfigure 中,需要使用 arrayObj.length,在 Native 层会使用这个 length 判断当前的 Bitmap 能否满足 reconfigure 需求:

b35a18794791eea1272da8cdf555b1b7.png
  • 在 Bitmap.getAllocationByteCount()中通过 arrayObj.length 获取这个 Bitmap 的像素内存大小:

deb50f10e6d7f087e95e1d6c595c3d4f.png

小结:arrayObj 对象的引用只在 Bitmap native 对象和 Java 对象中,作用分别是用来管理 arrayObj 的生命周期以及使用它的 length 来获取 Bitmap 像素占用的内存大小。

addr 的使用

在为 Bitmap 分配 nonMovableArray 之后,通过 addr = addressOf(arrayObj)获取:

623737fa084cda721b716a4a9fd0782a.png

在创建 native bitmap 时,作为指针传递给其成员 mPixelRef:

f8ed58e076292163499748966ebb25f6.png 63a40eaefa265e41ed9b7420e7a81a1a.png

上述参数 mStorage 就是 addr,其关键使用点是在 WrappedPixelRef 的 onNewLockPixels 被调用时,赋值给 LockRec 的 fPixels 成员:

d108a5a37a5c574348a5ea358ea009a6.png

mPixelRef 会被设置给 skBitmap。

每个 nativeBitmap 对应一个 skia 的 skBitmap 对象,在创建 Bitmap 时会把 native bitmap 的成员 mPixelRef 设置给 skBitmap:

871a55d877ca8dcd92b79425bf83450d.png 2ae2f0f351b576952442fe8980258c6a.png

在 skia 中 SkBitmap 绘制 Bitmap 需要使用内存来处理 Bitmap 像素数据时,就会通过 mPixelRef->onNewLockPixels() 来获取存放 Bitmap 像素的内存地址,即 arrayObj 的元素地址 addr,其是作为指针类型数据来使用的。

小结:addr 指向的内存是在 java 堆上,其会在需要的时候被传递给 skia 用来处理 bitmap 像素数据。

Bitmap 内存使用总结:

存储 Bitmap 像素数据使用的内存是通过 NewNonMovableArray 从 Java heap 申请的 byte 数组 arrayObj,arrayObj 对象的引用只在 Bitmap native 对象和 Java 对象中,作用分别是用来管理 arrayObj 的生命周期以及使用它的 length 来获取 Bitmap 像素占用的内存大小。

skia 中并不会为 Bitmap 的像素数据分配内存,它把 Java heap 上 byte 数组的元素首地址转换为 void* 来使用;也就是说在当前实现中,Bitmap 像素内存不一定非得是在 Java heap 上分配,我们可以 malloc 一块内存传递给 skia 使用,并不需要再给 skia 做任何适配。

有了上面这些信息,把 android 8.0 之前的 Bitmap 像素内存改到在 Native 层分配目标就看到了希望,因为不需要在 skia 层适配,可以降低一定难度。

尝试从 native 层申请 Bitmap 内存

根据上面的分析,只需要找好 hook 的切入点,并完成 3 个关键点的替换即可,如下图:

53ed56a3c9d067fba38ae662da8a53d4.png
  1. 目标是不再从 java heap 给 Bitmap 分配内存,这一步的 byte[] 申请必然是需要去掉的

  2. 这里通过 malloc 分配内存,交给 PixelRef 引用,间接的就可以被 SkBitmap 使用了

  3. 原有实现中 Java Bitmap 通过 mBuffer 成员引用 byte[],主要用来通过 mBuffer.length 获取图片大小

上述 3 个关键点中,前两个点比较好实现,都是 native 层的代码,hook 点也比较好找,这里不再赘述。而第 3 个点需要特殊处理,因为 Java 层 Bitmap 通过 mBuffer.length 获取 Bitmap size,目前没有稳定的 Java hook 方案,且我们又不能真的给它一个长度为 Bitmap size 大小的 byte[](那样就又从 Java 堆上进行 Bitmap 的内存分配了),所以只能给个假的。

那么如何构造一个假的 byte array ?前面分析过 java byte array 的内存布局:

72ac8749d120f7dfbced0b54b257bda0.png

实际上 array.length 的就是 array 对象的 length_ 值,而虚拟机又提供了 addressOf 来获取一个 array 的首元素地址,也即 first_element_ 地址,所以可以尝试通过 first_element_ 来定位 length_ 的位置,进行修改即可。

这样就可以在 java heap 上申请一个比较小的 byte array,并把它的长度伪造成与 Bitmap size 相等。申请的这个小 size 的 byte array 本身占用的内存就作为 Bitmap 内存转移到 Native 层的代价。

这种方式看起来好像不太稳定,但是可以通过校验来保证,比如我们在执行方案之前先尝试伪造一个 byte array 来进行验证,如下代码就是申请了 1 字节长度的 byte array,把它的长度伪造成 36,然后进行校验,校验失败则不再执行 NativeBitmap 方案。

b76845570d4d8828e30c8cd82a36c5bc.png

至此,Bitmap 内存申请从 Java heap 转移到 native heap 所需要解决的关键问题都解决了,离最终的目标还有 50% 的距离。接下来需要完成 malloc 出来的 Bitmap 内存的释放逻辑。

Bitmap 内存释放
原生释放逻辑

原生 Bitmap 的像素内存存放在 byte array (mBuffer)中,Bitmap 的内存释放流程就对应于 mBuffer 对象的释放,这个释放流程在 android 5.x ~7.x 大体相同,只有细微差别,下述以 android 6.0 代码为例进行说明。Bitmap 像素内存释放主要有两种方式触发:一种是 Java Bitmap 对象不再被引用后,GC 回收 Java Bitmap 对象时析构 Native Bitmap ,从而释放 Bitmap 像素内存;一种是主动调用 Bitmap.recycle() 来触发 Bitmap 像素内存的释放:

2e4b043b69348d3ea40b9b5e02c36f87.png

这个 mBuffer 是在 Native 层申请的 Java 对象,主要在两个地方引用:

  1. Native 层通过 NewWeakGlobalRef(arrayObj) 把它添加到 Weak Global Reference table 中进行引用

  2. Java 层 Bitmap 通过 mBuffer 来引用,实际是在 Native 层通过 NewGlobalRef(arrayObj) 把它添加到了 Global Ref table 中,即 mBuffer 是一个关联到 Java byte array 的 Global ref

而这两个引用的释放顺序是先通过 DeleteGlobalRef 删除全局强引用(Skia 中不再使用这个 Bitmap 时会触发强引用删除),再通过 DeleteWeakGlobalRef 来删除全局弱引用,最终这个 byte array 对象被 GC 回收。

但实际运行过程中不完全是这样的顺序,mBuffer 的回收必然是在 DeleteGlobalRef 之后,但却不一定是在 DeleteWeakGlobalRef 之后,因为一旦 bytearray 只被 Weak glabal ref table 引用时,只要发生 GC,就会把它回收掉。

新的释放逻辑

原生的 Bitmap 像素内存释放是通过回收 mBuffer 引用的 byte array,而 NativeBitmap 方案将像素内存转移到 Native 内存之后,存在两份内存需要被释放:

  1. 给 Java Bitmap 使用的小 size 的 byte array 对象,这个对象仍然按照原生逻辑释放,无需再做其他变动

  2. malloc 出来的用以存放 bitmap 像素数据的内存,在 byte array 释放时进行 free,相当于附着于原生的内存释放逻辑,从而不会影响 Bitmap 的生命周期

实现释放有两个关键点:

  1. malloc 出来的指针需要与 mBuffer 关联,这样才能在 mBuffer 释放时找到对应的内存进行释放

解决方式:由于此时的 mBuffer 是伪造的 byte array,可以把 malloc 出来的 bitmap 指针保存在 byte array 中,当 byte array 被释放时,先从中取出 bitmap 指针进行 free,再进行 byte array 释放即可

  1. 需要使 mBuffer 的释放逻辑固定,这样便于确认 hook 点,原生的 mBuffer 释放逻辑是在 DeleteGlobalRef 之后的首次 GC 时,比较难以操作

解决方式:给 mBuffer 额外添加一个引用,放到 Global Reference Table 中,保证 mBuffer 不被提前释放,从而保证 mBuffer 的释放时机稳定保持在 Bitmap::doFreePixels() 中的 DeleteWeakGlobalRef(mBuffer) 位置,在这里从 mBuffer 中取出 malloc 出的 bitmap 指针执行 free,然后再依次删除给 mBuffer 额外添加的 Global Reference 和 Weak global ref。

新的释放逻辑与原生释放逻辑变化不大,如下图,主要是固定了 mBuffer 的释放时机在 DeleteWeakGlobalRef(mBuffer) 时,以及在此时释放 malloc 出来的 bitmap 内存:

ec4754b02e67af08944b45383cabe150.png

至此,malloc 出来的内存也能够找到合适的时机进行释放,把 Bitmap 的像素内存从 Java heap 转移到 Native heap 上的方案理论上完全可以实现,且需要的改动不大,只需要在原生 Bitmap 的创建流程和释放流程中做好修改即可。

三、实现方案

根据上述思路 3 的方案,最终实现如下:

Bitmap 创建改造

改造前 Bitmap 的创建和内存申请流程:
48103b54566737fbb8fed7b0505d0b6d.png
改造后 Bitmap 的创建和内存申请流程:
562d173e81badd2055c98db4c8aa7cd1.png

改造后在 Bitmap 创建过程中做了两个 hook,对应上图中两条紫色箭头指向的代码:

1. hook newNonMovableArray 函数

当为一个 Bitmap 在 java 堆上通过 newNonMovableArray 申请一个 bitmapSize 大小的 byte array 时,通过代理改造,实际只申请大小为 (sizeof(int) + sizeof(jobject) + sizeof(void*)) 的 byte array(32 位上大小为 12 字节,64 位上为 16 字节)。

修改这个 byte array 的 size 为 bitmapSize,以供 Java 层 Bitmap 使用它获取 bitmap 的真实 size。

在 byte array 的 element 首地址开始的前 4 个字节保存 0x13572468 作为 magic number,用以判断这是一个改造之后的 byte array。

通过 NewGlobalRef(fakeArrayObj) 把这个 byte array 对象添加到 Global Ref table 中,以保证 byte array 的释放时机一定是在 DeleteWeakGlobalRef 之后,并保存到 byte array 中,以便后续释放时使用;实际创建的 array 内存布局如下,这个 array 称为 fakeArray。

a957c1a8f089cdbaddd0116e7c9395fc.png

这个 array 的实际 length 是 12 字节(32 位),此时 1~4 字节存放 magic:0x13572468,5~8 字节存放 globalRef,9~12 字节暂时没有存放数据

2. hook addressOf 函数

在 addressOf 的代理函数中根据前 4 个字节数据是否是 magic number 来判断传入进来的 array 是否是被改造的 array,如果不是则调用原函数进行返回,如果是则继续进行下述步骤;

  • 从 array 中获取 bitmapSize,并通过 calloc(bitmapSize,1) 在 Native 堆上为 Bitmap 分配内存;

  • 把分配出来的 bitmap 指针保存到 fakeArray 的 9-12 字节中;

  • 把 bitmap 指针返回,由原生逻辑在后续传递给 skia 使用;

此时 fake array 中存放数据如下:

4ec03dc58dd557f9b7f900c8845ba7b3.png

在后面释放 Bitmap 相关内存时会使用到 byte array 中填充的这些数据。

在前面提到过申请的 fakeArray 本身占用的内存就作为 Bitmap 内存转移到 Native 层的代价,到这里及可以计算一出 Bitmap 被转移到 Native 层需要付出的内存代价是多少 ?

答案是:在 32 位上是 12 字节,在 64 位上是 16 字节,多使用的内存就是 fakeArray 中 0x13572468,globalRef,bitmap 这三个数据占用的内存。一个进程如果使用 1000 个 Bitmap,最多额外占用 16* 1000 = 15KB+,是能够被接受的。

Bitmap 释放改造

前述 Bitmap 创建过程的改造已经保证了 Bitmap 成员 mBuffer 的释放一定是在 Bitmap::doFreePixels() 的 DeleteWeakGlobalRef 之后了,所以只需要按照之前思路 hook DeleteWeakGlobalRef 函数即可:

8f1db15aad5102cac7e3be2407aafa11.png

上图中虚线上方为原生的释放流程,虚线下方是在原生流程上新添加的释放流程。其中右侧的代码就是新的逻辑下对 Bitmap 像素数据和辅助数据释放的关键代码。释放逻辑已经在第二大节中的 [新的释放逻辑] 中说明,这里不再复述。

上述对 Bitmap 创建和释放流程的改造即可实现从 Native heap 给 Bitmap 申请像素内存,但这样的改造必然会影响原有的 java heap GC 的发生,因为 Bitmap 使用的像素内存被转移到了 Native 层,Java heap 内存的压力会变小,但 Native heap 内存的压力会变大,需要有对应的 GC 触发逻辑来回收 Java Bitmap 对象,从而回收其对应的 Native 层像素内存。

这种情况可以通过在 native 内存申请和释放时通知到虚拟机,由虚拟机来判断是否达到 GC 条件,来进行 GC 的触发。实际上 android 8.0 之后 Bitmap 内存申请和释放就是使用的这个方式。

对应的代码在 VMRuntime 中实现:

a4ff456dcd027766a0338351e8cda0c6.png

只需要在给 Bitmap 申请内存时调用 registerNativeAllocation(bitmapSize),在释放 Bitmap 内存时调用 registerNativeFree(bitmapSize)即可。

兼容性:android 5.1.x ~ 7.x

目前该方案支持到 android 5.1.x ~ 7.x 的系统。4.x~5.0 的系统较早,实现差异较大,待后续完善。

四、线下验证和线上效果

线下验证

使用一台 android 6.0 的手机机型验证,java heapsize 是 128M。

测试代码

在测试代码中尝试把一个 bitmap 缓存 5001 次:

private static ArrayList<Bitmap> sBitmapCache = new ArrayList<>();
void testNativeBitmap(Context context) {
    NativeBitmap.enable(context);
    for (int i = 0; i <= 5000; i++) {
        Bitmap bt = BitmapFactory.decodeResource(context.getResources(),R.drawable.icon);
        if (i%100 == 0) {
            Log.e("hanli", "loadbitmaps: " + i);
        }
        sBitmapCache.add(bt);
    }
}
原生流程,只能加载 1400+个 Bitmap

在不开启 NativeBitmap 时,load 1400+ 张图片后,应用的 Java 堆内存耗尽,发生 OOM 崩溃:

17979 18016 E hanli: loadbitmaps: 0
17979 18016 E hanli: loadbitmaps: 100
...
17979 18016 E hanli: loadbitmaps: 1300
17979 18016 E hanli: loadbitmaps: 1400
17979 18016 I art   : Alloc concurrent mark sweep GC freed 7(208B) AllocSpace objects, 0(0B) LOS objects, 0% free, 127MB/128MB, paused 280us total 15.421ms
17979 18016 W art   : Throwing OutOfMemoryError "Failed to allocate a 82956 byte allocation with 7560 free bytes and 7KB until OOM"
打开 NativeBtimap

完成加载 5001 个 Bitmap,并且应用仍能够正常使用:

17516 17553 D hanli: NativeBitmap enabled.
17516 17553 E hanli: loadbitmaps: 0
17516 17553 E hanli: loadbitmaps: 100
...
17516 17553 E hanli: loadbitmaps: 4800
17516 17553 E hanli: loadbitmaps: 4900
17516 17553 E hanli: loadbitmaps: 5000

线上效果:发生 Java OOM 的用户数量降低 50%+

产品 1

针对 heapsize 为 256M 及以下的设备启用,当 Java heap 使用率达到 heapsize 的 70% 之后开始打开 NativeBitmap,Java OOM 崩溃影响用户数-56.4785%,OOM 次数降低 72%。

产品 2

针对 heapsize 为 384M 及以下的设备启用,当 Java heap 使用率达到 heapsize 的 80% 之后开始打开 NativeBitmap,Java OOM 崩溃影响用户数降低 63.063%,OOM 次数降低 76%。

在使用中我们对 NativeBitmap 方案的使用做了限制,因为 Bitmap 内存转移到 Native 层之后会占用虚拟内存,而 32 位设备的虚拟内存可用上限为 3G~4G,为了减少对虚拟内存的使用,只在 heap size 较小的机型才开启 NativeBitmap。我们在持续的优化中发现 Android 5.1.x ~ 7.1.x 版本上,已经有很多设备是 64 位的,所以当用户安装了 64 位的产品时,就可以在 heap size 较大的机型上也开启 NativeBitmap,因为此时的虚拟内存基本无法耗尽。在 64 位产品上把开启 NativeBitmap 的 heap size 限制提升到 512M 之后,Java OOM 数据在优化的基础上又降低了 72%。

五、两点说明

有两个问题做一下说明:

  1. 是否使用了 NativeBitmap 就一定不会发生 Java OOM 了?

答:并不是,NativeBitmap 只是把应用内存使用的大头(即 Bitmap 的像素占用的内存)转移到 Native 堆,如果其他的 Java 对象使用不合理占用较多内存,仍然会发生 Java OOM

  1. 方案可能产生的影响?

Bitmap 的像素占用的内存转移到 Native 堆之后,会使得虚拟内存使用增多,当存在泄漏时,可能会导致 32 位应用的虚拟内存被耗尽(实际上这个表现和 Android8.0 之后系统的表现一致)。

所以,方案的目标实际是为了使老的 android 版本能够支持更复杂的应用设计,而不是为了解决内存泄漏。

六、加入我们

抖音 Android 基础技术团队是一个深度追求极致的团队,我们专注于性能、架构、包大小、稳定性、基础库、编译构建等方向的深耕,保障超大规模团队的研发效率和数亿用户的使用体验。目前北京、上海、杭州、深圳都有大量人才需要,欢迎有志之士与我们共同建设亿级用户全球化 APP!

可以点击阅读原文,进入字节跳动招聘官网查询「抖音基础技术 Android」相关职位,直接发送简历内推。也可以邮件联系:hanli.lee@bytedance.com 咨询相关信息!

往期阅读:

抖音 Android 性能优化系列:Java 内存优化篇

抖音 Android 性能优化系列:新一代全能型性能分析工具 Rhea

抖音 Android 性能优化系列:启动优化之理论和工具篇

抖音 Android 性能优化系列:启动优化实践

42d4285ad44c62661f6d3c6ca3297ad1.png  点击“阅读原文”了解更多岗位!

相关推荐

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言随着 Web 前端技术的不断发展,越来越多的新兴技术方案被引入到 Web 开发中,其中 Wasm 和 WebGL 作为前端领域的两大利器,为开发者带来了更多的可能性。本文将结合2024 年抖音欢笑中国年的部分项目,重点介绍如何利用 Wasm 和 WebGL 对目前流行的一些前端互动技术(比如 Lottie、渲染引擎、动画图片等)进行创新和实践,利用 Wasm 和 WebGL 等新技术方案的特性...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(四):渲染技术实践与探索

作者:陈瑞、欧阳浩铸、王武俊、倪梵云前言抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(三):编辑器技巧与实践

前言本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

本文基于24年抖音春节活动业务背景,介绍了字节跨端容器AnnieX在游戏互动套件上的探索,致力于提升容器在游戏互动场景的优化能力。业务背景AnnieX作为字节一方游戏统一容器,服务字节内部电商、直播、UG等跨端场景业务。在字节一方游戏互动场景,有大量的一方游戏业务对容器有特定的流量、端能力和游戏优化的诉求。因此我们不断深入互动游戏业务特点,为字节游戏提供完善游戏端能力和流量运营能力,同时提供游戏互...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。前言本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:跨端框架提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator 等渲染引擎的...

字节跳动技术团队官方博客 1万+

Monorepo 解决方案 — 基于 Bazel 的 Xcode 性能优化实践

背景介绍书接上回《Monorepo 解决方案 — Bazel 在头条 iOS 的实践》,在头条工程切换至 Bazel 构建系统后,为了支持用户使用 Xcode 开发的习惯,我们使用了开源项目 Tulsi 作为生成工具,用于将 Bazel 工程转换为 Xcode 工程。但是在使用的过程中,我们发现了一些问题,其中影响较大的是,Xcode 工程卡顿:对于头条这种大型项目来说,Xcode 卡顿一直是本地...

字节跳动技术团队官方博客 1万+

抖音 ANR 自动归因平台建设实践

背景介绍本文在 2024 年初最新一期『抖音客户端基础技术大揭秘』技术沙龙活动中已做过专题分享,本次将内容重新整理文章进行分享。公众号后台回复技术沙龙可查看沙龙回放及 PPT~抖音作为一个超大型的应用,我们在 ANR 问题治理上面临着很大的挑战。首先对于存量问题的优化,由于缺少有效的归因手段,一些长期的疑难问题一直难以突破解决,例如长期位于 Top 1 的 nativePollOnce 问题。同时...

字节跳动技术团队官方博客 2万+

CVPR 2024 | CAMixerSR 动态注意力分配的超分辨率加速框架

背景随着相关技术和应用的发展,比如超高清屏幕、虚拟现实(VR)等沉浸式体验的增加,用户对超高分辨率图像和视频的需求变得越来越强烈。在这些场景中,图像的质量和清晰度对于提供最佳的用户体验至关重要。超高分辨率不仅能提供更清晰、更真实的视觉效果,还能在一定程度上增强人们的互动和沉浸感,在一些VR场景中我们需要8K甚至16K的才可以满足需求。然而要生成或者处理这些超高分辨率的内容,对算力的要求也是与日增长...

字节跳动技术团队官方博客 1万+

CVPR 2024 | Modular Blind Video Quality Assessment:模块化无参视频质量评估

无参视频质量评估 (Blind Video Quality Assessment,BVQA) 在评估和改善各种视频平台并服务用户的观看体验方面发挥着关键作用。当前基于深度学习的模型主要以下采样/局部块采样的形式分析视频内容,而忽视了实际空域分辨率和时域帧率对视频质量的影响,随着高分辨率和高帧率视频投稿逐渐普及,特别是跨分辨率/帧率视频转码档位画质评估场景中,这种影响变得更加不可忽视。在本文中,我们...

字节跳动技术团队官方博客 1万+

2024 AI & 前端:回首展望,光芒未至,破晓之前!

前言回望 2023 年,ChatGPT 的突然爆火,让 AI 无疑成为最为值得注目的新兴领域之一,我们也一起见证了生成式 AI 的寒武纪大爆发。这一年来,国内外的生成式 AI 、大模型和相关产品以令人眼花缭乱的速度更新迭代,新的创业浪潮风起云涌。在这 AI 浪潮下,也让我们有了新的开发思考,探索着在各个环节中“前端 & AI”的应用场景。勇于探索的前端开发者们已经开始挥舞着 AI 的“魔法...

字节跳动技术团队官方博客 1万+

Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。一、Monorepo...

字节跳动技术团队官方博客 1万+

字节跳动基础架构SRE-Copilot获得2023 CCF国际AIOps挑战赛冠军

近日,2023 CCF国际AIOps挑战赛决赛暨“大模型时代的AIOps”研讨会在北京成功举办,活动吸引了来自互联网、运营商、科研院所、高校、软硬件厂商等领域多名专家学者参与,为智能运维的前沿学术研究、落地生产实践打开了新思路。决赛中,从初赛两百多支队伍中脱颖而出的十支入围队伍分别展示了各自的方案,并进行了现场答辩,评审专家从选题方向、创新性、实用性、完整度和实验复现结果等多角度进行了综合评定,最...

字节跳动技术团队官方博客 2万+

字节跳动百万级Metrics Agent性能优化的探索与实践

背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Ag...

字节跳动技术团队官方博客 1万+

西瓜视频RenderThread引起的闪退问题攻坚历程

背景影响西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了...

字节跳动技术团队官方博客 2万+

字节电商双11 大促容量保障是如何做的?

前言Rhino 简介Rhino是字节自研全链路容量评估产品,致力于构建完整的全链路容量评估解决方案(覆盖:容量预估->资源准备->数据准备->容量验证->监控->分析->决策->处理反馈);围绕容量在稳定性、成本、效率 三方面提供业务全方位基础支撑。Rhino 目前已经成为字节各业务容量评估主流解决方案,并且历年来在业务大型活动稳定性保障中(抖音春节项目、...

字节跳动技术团队官方博客 10万+

使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案

背景近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。初步分析多维特征我们以某发版期间数据为例进行分析:机型方面:比较分散,有聚集部分samsung sm-s9180 占比...

字节跳动技术团队官方博客 2万+

用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。此外还有基于 C ABI Calling Convent...

字节跳动技术团队官方博客 2万+

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。作为一款OLAP引擎,火山引擎云原生数据仓库ByteHouse源于开源ClickHouse,在字节跳动多年打磨下,提供更丰富的能力和更强性能,能为用户带来极速分析体验,支撑实时数据...

字节跳动技术团队官方博客 1万+

打造企业级智能问答系统的秘密:如何使用云数据库 PostgreSQL 版实现向量检索

本文就如何利用火山引擎云数据库 PostgreSQL 版和大语言模型技术(Large Language Model,简称 LLM),实现企业级智能交互式问答系统进行介绍。通过本文,你将会了解交互式问答系统的原理,学习 PostgreSQL 的向量化存储和检索技术,以及大语言模型交互技术等。背景在大数据的浪潮下,众多企业建立了自己的知识库,以便于信息检索和知识查询。然而,随着知识库内容的膨胀,传统的...

字节跳动技术团队官方博客 2万+

抖音大型直播的画质优化实践

面临挑战随着抖音内容生态的不断丰富,越来越多的大型赛事在抖音平台进行直播,世界杯/春晚/亚运会等各项赛事节目引来大量用户观看。卡塔尔世界杯期间,抖音提供的稳定高质直播画面为观众带来了完美的观赛体验,决赛的 PCU 高达 3700W+。不同赛事节目涉及链路众多,且不同赛事之间存在差异,如何保障各链路的画质稳定并进一步提升画质,是一个巨大的挑战。如何应对挑战?画质优化链路大型赛事直播涉及链路较长,不同...

字节跳动技术团队官方博客 2万+

抖音直播新一代BVC编码器正式亮相

面临挑战在直播行业发展如火如荼的今天,用户对视频体验的要求也水涨船高。视频基础体验的关键要素包括清晰度、流畅度、低延迟等,而这些要素的“第一性原理”,就是视频本身的编码效率,也就是压缩率。视频编码是整个技术体系的基座,编码效率的显著提升,能够在同等码率下极大提高画质,从而改善用户体验。视频编码效率的重要性不言而喻,但进一步地提升也并非易事,尤其在直播场景中,对编码速度、延迟、码率控制等方面都有很高...

字节跳动技术团队官方博客 7356

Go Metrics SDK Tag 校验性能优化实践

背景Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK,在字节内数十万服务中集成,应用广泛,因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例,讲述对打点 API 的 hot-path 优化的实践。用户在使用 SDK API 进行打点时,需要传入指标对应的 Tag:tags:=[]m.T{{Name:"foo...

字节跳动技术团队官方博客 1万+

云上智能驾驶三维重建最佳实践

智能驾驶技术的不断发展,正在改变着我们的出行方式和交通系统。作为其中的一个关键技术,三维重建在智能驾驶系统中起着重要的作用。除去车端本身的感知、重建算法,自动驾驶技术的落地与发展需要庞大的云端重建能力支撑,火山引擎多媒体实验室通过行业领先的自研三维重建技术,结合强大的云平台资源与能力,助力相关技术在云端大规模重建、自动标注、真实感仿真等场景的落地与应用。本文重点介绍火山引擎多媒体实验室三维重建技术...

字节跳动技术团队官方博客 6305

火山引擎实时、低延时拥塞控制算法的优化实践

摘要火山引擎智能拥塞控制算法 VICC(Volcano Intelligent Congestion Control)是一种自适应的拥塞控制算法,旨在解决全球不同网络环境下,不同音视频应用对带宽利用率和延时的差异化要求。它结合了传统拥塞控制算法(如 GCC 和 BBR)的优点,并且能够根据不同的网络条件、业务偏好和码率特征进行自适应调整,包括自适应拥塞响应速度、自适应带宽探测幅度、自适应丢包检测策...

字节跳动技术团队官方博客 6735

veImageX 演进之路:Web 图片加载提速50%

背景说明火山引擎veImageX演进之路主要介绍了veImageX在字节内部从2012年随着字节成长过程中逐步演进的过程,演进中包括V1、V2、V3版本并最终面向行业输出;整个演进过程中包括服务端、客户端、网络库、业务场景与优化等多个角度介绍在图像处理压缩、省成本与体验优化的经验与方案;本篇文章重点介绍在web端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式...

字节跳动技术团队官方博客 6589

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路

作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方...

字节跳动技术团队官方博客 7818

如何利用播放器节省20%点播成本

点播成本节省的点其实涉及诸多部分,例如:CDN、转码、存储等,而利用播放器降本却是很多客户比较陌生的部分。火山引擎基于内部支撑抖音集团相关业务的实践,播放器恰恰是成本优化中最重要和最为依赖的部分。火山引擎的视频团队做了份数据统计,在一个很经典的视频业务中,我们在2022年至2023年大约1年半的时间里,针对这个业务进行了33次成本优化点,其中13次是播放器主导的优化,其余的有12次也是需要播放器强...

字节跳动技术团队官方博客 6176

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景ClickHouse是一个开源的OLAP引擎,不仅被全球开发者广泛使用,在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点,ClickHouse可以满足大规模数据的分析和查询需求,因此字节研发团队以开源ClickHouse为基础,推出火山引擎云原生数据仓库ByteHouse。在日常工作中,研发人员经常会遇到业务链路过长,导致流程稳定性和数据一致性难保障的问题,这在分布式、跨服务的场...

字节跳动技术团队官方博客 6900

抖音集团都在用的画质评估工具,确定不试试吗?

导读本文从抖音集团内部画质评估体系的建设历程着笔,主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路,希望能抛砖引玉,为遇到类似困扰的伙伴们提供有价值的参考。画质评估体系建设历程为何评测画质如此重要?我们通过线上业务大量实验发现,图片画质优劣对点击率、 停留时长等消费类指标有正相关影响,间接影响用户收益指标。因此,建设一套行...

字节跳动技术团队官方博客 7940

VLDB 2023 | CDSBen: 字节跳动 veDB 数据库存储系统性能测试模型

背景随着业务爆炸式增长与云原生技术的日渐成熟,大量云原生分布式数据库产品如雨后春笋般涌现,其中一部分主打 OLTP 场景的分布式数据库强调的是从计算-存储分离架构获得弹性收益;对于业界各种计算-存储分离架构的数据库而言,怎么用真实的端到端数据库 workload 去 benchmark 其底层存储系统一直存在以下难题:对于数据库专用存储系统,不存在如 fio 一样的“事实标准” benchmark...

字节跳动技术团队官方博客 5896
上一篇: 打造 Go 语言最快的排序算法
下一篇: 分析 Android 耗电原理后,飞书是这样做耗电治理的
 字节跳动技术团队
字节跳动技术团队 企业官方账号 企业官方账号
博客等级 码龄7年 8153粉丝 316原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值