近期面试记录 (1)
3月25的一场面试,有点翻车的感觉。记录一下,查漏补缺。
好多年没有正儿八经的技术面试,过去几年主要是我面别人,角色突然反转一下,还得适应适应。这个面试官风格四平八稳。虽然我有不少问题没答上来,但面试体验还是挺好的:因为主要是围绕着简历提问,然后深挖,想考查哪些东西我会,没有故意挑刺、专找不会的问。
面试中的问题回答不上来,其实分两种情况看:一是面试官的问题本身就有问题,可能超出了合理认知范围;二是问题是合理的,但面试者的知识积累有欠缺,无法作答。一个简单的原则就是:写在简历上的东西,应当答得上来。这也是我面别人的原则。(当然,有时面试官并非要求所有问题都有完美答案,不过只是考查就变能力或测试技术深度)
平心而论,我这次的表现很一般,过去的一些知识遗忘挺严重的,所以简历上引申出来的一些问题答得并不好。不过,好在我博客上有记录,其中一些内容是我六七年前晋级答辩的主题,重新捡起来也快,也算是知耻后勇亡羊补牢吧。
Bitmap 像素数据存储
演变历史
管理位图内存 | App quality | Android Developers
在 Android 2.3.3(API 级别 10)及更低版本上,位图的后备像素数据存储在本地内存中。它与存储在 Dalvik 堆中的位图本身是分开的。本地内存中的像素数据并不以可预测的方式释放,可能会导致应用短暂超出其内存限制并崩溃。从 Android 3.0(API 级别 11)到 Android 7.1(API 级别 25),像素数据会与关联的位图一起存储在 Dalvik 堆上。在 Android 8.0(API 级别 26)及更高版本中,位图像素数据存储在原生堆中。
API 级别 | 像素数据 | 背景 | 好处 | 潜在问题 | 最佳实践 |
---|---|---|---|---|---|
10及之前 | native 层 | 早期设备 Java 堆内存上限小;GC 性能低 | 减少 Java OOM 及 GC 延迟 | 不能自动 GC,内存超限 | 及时调用 recycle() |
11-25 | Java 层 | 硬件提升,GC 性能提升 | 自动 GC | 受限于 JVM ,不能充分使用系统内存 | - |
26之后 | native 层 | NativeAllocationRegistry 自动回收 Native 内存 | 减少 Java OOM,能自动 GC,充分使用系统内存 | - |
演变依据
抛开性能不谈,个人认为这种 Bitmap 像素数据到该放在 native 内存还是 Java 内存的演变更多还是基于稳定性、灵活性、易用性来考虑的,达到一个”既要稳定、又要灵活、还要易用”的平衡。
- 稳定性 - 指的是尽可能少一些 Java OOM
- 灵活性 - 指的是尽可能允许进程最大限度地使用内存
- 易用性 - 指的是要尽可能自动 GC,减少手动干预
像素数据放在 native 层,稳定性和灵活性都比较好,但是易用性差(用完 Bitmap 之后要调用 recycle()
违背了 Java 自动管理内存的原则);像素数据放在 Java 层,易用性上来了,但稳定性和灵活性都降低了。Android 8.0 引入NativeAllocationRegistry
后,可以自动管理 native 层的像素数据,稳定性、灵活性和易用性三者达到了一个很好的平衡。
API 级别 | 稳定性 | 灵活性 | 易用性 |
---|---|---|---|
10及之前 | 好 | 好 | 不好 |
11-25 | 不好 | 不好 | 好 |
26及之后 | 好 | 好 | 好 |
NativeAllocationRegistry
的更多细节,可以参考我之前写的 Bitmap 之从出生到死亡 (Bitmap 销毁过程那一节)。
以下是 DeepSeek 关于这个问题的回答。点击查看详情
Android 将 Bitmap 像素数据从 Java 层迁移到 Native 层的设计决策,主要基于以下核心原因:1. 内存管理优化
- Java 堆限制
Java 堆内存存在固定上限(可通过Runtime.getRuntime().maxMemory()
查询),早期设备通常为 16MB-256MB。若将 Bitmap 像素数据(例如 1920x1080 的 ARGB_8888 格式图片占用 8MB)存储在 Java 堆,极易触发 OOM(OutOfMemoryError)。 - Native 堆灵活性
Native 堆仅受设备物理内存限制,且可动态扩展。迁移后,Bitmap 像素数据不再挤占 Java 堆空间,显著降低 OOM 风险。
2. 垃圾回收(GC)效率提升
- Java GC 卡顿问题
Java 垃圾回收会引发 Stop-The-World 暂停(尤其是 Full GC),影响应用流畅度。大尺寸 Bitmap 频繁分配/释放会加剧 GC 频率。 - Native 内存自主管理
Native 内存不受 Java GC 管辖,由开发者通过Bitmap.recycle()
或系统自动释放(API 26+)。减少 GC 次数,提升 UI 响应速度。
3. 性能与跨进程优化
- 减少数据拷贝开销
图像处理算法(如缩放、滤镜)若需在 Native 层执行,直接访问 Native 内存可避免 Java-Native 层间像素数据的冗余拷贝。 - 共享内存机制
Android 使用 Ashmem(Anonymous Shared Memory) 管理 Native Bitmap 内存,支持跨进程高效传递 Bitmap(如 Binder 传输仅复制元数据,不复制像素数据)。
4. 系统版本演进
- Android 3.0+(API 11)
默认将 Bitmap 像素数据迁移至 Java 层。 - Android 8.0(API 26)
引入NativeAllocationRegistry
,自动回收 Native 内存,减少手动调用recycle()
的需求。
开发者注意事项
1 | // 旧版本需手动回收(API < 26) |
- 避免野指针:确保 Java 层 Bitmap 对象未被引用时再调用
recycle()
。 - 兼容性处理:部分低端设备 Native 堆仍有限,需通过
BitmapFactory.Options.inSampleSize
合理缩放图片。
总结
迁移至 Native 层的本质是 权衡内存模型与性能:
- 利用 Native 堆的弹性规避 Java 堆限制;
- 绕过 Java GC 提升内存效率;
- 契合图像处理的低延迟需求,构建更健壮的图形子系统。
如何 hook Bitmap
Bitmap 对象是 Android 应用中的内存大户。优化 Bitmap 内存占用,代价小、收益大。所以一个很自然就能想到的问题是应用内哪些 Bitmap 对象占用内存最多?这些占用是否合理?
- 获取应用全局范围内所有 Bitmap 对象的引用,简单地排序比比看,就能解决第一个问题
- 如果同时还回溯创建 Bitmap 对象的来源,我们稍加分析就能回答前面第二个问题
想获取应用全局范围内的 Bitmap 对象,一个可行的方案就是在 native 层进行 hook。简单来说,
第一,无论在 Java 层用何种方式创建 Bitmap 对象,JNI 层的 Bitmap.createBitmap()
方法都是必经之路。所以,它就是最佳的 hook 点
第二,通过 hook 上述方法我们可以就可以玩些花样了。Hook 前后对比如下。
如何 hook Bitmap 的更多细节可以参考 Bitmap: 监控与分析
常见的 hook 方案
PLT hook
PLT(Procedure Linkage Table)Hook 是一种基于动态链接机制的 Hook 技术,主要用于拦截动态库函数调用。其核心原理是通过修改 GOT(Global Offset Table)表中的函数地址,实现函数调用的重定向。
PLT Hook 实现步骤:
- 定位目标函数 GOT 地址
- 解析 ELF 文件获取
.rel.plt
或.rel.dyn
节区 - 通过重定位条目计算目标函数在 GOT 中的偏移
- 解析 ELF 文件获取
- 修改内存权限
1
mprotect(ALIGN_ADDRESS(got_addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
- 替换 GOT 值
1
*((void **)got_entry) = hook_function;
- 处理缓存一致性(ARM 架构需要)
PLT Hook 原理示例代码如下:
1 |
|
通常我们使用现成的 PLT Hook 开源库,例如 bytedance/bhook
Inline Hook
Inline Hook 通过修改目标函数在内存中的机器码,插入跳转指令劫持执行流,实现拦截/监控目标函数行为。
Inline Hook 实现步骤:
- 内存操作:使用
mprotect
修改内存页属性为可写 - 指令替换:覆盖目标函数入口指令(ARM架构需处理Thumb/ARM模式)
- 跳转构造:插入
LDR PC
或B/BLX
指令跳转至Hook函数 - 指令恢复:备份原始指令用于后续还原
Inline Hook 原理示例代码如下:
1 |
|
Android-Inline-Hook 是一个开源的 Inline Hook 库,不过有段时间没更新了。