Android BitmapFactory解码Bitmap中间过程产生内存占用量分布统计

发布时间:2026/7/12 18:17:40

Android BitmapFactory解码Bitmap中间过程产生内存占用量分布统计 Android BitmapFactory解码Bitmap中间过程产生内存占用量分布统计摘要BitmapFactory解码图片时除了最终生成的Bitmap像素内存外还会在native层分配大量临时内存包括输入流buffer、解码器对象、格式库buffer、颜色转换buffer等。这些临时内存在解码过程中形成峰值通常大于最终Bitmap大小。内存会统计到NativeHeap和部分JavaHeap但具体能否被系统统计到取决于采样时机。大部分临时内存在解码完成后释放而最终Bitmap内存会持续占用直到被回收。建议通过合理设置inSampleSize和inPreferredConfig优化内存并避免二次缩放等操作。内存分析需结合heapprofd等工具关注完整生命周期而非单次采样。BitmapFactory在解码过程中除了最终生成的Bitmap像素内存外解码链路本身还会分配一批临时内存。这些内存大部分在 native 层主要由 Skia / 图片格式解码器 / 输入流 / 颜色转换 / 采样缩放过程使用。但要注意这些解码过程内存通常是临时峰值内存不是最终稳定态内存。OS 是否能统计到取决于统计时机如果dumpsys meminfo/ LMKD / PSS 采样刚好发生在解码过程中就能看到如果解码已经结束很多临时内存已经释放就只会看到最终 Bitmap 或 allocator 残留。1. BitmapFactory 解码过程中会不会额外占内存会。以这条链路为例InputStream is contentResolver.openInputStream(uri); Bitmap bitmap BitmapFactory.decodeStream(is, null, options); imageView.setImageBitmap(bitmap);BitmapFactory.decodeStream()底层大致会经过Java BitmapFactory ↓ JNI nativeDecodeStream / nativeDecodeAsset / nativeDecodeFileDescriptor ↓ Skia ImageDecoder ↓ JPEG / PNG / WebP / HEIF 等具体格式解码器 ↓ 分配最终 Bitmap pixel buffer ↓ 将压缩数据解码成像素在这个过程中除了最终 Bitmap 外还可能有这些临时内存1. 输入流 buffer 2. Java 层临时 byte[] / BufferedInputStream buffer 3. native 层解码器对象 4. native 层压缩数据 buffer 5. native 层 scanline / row buffer 6. native 层采样缩放 buffer 7. native 层颜色空间转换 buffer 8. native 层 EXIF / ICC profile / metadata buffer 9. 解码格式库自己的临时内存比如 libjpeg、libpng、libwebp 10. 如果二次缩放/旋转可能产生中间 Bitmap所以解码阶段峰值内存通常大于最终 Bitmap 大小。2. 最终 Bitmap 是多少假设最终解码结果是400 x 400 ARGB_8888最终 Bitmap 像素内存是400 × 400 × 4 640,000 bytes约0.61 MiB 0.64 MB这是最终稳定保留的 Bitmap native pixel buffer。但解码过程峰值可能是最终 Bitmap 0.64 MB 解码器临时 buffer 输入流 buffer row buffer / color conversion buffer 可能的中间采样图所以解码瞬间可能看到0.64 MB 若干 KB ~ 数 MB具体大小取决于图片格式、采样方式、是否二次缩放、是否旋转、是否色彩转换。3. BitmapFactory 解码过程中主要有哪些临时内存3.1 输入流 buffer如果从 URI 解码contentResolver.openInputStream(uri)通常会有ContentProvider / FileInputStream / ParcelFileDescriptor InputStream buffer BufferedInputStream buffer BitmapFactory 内部 mark/reset buffer这部分可能在 Java heap也可能在 native 或系统 I/O 层。常见大小几 KB ~ 几十 KB如果业务代码自己先把 URI 读成 byte[]byte[] data readAllBytes(uri); Bitmap bitmap BitmapFactory.decodeByteArray(data, 0, data.length);那就会额外产生一份完整压缩文件大小的 Java heap 占用。例如原图 JPEG 文件是 5MBJava byte[] ≈ 5MB 最终 Bitmap ≈ 0.64MB这种会显著增加 Java Heap。3.2 Skia decoder native 对象BitmapFactory底层使用 Skia 解码native 层会创建SkCodec SkImageInfo SkStream SkBitmap SkPixmap decoder state这些对象本身不大通常是KB 级别但它们会管理后续解码 buffer。3.3 JPEG / PNG / WebP 解码器临时 buffer不同格式不同。JPEGJPEG 解码通常按 MCU / scanline 逐行解码不一定需要把整张 3000x4000 原图完整展开到内存。但会有压缩流 buffer Huffman / quantization table MCU buffer scanline buffer 采样缩放中间 buffer 颜色转换 buffer这部分通常是几十 KB ~ 数 MB取决于图片尺寸、采样比例、色彩格式。PNGPNG 是无损压缩解码时可能需要zlib inflate buffer 上一行/当前行 row buffer filter buffer 调色板/透明度表 颜色转换 bufferPNG 某些情况下临时内存会比 JPEG 更明显。WebP / HEIFWebP / HEIF 可能涉及格式解码库内部 buffer YUV/RGB 转换 buffer tile buffer 硬件/软件解码中间 buffer如果走硬解/codec 路径可能还涉及更复杂的 native / gralloc / codec buffer。3.4 颜色空间转换 buffer如果源图是YUV CMYK JPEG wide color 带 ICC profile最终要变成ARGB_8888 / sRGB可能需要额外的转换 buffer。例如YUV - RGB CMYK - RGB wide color - sRGB这部分通常是临时 native 内存。3.5 采样缩放过程 buffer如果原图是3000 x 4000目标是400 x 400传统BitmapFactory.Options.inSampleSize通常不是任意精确缩放而是采样。例如可能先采样成375 x 500然后业务或图片库再裁剪/缩放成400 x 400如果存在二次处理峰值可能变成sampled intermediate bitmap final bitmap 临时 scale/crop buffer例如中间图 500x667 ARGB_8888 ≈ 1.33MB 最终图 400x400 ARGB_8888 ≈ 0.64MB解码/缩放峰值可能至少约 1.97MB 其他临时 buffer稳定态如果只保留最终图则回到0.64MB native Bitmap3.6 EXIF 旋转产生的中间 BitmapBitmapFactory本身只负责解码不一定自动应用 EXIF 旋转。很多图片库会在解码后根据 EXIF 做旋转Matrix matrix new Matrix(); matrix.postRotate(90); Bitmap rotated Bitmap.createBitmap(src, 0, 0, w, h, matrix, true);这会产生原 Bitmap 一份 旋转后 Bitmap 一份峰值接近2 × Bitmap size然后原 Bitmap 等待释放。如果图库或图片库有 EXIF 旋转、centerCrop、round corner、blur、transform这些都会造成额外临时 Bitmap。4. 这些解码过程内存会被 OS 统计到吗答案是会但前提是统计发生时这些内存还存在且已经实际触碰/驻留。Android 的内存统计不是“这段代码曾经分配过多少”而是某个时间点上进程当前持有/驻留了多少内存。5. 会统计到哪些量里5.1 Java Heap如果有 Java 层对象或 byte[]会统计到Java Heap例如byte[] compressed readAllBytes(uri); BitmapFactory.decodeByteArray(compressed, 0, compressed.length);这里的compressed会进入Java Heap如果只是普通decodeStream(inputStream)Java Heap 通常只有少量InputStream 对象 Options 对象 byte[] 小 buffer Bitmap wrapper不会很大。5.2 Native Heap大部分BitmapFactory/ Skia / decoder 临时内存会统计到Native Heap包括Skia decoder buffer libjpeg/libpng/libwebp malloc row buffer color conversion buffer 最终 Bitmap pixel buffer native metadata所以在解码过程中如果刚好看adb shell dumpsys meminfo com.xxx可能会看到Native Heap PSS / Private Dirty 上升解码结束后临时 buffer 释放但最终 Bitmap pixel buffer 仍然留在 Native Heap。5.3 Graphics / GL / EGL / Gfx dev普通BitmapFactory解码 software Bitmap 时解码阶段通常不会产生 GPU texture。GPU 内存一般发生在Bitmap 被绘制到硬件加速 Canvas ImageView draw RenderThread 上传 texture也就是setImageBitmap 之后真正绘制那一帧才会进入Graphics GL mtrack EGL mtrack Gfx dev例外是Bitmap.Config.HARDWARE ImageDecoder 设置 ALLOCATOR_HARDWARE 某些 HEIF/硬解码路径使用 GraphicBuffer这种情况下解码结果本身可能就是 GPU / GraphicBuffer 侧资源会更多统计到Graphics / dmabuf / gralloc / mtrack而不是普通 Native Heap。但普通BitmapFactory.decodeStream()默认常见是 software Bitmap解码阶段Native Heap 显示阶段Graphics/GPU 额外增加 texture5.4 Private Other / mmap / ashmem / dmabuf某些 Android 版本或厂商实现中部分内存可能显示在Private Other Ashmem dmabuf mmap例如文件 mmap GraphicBuffer hardware bitmap codec buffer allocator 没有归类到 Native Heap 的 native 映射不过普通 BitmapFactory software decode 的主路径最常见还是Native Heap5.5 Page Cache从 URI 读取原始文件时文件数据可能进入系统 page cache。这部分通常属于系统文件缓存不一定直接算到 App 的私有 PSS 里。如果 App mmap 了文件或者某些 Provider/decoder 使用 mmap那么相关页面可能以 file-backed mapping 的形式出现在进程地址空间里PSS 可能分摊部分。但普通流式读取时不应简单把原始 JPEG 文件大小算成 App 私有内存。6. 这些解码过程内存是临时的还是持久的大部分是临时的。可以分成三类6.1 持久内存最终 Bitmap最终 Bitmap 像素内存是持久的只要这个 Bitmap 还被引用就不会释放。例如ImageView 持有 BitmapDrawable BitmapDrawable 持有 Bitmap Bitmap 对象持有 native pixel buffer这部分会一直存在。释放条件ImageView 清掉 drawable Bitmap 没有其他强引用 GC 发现 Bitmap Java wrapper 可回收 native pixel buffer 随 Bitmap native finalizer / native destructor 释放例如imageView.setImageDrawable(null); // 或 imageView.setImageBitmap(null); bitmap null;然后等待 GC。如果显式调用bitmap.recycle();可以更主动释放 native pixel buffer但现代 Android 一般不建议在仍可能被 UI 使用的 Bitmap 上随意 recycle容易Canvas: trying to use a recycled bitmap图片库一般通过缓存生命周期管理。6.2 临时内存decoder buffer这些通常在BitmapFactory.decodeXXX()返回前后释放SkCodec libjpeg/libpng/libwebp decoder state scanline buffer row buffer color conversion buffer temporary scale buffer metadata buffer释放时机一般是native decode 函数结束 decoder 对象析构 局部 native buffer free InputStream close 或 native stream 析构也就是Bitmap bitmap BitmapFactory.decodeStream(is);这行返回之后大部分解码器临时内存就应该已经释放。但注意“释放给 native allocator” 不等于 “立即还给 OS”。6.3 半临时/缓存类内存allocator 和库内部缓存有些内存虽然业务上已经 free但系统统计上可能短时间不下降。原因是jemalloc/scudo/malloc allocator 可能保留 arena Skia 可能有小缓存 线程栈/线程本地缓存可能不立刻归还 libc malloc free 后未必 munmap所以可能看到decode 结束了 临时对象释放了 但 Native Heap size 没有马上完全降回去这不一定是泄漏。要区分Native Heap Alloc Native Heap Free Native Heap Size PSS / RSS有时候Alloc 降了 Free 增了 但 Size/PSS 不一定马上降因为 allocator 还持有空闲页等待后续复用或系统压力回收。7. 临时内存什么时候释放按类型拆类型是否临时释放时机decoder native 对象临时decodeXXX()返回前后 native 析构row buffer / scanline buffer临时解码完成或失败时 freeJPEG/PNG/WebP 内部 buffer临时解码器销毁时 free颜色转换 buffer临时转换完成后 free输入流小 buffer临时Java 对象不可达后 GC或 stream closeJava byte[] 压缩数据取决于业务引用byte[] 无引用后 GC中间 Bitmap临时或缓存无引用后 GC 释放 native pixels最终 Bitmap持久无引用后 GC或 recycleGPU texture持久/缓存Bitmap 不再绘制、HWUI cache 淘汰、View detach、trim memory 等8. 重要临时内存不一定能被 dumpsys 捕捉到如果解码很快例如几十毫秒decode start 临时 Native 2MB decode end 临时 Native free而在 decode 后 1 秒执行dumpsys meminfo可能只看到最终 BitmapNative Heap 0.64MB看不到那 2MB 临时峰值。如果刚好在解码过程中采样就可能看到Native Heap 瞬时更高所以分析峰值最好用heapprofd Perfetto memory timeline Android Studio Memory Profiler malloc debug dumpsys meminfo 高频采样而不是只看单次dumpsys meminfo。9. 对 3000x4000 - 400x400 的峰值估算假设理想情况BitmapFactory 直接采样解码到接近 400x400 没有二次 scale 没有 EXIF 旋转 没有 readAllBytes ARGB_8888最终 Bitmap0.64MB Native Heap解码过程临时内存可能几十 KB ~ 几百 KB或者在某些格式/色彩转换下到1MB稳定显示后Native Bitmap ≈ 0.64MB GPU texture ≈ 0.64MB 合计 ≈ 1.28MB如果不理想例如图片库流程是原图 3000x4000 inSampleSize 解到 750x1000 再 centerCrop/scale 到 400x400则峰值可能是750x1000x4 ≈ 3.0MB 中间 Bitmap 400x400x4 ≈ 0.64MB 最终 Bitmap decoder 临时 buffer显示后稳定态如果中间图释放0.64MB Native 0.64MB GPU但如果中间图被缓存保留那就会继续占额外 3.0MB Native如果最差情况先完整解码原图3000x4000x4 48,000,000 bytes 约 45.8MiB再缩到 400x400原图 Bitmap ≈ 45.8MiB 目标 Bitmap ≈ 0.61MiB GPU texture ≈ 0.61MiB峰值可能超过47MiB这就是图库场景中必须避免 full decode 的原因。10. BitmapFactory.Options 对内存的影响10.1 inJustDecodeBoundsoptions.inJustDecodeBounds true; BitmapFactory.decodeStream(is, null, options);只读取宽高和元数据不分配最终 pixel buffer。会有少量临时 decoder buffer但没有最终 Bitmap。内存通常很小。10.2 inSampleSizeoptions.inSampleSize 8;减少最终 Bitmap 尺寸显著降低最终 native pixel buffer。例如3000x4000 原图 inSampleSize8 大约得到 375x500Bitmap 内存375x500x4 ≈ 0.75MB不是完整原图45.8MiB10.3 inPreferredConfigoptions.inPreferredConfig Bitmap.Config.RGB_565;如果可用每像素从 4 字节变成 2 字节。400x400ARGB_8888 ≈ 0.64MB RGB_565 ≈ 0.32MB但会牺牲透明度/色彩质量。10.4 inBitmapoptions.inBitmap reusableBitmap;可以复用已有 Bitmap 的 pixel buffer减少重复分配和 GC 压力。如果复用成功不会新增一块 final pixel buffer 而是复用旧内存但要求尺寸/config/可变性满足条件。图库列表滑动中inBitmap或图片库的 BitmapPool 可以明显降低峰值抖动。11. 为什么有时 Native Heap 释放不明显这点在分析PSS 时很重要。即使 BitmapFactory 临时 buffer 已经 freedumpsys meminfo可能仍然看到 Native Heap 没明显下降原因包括1. native allocator 保留 free chunk准备复用 2. free 后内存页还在进程 RSS 中没有归还内核 3. Skia/HWUI/图片库有缓存 4. Bitmap 被 ImageView、Drawable、缓存、闭包、协程引用着实际没释放 5. GPU texture 仍在 HWUI cache 中 6. Java GC 未发生Bitmap wrapper 还没 finalize/native free所以看到 Native Heap 高不一定都是泄漏但如果持续增长不回落需要查引用链和缓存策略。12. 和 OS 统计的对应关系总结内存来源是否临时OS 统计归属JavaBitmapwrapper持久随 Bitmap 生命周期Java HeapBitmapFactory.Options/InputStream对象临时/持久取决于引用Java HeapJava byte[] 压缩文件数据取决于业务引用Java HeapSkia decoder object临时Native HeapJPEG/PNG/WebP decode buffer临时Native Heaprow buffer / scale buffer临时Native Heapcolor conversion buffer临时Native Heap最终 Bitmap pixel buffer持久Native Heap / Bitmaphardware bitmap buffer持久Graphics / dmabuf / grallocGPU texture持久/缓存Graphics / GL mtrack / EGL mtrack / Gfx dev文件 page cache系统缓存通常不算 App 私有 heap13. 实践建议建议打点时区分decode start decode bounds end decode bitmap end setImageBitmap first draw texture upload end/下一帧 clear/recycle/cache evict同时记录目标 decode 尺寸 原图尺寸 inSampleSize Bitmap config Bitmap byteCount/allocationByteCount 是否有 transform 是否有 hardware bitmap 是否进入内存缓存 是否进入 GPU 显示尤其要避免1. 先 full decode 再 scale 2. 同一 URI 多 Loader 并发重复 decode 3. 中间 Bitmap 被缓存误持有 4. Fragment 退出后 decode 任务继续跑 5. decode 完 setImageBitmap 后旧 ImageView/Drawable 未清理 6. Bitmap native 已释放但 GPU texture 还被 HWUI cache 保留14. 总结BitmapFactory解码过程中确实会分配额外内存主要在 native 层包括 decoder、row buffer、采样缩放、颜色转换、格式库临时 buffer 等。这些内存如果在 OS 采样时仍然存在会被统计到Native Heap、少量Java Heap或特殊情况下Private Other / Graphics。大多数解码过程内存是临时的通常在decodeXXX()返回前后释放但最终 Bitmap 像素内存会持续存在直到 Bitmap 无引用并被 GC/native 释放。释放给 allocator 后也不一定立刻反映为 PSS/RSS 下降因为 native allocator 和 Skia/HWUI 可能保留缓存或空闲页。好好学习天天向上

相关新闻