知道了创建的图片信息是第一步,更重要的是知道哪些图片没有被及时回收。
经常遇到的图片泄漏问题:手动 decode 的 Bitmap 没有及时调用 recycle,导致反复进入页面内存不停上涨,最终导致功能异常。
在 Android 不同版本上,Bitmap 对象的释放流程有所不同:
- Android 8.0 及以前版本:调用 Java Bitmap 的 recycle 方法通过 JNI 调用只是释放了引用,图片的像素数据所占内存需要等待 GC 执行时才释放
- 从 8.0 开始,会直接释放掉 native 内存
两者的共同点是在执行后Java Bitmap的mRecycled状态会变为true。因此我们可以通过轮训Bitmap对象的mRecycled属性来判断这个图片是否被回收。
完整代码:
kotlin
// ===== BitmapProxy.cpp =====
// 这个文件包含:
// 1. create_bitmap_proxy() --- 代理函数,hook 生效后替代原始 createBitmap 被调用
// 2. BitmapProxy 类的所有方法实现
// 3. 后台轮询线程的逻辑
#include "BitmapProxy.h"
// shadowhook.h 提供 ShadowHook 的 API:
// shadowhook_hook_sym_name() --- 按符号名 hook
// shadowhook_unhook() --- 取消 hook
// SHADOWHOOK_STACK_SCOPE() --- 防止递归调用的宏
// SHADOWHOOK_CALL_PREV() --- 通过 stub 调用原始函数
#include "shadowhook.h"
// Android NDK 的 Bitmap 操作 API,提供:
// AndroidBitmap_getInfo() --- 获取 Bitmap 的宽、高、格式、stride
#include <android/bitmap.h>
// ===== 全局变量 =====
// shadowhook_hook_sym_name 返回的 stub 指针
// 保存它是为了之后能 unhook(取消 hook)和通过 SHADOWHOOK_CALL_PREV 调用原始函数
static void *stub = nullptr;
// ===== 辅助函数 =====
/**
* 获取当前时间戳,单位毫秒。
*
* gettimeofday 是 POSIX 系统调用,把当前时间写入 timeval 结构体:
* tv_sec: 秒(从 1970-01-01 开始)
* tv_usec: 微秒(1 秒 = 1,000,000 微秒)
* 返回值 = 秒 * 1000 + 微秒 / 1000,得到毫秒数。
*/
static long long get_current_time_millis() {
struct timeval tv{}; // {} 是 C++ 的值初始化语法,会把结构体的所有字段初始化为 0
gettimeofday(&tv, nullptr); // & 是取地址运算符,把 tv 的地址传给函数
return (long long) tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
// ===== 代理函数 =====
/**
* 代理函数,签名必须和被 hook 的原始函数完全一致。
*
* 原始函数签名(Android 8.0+):
* jobject createBitmap(JNIEnv *env, void *bitmap,
* int bitmap_create_flags,
* jbyteArray nine_patch_chunk,
* jobject nine_patch_insets,
* int density)
*
* Hook 之后,所有对原始 createBitmap 的调用都会被重定向到这个函数。
* 这个函数做的事情:
* 1. 调用原始函数创建 Bitmap → 拿到 Java Bitmap 对象
* 2. 获取 Bitmap 的信息(尺寸、格式等)
* 3. 把信息记录到追踪列表中
* 4. 把 Bitmap 对象原样返回给调用方
*
* 参数说明:
* JNIEnv *env : JNI 环境指针,当前线程的函数表
* void *bitmap : C++ 层的 native Bitmap 指针(Skia 对象)
* int bitmap_create_flags : 创建标志位
* jbyteArray nine_patch_chunk : .9.png 的 chunk 数据
* jobject nine_patch_insets : .9.png 的 padding 信息
* int density : 屏幕密度
*/
jobject create_bitmap_proxy(JNIEnv *env, void *bitmap,
int bitmap_create_flags, jbyteArray nine_patch_chunk,
jobject nine_patch_insets, int density) {
// ----- 第 1 步:声明栈作用域,防止递归 -----
// SHADOWHOOK_STACK_SCOPE() 在当前线程的调用栈上做一个标记
// 如果这个函数被递归调用(即原始函数又触发了 hook),ShadowHook 会检测到并直接跳过
// 不加这个宏的话,如果 bitmap 创建过程中又创建了另一个 bitmap,就会无限递归直到栈溢出
SHADOWHOOK_STACK_SCOPE();
// ----- 第 2 步:调用原始 createBitmap,真正创建 Bitmap -----
// SHADOWHOOK_CALL_PREV 通过 stub 调用原始函数,绕过 hook
// 第一个参数 create_bitmap_proxy 用于 ShadowHook 内部查找对应的 stub
// 后面 6 个参数原封不动传给原始函数
// 返回的 bitmap_obj 是 Java 层 android.graphics.Bitmap 对象的 JNI 引用(jobject)
jobject bitmap_obj = SHADOWHOOK_CALL_PREV(
create_bitmap_proxy,
env, bitmap, bitmap_create_flags,
nine_patch_chunk, nine_patch_insets, density);
// ----- 第 3 步:检查返回值 -----
// 如果原始函数返回 nullptr,说明 Bitmap 创建失败了(比如内存不足)
if (bitmap_obj == nullptr) {
LOGD("create_bitmap_proxy: create failed, skipping record");
return bitmap_obj; // 直接返回 nullptr 给调用方
}
// ----- 第 4 步:使用 NDK API 获取 Bitmap 信息 -----
// AndroidBitmapInfo 是 NDK 提供的结构体,包含:
// width: 宽度(像素)
// height: 高度(像素)
// stride: 每行的字节数(可能因内存对齐而 > width * 每像素字节数)
// format: 像素格式(如 ANDROID_BITMAP_FORMAT_RGBA_8888 等)
//
// AndroidBitmap_getInfo 比用 JNI 反射调用 getWidth()/getHeight() 更高效:
// - 不需要查找 methodID
// - 是直接的 C 函数调用,没有 JNI 跨语言开销
AndroidBitmapInfo bitmapInfo{}; // {} = 初始化为零
int ret = AndroidBitmap_getInfo(env, bitmap_obj, &bitmapInfo);
if (ret < 0) {
// 获取信息失败(极少见的情况,比如传入了非 Bitmap 对象)
LOGD("create_bitmap_proxy: AndroidBitmap_getInfo failed, ret=%d", ret);
return bitmap_obj;
}
// 计算实际分配的内存大小
// stride(每行字节数)* height(行数)= 像素数据占用的总字节数
// 注意要用 stride 而不是 width * 每像素字节数,因为 stride 包含了内存对齐的填充字节
int64_t allocSize = (int64_t) bitmapInfo.stride * bitmapInfo.height;
LOGD("create_bitmap_proxy: w=%d h=%d stride=%d format=%d alloc=%lld bytes (%.2f KB)",
bitmapInfo.width, bitmapInfo.height, bitmapInfo.stride, bitmapInfo.format,
(long long) allocSize, (float) allocSize / 1024.0f);
// ----- 第 5 步:读取 mNativePtr 字段 -----
// mNativePtr 是 android.graphics.Bitmap 的内部字段(类型 long)
// 它保存着 native 层 Skia Bitmap 对象的指针地址
// 用它作为 Bitmap 的唯一标识(类似于身份证号)
// 这里我们用 ShadowHook 已经提供的 native_ptr 参数,两者实际是同一个值
jlong nativePtr = reinterpret_cast<jlong>(bitmap);
// reinterpret_cast: C++ 的类型转换运算符,把 void* 转成 jlong(即 int64_t)
// 注意:不改变内存中的值,只是告诉编译器"把这个指针当整数看待"
// ----- 第 6 步:记录到追踪列表 -----
// 检查全局 BitmapProxy 实例是否存在(正常情况下一定存在,除非还没初始化)
if (g_bitmapProxy != nullptr) {
// -> 运算符:通过指针访问对象的成员函数
// 等价于 (*g_bitmapProxy).onBitmapCreated(...)
g_bitmapProxy->onBitmapCreated(
env, bitmap_obj,
bitmapInfo.width, bitmapInfo.height,
bitmapInfo.stride, bitmapInfo.format,
nativePtr);
}
// ----- 第 7 步:返回 Bitmap 对象给调用方 -----
// 调用方对此完全无感知------它拿到的 Bitmap 和没 hook 时一模一样
return bitmap_obj;
}
// ===== BitmapProxy 成员函数实现 =====
// 类名::函数名 是 C++ 的成员函数定义语法
// :: 是作用域解析运算符,"BitmapProxy::startProxy" 意思是 "BitmapProxy 类的 startProxy 方法"
/**
* 启动整个监控系统:
* 1. 根据 Android 版本选择正确的 so 文件和函数符号
* 2. 执行 PLT hook
* 3. 启动后台追踪(缓存 JNI ID + 开启轮询线程)
*/
void BitmapProxy::startProxy(JavaVM *jvm) {
// android_get_device_api_level() 是 ShadowHook 提供的函数,返回当前设备的 API Level
int api_level = android_get_device_api_level();
// 根据 API Level 选择要 hook 的 .so 文件
// auto 是 C++11 的类型推导关键字,编译器自动推断变量类型
// 这里 auto 推导为 const char*
// 三元运算符 ? : = 条件 ? 值1 : 值2
auto so = api_level > API_LEVEL_10_0
? BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10 // API > 29 → libhwui.so
: BITMAP_CREATE_SYMBOL_SO_RUNTIME; // API ≤ 29 → libandroid_runtime.so
// 根据 API Level 选择要 hook 的函数符号名
// Android 8.0 重构了 Bitmap 相关代码,C++ 函数名发生了变化
auto symbol = api_level >= API_LEVEL_8_0
? BITMAP_CREATE_SYMBOL_RUNTIME // API ≥ 26 → 新符号
: BITMAP_CREATE_SYMBOL_BEFORE_8; // API < 26 → 旧符号
LOGD("startProxy: api_level=%d, so=%s, symbol=%s", api_level, so, symbol);
// 执行 PLT Hook
// shadowhook_hook_sym_name 参数:
// so : 目标 .so 文件名
// symbol : 目标函数符号名
// (void *) create_bitmap_proxy : 代理函数的地址(转成 void* 指针)
// nullptr : 可选的回调,这里不需要
// 返回值 stub : 一个不透明的指针,用于后续 unhook 和 SHADOWHOOK_CALL_PREV
stub = shadowhook_hook_sym_name(so, symbol, (void *) create_bitmap_proxy, nullptr);
if (stub != nullptr) {
LOGD("startProxy: hook success");
} else {
LOGW("startProxy: hook failed!");
return;
}
// Hook 成功后,启动 Bitmap 追踪(缓存 JNI ID + 开启后台轮询线程)
startTracking(jvm);
}
/**
* 停止整个监控系统:
* 1. 取消 Hook
* 2. 停止后台轮询线程
*/
void BitmapProxy::stopProxy() {
// 先取消 hook ------ 之后新创建的 Bitmap 不再经过代理函数
if (stub != nullptr) {
shadowhook_unhook(stub);
stub = nullptr;
LOGD("stopProxy: unhook done");
}
// 停止后台追踪线程
stopTracking();
}
// ===== 追踪记录的管理 =====
/**
* 由代理函数调用,记录新创建的 Bitmap。
*
* 这里做了两件事:
* 1. 创建一个弱全局引用(不阻止 GC)
* 2. 把记录加入 m_records 列表
*
* 关于 NewWeakGlobalRef:
* - NewGlobalRef: 强全局引用,只要不手动释放,GC 永远不会回收这个对象
* 如果一直不释放就会造成内存泄漏 → 监控工具反而制造了泄漏!
* - NewWeakGlobalRef: 弱全局引用,不会阻止 GC
* 对象被 GC 后,弱引用自动变成 NULL(可以通过 IsSameObject 检测)
* - 所以我们用弱引用,既能在对象存活时访问它,又不会阻止它被正常回收
*/
void BitmapProxy::onBitmapCreated(JNIEnv *env, jobject bitmap,
jint width, jint height, jint stride, jint format,
jlong native_ptr) {
// NewWeakGlobalRef: 创建弱全局引用
// 参数是局部引用(只在当前 JNI 调用中有效),返回的全局引用可以跨线程使用
jweak weakRef = env->NewWeakGlobalRef(bitmap);
// 如果创建弱引用失败(极少见),放弃这次记录
if (weakRef == nullptr) {
LOGW("onBitmapCreated: NewWeakGlobalRef failed");
return;
}
// 获取创建时间戳
long long now = get_current_time_millis();
// ----- 临界区开始 -----
// std::lock_guard 是 RAII 风格的锁包装器:
// 构造时自动调用 m_mutex.lock()(加锁)
// 析构时自动调用 m_mutex.unlock()(解锁)
// 即使中间发生异常,析构也会自动执行,不会死锁
// 这比手动 lock()/unlock() 安全得多
{
std::lock_guard<std::mutex> lock(m_mutex);
// push_back: 往 vector 末尾追加一个元素
// { ... } 是 C++11 的统一初始化语法,直接构造一个 BitmapRecord 并加入 vector
m_records.push_back({
weakRef, // java_bitmap_ref
native_ptr, // native_ptr
width, // width
height, // height
stride, // stride
format, // format
now // create_time
});
}
// ----- 临界区结束(lock_guard 析构,自动解锁)-----
LOGD("onBitmapCreated: recorded, total=%zu", m_records.size());
// %zu 是 size_t(无符号整数类型,vector::size() 的返回类型)的格式说明符
}
// ===== 后台轮询 =====
/**
* 启动追踪系统:
* 1. 获取当前线程的 JNIEnv,缓存 isRecycled() 方法的 methodID
* (methodID 是跨线程有效的,可以在任何线程使用)
* 2. 创建一个新的后台线程,循环检查哪些 Bitmap 已被回收
*/
void BitmapProxy::startTracking(JavaVM *jvm) {
m_jvm = jvm; // 保存 JavaVM 指针,后台线程需要用它获取 JNIEnv
// ----- 缓存 JNI 方法 ID -----
// JNI 中,类的 methodID 和 fieldID 在进程内是全局有效的,跨线程安全
// 提前缓存好,避免轮询线程每次都要重新查找(省去 FindClass + GetMethodID 的开销)
JNIEnv *env;
// AttachCurrentThread: 如果当前线程已经附加到 JVM,返回已有的 JNIEnv
// 如果还没附加,会自动附加
// 这里当前线程是主线程,已经附加了,所以直接返回
jvm->AttachCurrentThread(&env, nullptr);
// FindClass: 根据全限定类名查找类,返回局部引用
// 注意 JNI 中的类名用 / 而不是 . 分隔
jclass localBitmapClass = env->FindClass("android/graphics/Bitmap");
if (localBitmapClass == nullptr) {
LOGW("startTracking: FindClass failed");
return;
}
// NewGlobalRef: 把局部引用升级为全局引用
// 局部引用只在当前 JNI 调用帧中有效,函数返回后就失效
// 全局引用一直有效(直到手动 DeleteGlobalRef),可以从其他线程访问
m_bitmapClass = (jclass) env->NewGlobalRef(localBitmapClass);
env->DeleteLocalRef(localBitmapClass); // 释放局部引用,避免引用表溢出
// GetMethodID: 获取方法的 ID
// 参数: 类, 方法名, 方法签名
// 方法签名 "()Z" 的含义:
// () = 无参数
// Z = 返回 boolean(Z 是 JNI 中 boolean 的类型码)
m_isRecycledMethod = env->GetMethodID(m_bitmapClass, "isRecycled", "()Z");
if (m_isRecycledMethod == nullptr) {
LOGW("startTracking: GetMethodID(isRecycled) failed");
return;
}
LOGD("startTracking: JNI method IDs cached");
// ----- 启动后台轮询线程 -----
m_polling.store(true); // atomic 写操作,设置运行标志为 true
// 创建 std::thread 对象
// 构造函数的参数:一个可调用对象(这里用 lambda 表达式)
// [this] 是 lambda 的捕获列表,把 this 指针传进去,lambda 内部就能访问成员变量
// () 是参数列表(这里没有参数)
// { ... } 是函数体
m_pollThread = std::thread([this]() {
this->pollLoop(); // 在后台线程中执行 pollLoop
});
LOGD("startTracking: poll thread started, interval=%d seconds", CHECK_INTERVAL_SECONDS);
}
/**
* 停止追踪系统:
* 1. 通过 atomic 标志通知轮询线程退出
* 2. 等待线程结束(join)
* 3. 清理缓存的 JNI 全局引用
*/
void BitmapProxy::stopTracking() {
// 第一步:通知轮询线程退出
m_polling.store(false);
// 第二步:等待后台线程结束
// join(): 阻塞当前线程,直到 m_pollThread 执行完毕
// joinable(): 检查线程是否可以 join(线程是否还在运行且没有被 join 过)
// 不 join 就析构 thread 对象会导致程序 crash(std::terminate)
if (m_pollThread.joinable()) {
m_pollThread.join();
}
LOGD("stopTracking: poll thread stopped");
// 第三步:清理 JNI 全局引用
// 全局引用不会自动释放,必须手动调用 DeleteGlobalRef
// 否则会一直占用 JNI 引用表空间,最终导致崩溃
JNIEnv *env;
if (m_jvm->AttachCurrentThread(&env, nullptr) == 0) {
// 清理弱引用:遍历所有记录,释放每个 Bitmap 的弱引用
for (auto &record : m_records) {
// & 在这里是引用符号,auto& 表示 record 是 m_records 中元素的引用(别名)
// 不用 & 的话会拷贝一份,修改副本不会影响原数据
env->DeleteWeakGlobalRef(record.java_bitmap_ref);
}
m_records.clear(); // 清空 vector
// 释放缓存的 Bitmap 类全局引用
if (m_bitmapClass != nullptr) {
env->DeleteGlobalRef(m_bitmapClass);
m_bitmapClass = nullptr;
}
}
LOGD("stopTracking: cleaned up");
}
/**
* 后台轮询线程的主循环。
*
* 执行流程:
* 1. 把当前线程附加到 JVM(获取 JNIEnv)
* 2. 循环(每次间隔 CHECK_INTERVAL_SECONDS 秒):
* a. 加锁复制当前记录列表
* b. 遍历每条记录:
* - 用 IsSameObject 检查弱引用是否还可达(对象是否被 GC 了)
* - 如果可达,用 NewLocalRef 获取临时强引用
* - 调用 isRecycled() 检查是否主动回收了
* - 如果已回收或已 GC → 从列表中移除
* - 如果都未回收 → 保留(这就是泄漏的可疑对象)
* c. 用过滤后的列表替换原列表
* d. 解锁
* 3. 线程退出时从 JVM 分离
*/
void BitmapProxy::pollLoop() {
// ----- 附加当前线程到 JVM -----
// 这个线程是 C++ 创建的,JVM 还不认识它
// AttachCurrentThread 把线程注册到 JVM,并返回一个 JNIEnv 指针
// 有了 JNIEnv 才能调用 Java 方法
JNIEnv *env = nullptr;
if (m_jvm->AttachCurrentThread(&env, nullptr) != 0) {
LOGW("pollLoop: AttachCurrentThread failed");
return;
}
LOGD("pollLoop: started");
// ----- 主循环 -----
// while (m_polling.load()): atomic 读操作,线程安全地读取标志位
while (m_polling.load()) {
// 分段 sleep:每秒醒来一次检查 m_polling 标志
// 如果用 sleep(CHECK_INTERVAL_SECONDS),停止时最多要等 5 秒才能响应
// 分段 sleep 让线程能更快地响应停止信号
for (int i = 0; i < CHECK_INTERVAL_SECONDS && m_polling.load(); i++) {
// std::chrono::seconds(1) = 1 秒的时间长度
// sleep_for 让当前线程休眠指定时间
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 可能在 sleep 期间被通知退出了,再检查一次
if (!m_polling.load()) {
break;
}
// ----- 检查所有记录 -----
{
// 加锁,保护 m_records
std::lock_guard<std::mutex> lock(m_mutex);
// 如果列表是空的,跳过本轮检查
// empty() 返回 vector 是否为空
if (m_records.empty()) {
continue; // continue 跳到 while 循环的下一次迭代
}
// 创建一个新列表,存放本轮检查后"仍未被回收"的记录
std::vector<BitmapRecord> surviving;
// 遍历 m_records 中的所有记录
// range-based for 循环(C++11):依次取出 m_records 中的每个元素,赋值给 record
// auto& : 引用类型,避免拷贝整个 BitmapRecord
for (auto &record : m_records) {
// ----- 检查第 1 关:对象是否被 GC 回收了? -----
// IsSameObject(obj1, obj2): 比较两个 JNI 引用是否指向同一个对象
// 特殊用法:IsSameObject(weakRef, nullptr)
// 返回 JNI_TRUE → 弱引用指向的对象已被 GC 回收(或从未有效过)
// 返回 JNI_FALSE → 对象还存在,可以继续访问
jboolean isGarbageCollected = env->IsSameObject(record.java_bitmap_ref, nullptr);
// jboolean 是 JNI 的布尔类型,本质是 unsigned char
if (isGarbageCollected == JNI_TRUE) {
// 对象已经被 GC 回收了 → 不需要再追踪这条记录
// DeleteWeakGlobalRef: 释放弱引用,归还 JNI 引用表空间
env->DeleteWeakGlobalRef(record.java_bitmap_ref);
continue; // 不加入 surviving 列表 = 从追踪中移除
}
// ----- 检查第 2 关:用户是否主动调用了 recycle()? -----
// 通过了第 1 关说明对象还活着,现在需要暂时获取一个强引用来调用方法
// NewLocalRef: 从弱引用创建一个局部强引用
// 这个局部引用会暂时阻止对象被 GC(在我们的检查期间)
// 返回 nullptr 说明弱引用在此期间失效了(极端情况)
jobject localRef = env->NewLocalRef(record.java_bitmap_ref);
if (localRef == nullptr) {
// 弱引用刚好在这时失效(对象被 GC 了)
env->DeleteWeakGlobalRef(record.java_bitmap_ref);
continue;
}
// 调用 Bitmap.isRecycled() 方法
// CallBooleanMethod: JNI 中调用返回 boolean 的 Java 方法
// 参数:对象引用, 方法 ID
jboolean isRecycled = env->CallBooleanMethod(localRef, m_isRecycledMethod);
if (isRecycled == JNI_TRUE) {
// 用户调了 recycle() → Bitmap 已主动释放
// 清理引用后从追踪中移除
env->DeleteLocalRef(localRef);
env->DeleteWeakGlobalRef(record.java_bitmap_ref);
continue;
}
// ----- 两关都没过 → 这是一个未被回收的 Bitmap! -----
// 把它保留在新列表中,继续追踪
surviving.push_back(record);
// 注意:这里 record 的 java_bitmap_ref 已经在上面通过 NewLocalRef 验证过是有效的
// surviving 中的记录保留了弱引用,下次循环还会再检查
// 释放局部引用(我们用完了,不需要一直持有强引用)
env->DeleteLocalRef(localRef);
}
// 用过滤后的列表替换原列表
// == 比较的是两个 vector 的内容
// 如果数量没变化,说明没有 Bitmap 在这轮被回收
if (surviving.size() != m_records.size()) {
m_records = surviving; // 替换
}
}
// 锁在这里释放(lock_guard 离开作用域时自动析构)
// 如果有未回收的 Bitmap,打印警告日志
if (!m_records.empty()) {
long long totalSize = getUnrecycledSize();
LOGW("pollLoop: %zu unrecycled bitmaps, total size=%lld bytes (%.2f KB)",
m_records.size(), totalSize, (float) totalSize / 1024.0f);
}
}
// ----- 线程退出,从 JVM 分离 -----
// DetachCurrentThread: 告诉 JVM 这个线程不再需要 JNI 访问
// 如果不 detach,JVM 会一直保留这个线程的 JNI 资源
m_jvm->DetachCurrentThread();
LOGD("pollLoop: stopped");
}
// ===== 对外查询接口 =====
/**
* 获取当前未被回收的 Bitmap 数量。
* 这个方法是线程安全的(加锁读取)。
*
* 返回值: 仍在追踪列表中的 Bitmap 数量
* 这些 Bitmap 满足两个条件:
* 1. 没有被 GC 回收(弱引用还可达)
* 2. 没有调用过 recycle()
*/
int BitmapProxy::getUnrecycledCount() {
// 加锁保护(因为代理函数可能在另一个线程中同时修改 m_records)
std::lock_guard<std::mutex> lock(m_mutex);
return (int) m_records.size(); // static_cast 也可以,但 C 风格强转在此处更简洁
}
/**
* 计算未被回收的 Bitmap 的总内存占用。
*
* 注意:实际内存 = stride * height(不是 width * height * 每像素字节数)
* 因为 stride 包含了内存对齐的填充字节。
*/
long long BitmapProxy::getUnrecycledSize() {
std::lock_guard<std::mutex> lock(m_mutex);
long long totalBytes = 0;
for (auto &record : m_records) {
// stride(每行字节数)* height(行数)= 像素数据总字节数
totalBytes += (long long) record.stride * record.height;
}
return totalBytes;
}
/**
* 将所有未被回收的 Bitmap 信息打印到 logcat。
* 每条记录包括:
* - native 指针地址
* - 尺寸(宽 x 高)
* - 内存占用
* - 格式
* - 存活时间(当前时间 - 创建时间)
*
* 这个方法适合在开发调试时手动调用,快速查看有哪些 Bitmap 没有被释放。
*/
void BitmapProxy::dumpUnrecycled(JNIEnv *env) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_records.empty()) {
LOGD("dumpUnrecycled: all bitmaps recycled!");
return;
}
long long now = get_current_time_millis();
// 打印概览
long long totalSize = 0;
for (auto &record : m_records) {
totalSize += (long long) record.stride * record.height;
}
LOGW("===== Unrecycled Bitmaps: %zu, total size: %lld bytes (%.2f KB) =====",
m_records.size(), totalSize, (float) totalSize / 1024.0f);
// 逐条打印详情
int index = 0;
for (auto &record : m_records) {
// 检查弱引用是否还可达
jboolean reachable = env->IsSameObject(record.java_bitmap_ref, nullptr);
// 计算存活时间
long long aliveSeconds = (now - record.create_time) / 1000;
// 打印单条记录
// 格式说明:
// nativePtr=0x%llx: 以十六进制打印指针地址
// w/h: 宽高
// stride: 行字节数
// fmt: 像素格式
// alive: 存活时长
// reachable: 弱引用是否可达(y=可达, n=已被GC)
LOGW(" [%d] nativePtr=0x%llx, %dx%d, stride=%d, fmt=%d, "
"alloc=%lld bytes, alive=%lld s, reachable=%c",
index,
(long long) record.native_ptr,
record.width, record.height,
record.stride, record.format,
(long long) record.stride * record.height,
aliveSeconds,
reachable == JNI_TRUE ? 'n' : 'y');
// reachable 反过来:IsSameObject == JNI_TRUE 表示已经是 nullptr(被 GC 了)
// 这里打 'y' 表示还可达,'n' 表示已被 GC
// 正常情况 unrecycled 列表里应该都是 reachable 的
// 如果出现 reachable='n',说明在上次检查和这次 dump 之间被 GC 了(正常的时间窗口)
index++;
}
LOGW("===== End of Unrecycled Bitmaps =====");
}
kotlin
#ifndef NATIVEDEMO_BITMAPPROXY_H
#define NATIVEDEMO_BITMAPPROXY_H
#include <android/log.h>
// jni.h 提供了 JNI 编程所需的所有类型和函数声明
// JNIEnv* : 每个线程独立的 JNI 函数表指针,所有 JNI 操作都要通过它
// jobject : Java 对象的引用(在 native 层是一个不透明的指针)
// jclass / jmethodID / jfieldID : 分别代表 Java 的类、方法、字段
#include <jni.h>
// ===== 以下三个是 C++ 标准库头文件 =====
// std::vector ------ C++ 的动态数组,类似 Java 的 ArrayList
// 相比 C 语言数组,vector 自动管理内存,可以动态增删元素
#include <vector>
// std::mutex ------ 互斥锁(mutual exclusion,互相排斥的锁)
// 同一时刻只有一个线程能持有这个锁,用来保护共享数据不被多个线程同时修改
// std::lock_guard ------ 锁的 RAII 包装器,构造时自动加锁,析构时自动解锁
// (RAII = Resource Acquisition Is Initialization,资源获取即初始化)
#include <mutex>
// std::atomic ------ 原子类型,对这个变量的读写操作是不可分割的
// 普通 bool 在多线程读写时可能读到中间状态,atomic<bool> 保证不会有这个问题
#include <atomic>
// std::thread ------ C++ 的线程类,类似 Java 的 Thread
// std::chrono ------ C++ 的时间库,用于 sleep 等时间相关操作
#include <thread>
#include <chrono>
// sys/time.h 提供 gettimeofday() 函数,用于获取当前时间戳(毫秒级精度)
#include <sys/time.h>
// ===== 日志宏 =====
#define LOG_TAG "NativeLog"
// __android_log_print 是 Android NDK 提供的日志函数
// ANDROID_LOG_DEBUG = 调试级别,ANDROID_LOG_WARN = 警告级别
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
// ===== Hook 目标:需要 hook 的 .so 文件名 =====
// Android 10 (API 29) 之后,Bitmap 创建逻辑移到了 libhwui.so
#define BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10 "libhwui.so"
// Android 10 之前,Bitmap 创建在 libandroid_runtime.so
#define BITMAP_CREATE_SYMBOL_SO_RUNTIME "libandroid_runtime.so"
// ===== Hook 目标:需要 hook 的函数符号名(C++ mangled name) =====
// C++ 支持函数重载,编译器会把函数名编码成包含参数类型的"mangled name"
// 这里的两串字符就是 createBitmap 函数经过编译器 name mangling 后的符号名
// Android 8.0 之前的 createBitmap 符号名
#define BITMAP_CREATE_SYMBOL_BEFORE_8 "_ZN11GraphicsJNI12createBitmapEP7_JNIEnvPN7android6BitmapEiP11_jbyteArrayP8_jobjecti"
// Android 8.0 及之后的 createBitmap 符号名
#define BITMAP_CREATE_SYMBOL_RUNTIME "_ZN7android6bitmap12createBitmapEP7_JNIEnvPNS_6BitmapEiP11_jbyteArrayP8_jobjecti"
// API Level 常量,用于版本判断
#define API_LEVEL_10_0 29
#define API_LEVEL_8_0 26
// ===== BitmapRecord 结构体 =====
// struct 是 C++ 中的数据结构,类似 Java 的 data class
// 每个 BitmapRecord 记录一次 Bitmap 创建事件的关键信息
// 我们用它来追踪哪些 Bitmap 还没被回收
struct BitmapRecord {
// jweak: JNI 弱全局引用,指向 Java 层的 Bitmap 对象
// 弱引用 vs 强引用:
// - 强引用 (NewGlobalRef): 会阻止 GC 回收对象 → 不合适,会制造新的泄漏
// - 弱引用 (NewWeakGlobalRef): 不阻止 GC,对象被 GC 后弱引用变成 NULL
// 所以我们用弱引用,既能在 Bitmap 存活时访问它,又不会阻止它被正常回收
jweak java_bitmap_ref;
// mNativePtr 的值,Bitmap 内部字段,指向 native 层 Skia Bitmap 对象
// 可以当作 Bitmap 的唯一标识
jlong native_ptr;
// Bitmap 的基本信息
jint width; // 图片宽度(像素)
jint height; // 图片高度(像素)
jint stride; // 每行像素占用的字节数(可能大于 width * 每像素字节数,因为有对齐)
jint format; // 像素格式,如 ARGB_8888=1, RGB_565=4 等(Android 内部格式常量)
// 创建时间戳(毫秒),记录这个 Bitmap 是什么时候创建的
long long create_time;
};
// ===== BitmapProxy 类 =====
// class 是 C++ 中的类,类似 Java 的 class
// public/private 控制成员的访问权限:
// public: 外部可以访问
// private: 只有类内部可以访问
class BitmapProxy {
public:
// ----- 生命周期管理 -----
// 启动代理:hook native 函数 + 开启后台轮询线程
// 参数 jvm: JavaVM 指针,后台线程需要用它来获取 JNIEnv
void startProxy(JavaVM *jvm);
// 停止代理:取消 hook + 停止后台线程 + 清理数据
void stopProxy();
// ----- 由代理函数调用的接口 -----
// 每当有新的 Bitmap 创建时,代理函数会调用这个方法记录一条数据
// env: 当前线程的 JNI 环境
// bitmap: 刚创建好的 Java Bitmap 对象
// info: Bitmap 的尺寸格式信息
void onBitmapCreated(JNIEnv *env, jobject bitmap,
jint width, jint height, jint stride, jint format,
jlong native_ptr);
// ----- 对外查询接口 -----
// 获取当前未被回收的 Bitmap 数量
int getUnrecycledCount();
// 将所有未被回收的 Bitmap 信息打印到 logcat
void dumpUnrecycled(JNIEnv *env);
// 获取未被回收 Bitmap 的总内存占用(字节)
long long getUnrecycledSize();
private:
// ----- 内部方法 -----
// 初始化追踪系统:缓存 JNI 方法 ID、启动轮询线程
void startTracking(JavaVM *jvm);
// 停止追踪:通知轮询线程退出、等待线程结束、清理 JNI 全局引用
void stopTracking();
// 后台轮询线程的主函数,循环检查哪些 Bitmap 已被回收
void pollLoop();
// ===== 成员变量 =====
// m_ 前缀是 C++ 中常见的成员变量命名约定(m = member)
// 这样可以区分成员变量和局部变量
// 轮询间隔(秒),多久检查一次
// static constexpr: 编译期常量,不占用对象内存,类似 Java 的 static final
static constexpr int CHECK_INTERVAL_SECONDS = 5;
JavaVM *m_jvm = nullptr;
// nullptr 是 C++11 的空指针,代替 C 语言的 NULL
// ---- 缓存 JNI 相关的 ID,避免每次使用时重复查找 ----
// jmethodID 是 JNI 中 Java 方法的"身份证号",拿到后可以反复使用
jmethodID m_isRecycledMethod = nullptr; // Bitmap.isRecycled() 方法 ID
jclass m_bitmapClass = nullptr; // android.graphics.Bitmap 类的全局引用
// ---- 数据存储 ----
// vector<BitmapRecord>: 一个动态数组,每个元素是一个 BitmapRecord
// 存储所有"尚未确认已回收"的 Bitmap 记录
std::vector<BitmapRecord> m_records;
// mutex: 保护 m_records 的互斥锁
// 因为代理函数可能在任意线程被调用(哪个线程创建 Bitmap 就在哪个线程触发 hook)
// 而轮询线程也会读写 m_records,所以必须加锁保护
std::mutex m_mutex;
// ---- 后台线程 ----
// atomic<bool>: 线程安全的布尔标志
// .load() 读取当前值,.store() 设置新值
// 轮询线程每轮循环开始时检查这个标志,为 false 就退出
std::atomic<bool> m_polling{false};
// 轮询线程对象
std::thread m_pollThread;
};
// 全局指针,指向唯一的 BitmapProxy 实例
// extern 关键字:告诉编译器"这个变量在别处定义,这里只是声明"
// 在 BitmapProxyUtils.cpp 中定义,在 BitmapProxy.cpp 代理函数中使用
// 这样代理函数(是个 C 函数,不是成员函数)也能访问到 BitmapProxy 实例
extern BitmapProxy *g_bitmapProxy;
#endif //NATIVEDEMO_BITMAPPROXY_H
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化 ShadowHook
ShadowHook.init(
ShadowHook.ConfigBuilder()
.setMode(ShadowHook.Mode.SHARED)
.build()
)
// 开启 Bitmap 创建监控(hook + 后台轮询线程)
BitmapProxyUtils.startProxyBitmap()
try {
testCreateBitmap()
} catch (e: Exception) {
Log.e("BitmapTest", "testCreateBitmap error", e)
}
setupLeakDemo()
}
override fun onDestroy() {
super.onDestroy()
BitmapProxyUtils.stopProxyBitmap()
}
/**
* 原始测试:通过各种方式创建 Bitmap,验证 hook 是否生效
*/
private fun testCreateBitmap() {
val tag = "BitmapTest"
// 1. 给 ImageView 设置图片
Log.d(tag, ">>> 1. ImageView.setImageResource")
val imageView = findViewById<ImageView>(R.id.iv_logo_pixelated_0)
imageView.setImageResource(R.mipmap.logo_pixelated_0)
// 2. 通过 BitmapFactory 解码本地图片
Log.d(tag, ">>> 2. BitmapFactory.decodeResource")
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.logo_pixelated_1)
Log.d(tag, "decodeResource: ${bitmap.width}x${bitmap.height}, ${bitmap.byteCount} bytes")
val imageView2 = findViewById<ImageView>(R.id.iv_logo_pixelated_1)
imageView2.setImageBitmap(bitmap)
// 3. 根据其他图片创建新图片 + setPixels
Log.d(tag, ">>> 3. Bitmap.createBitmap + setPixels")
val bitmapWidth = bitmap.width
val bitmapHeight = bitmap.height
val numPixels = bitmapWidth * bitmapHeight
val pixels = IntArray(numPixels)
bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight)
val copyBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
copyBitmap.setPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight)
Log.d(tag, "createBitmap+setPixels: ${copyBitmap.width}x${copyBitmap.height}, ${copyBitmap.byteCount} bytes")
// 4. 通过资源 id 获取 drawable
// 注意:在 API 35 上 Resources.getDrawable() 走的是 ImageDecoder 管线
// 不经过 android::bitmap::createBitmap,所以不会触发 hook
Log.d(tag, ">>> 4. resources.getDrawable")
val drawable: Drawable = resources.getDrawable(R.mipmap.logo_pixelated_2, theme)
val imageView1 = findViewById<ImageView>(R.id.iv_logo_pixelated_2)
imageView1.setImageDrawable(drawable)
if (drawable is BitmapDrawable) {
val bmp = drawable.bitmap
Log.d(tag, "getDrawable(BitmapDrawable): ${bmp.width}x${bmp.height}, ${bmp.byteCount} bytes")
}
// 5. 创建大图
Log.d(tag, ">>> 5. Bitmap.createBitmap 大图")
val largeBmp = Bitmap.createBitmap(2000, 1500, Bitmap.Config.ARGB_8888)
Log.d(tag, "createBitmap(2000x1500): ${largeBmp.width}x${largeBmp.height}, ${largeBmp.byteCount} bytes")
Log.d(tag, ">>> testCreateBitmap 完成")
}
// ===== 泄漏检测 Demo =====
// 故意持有 Bitmap 不释放,模拟内存泄漏
// object 让这个列表变成"静态"的,和 Activity 的生命周期无关
private val leakedBitmaps = mutableListOf<Bitmap>()
// 故意持有但是调了 recycle() 的 Bitmap(正确用法,不会被标记为泄漏)
private val recycledBitmaps = mutableListOf<Bitmap>()
private fun setupLeakDemo() {
val tag = "LeakDemo"
val statusText = findViewById<TextView>(R.id.tv_status)
val gcBtn = findViewById<Button>(R.id.btn_gc)
val checkBtn = findViewById<Button>(R.id.btn_check_leak)
val leakBtn = findViewById<Button>(R.id.btn_create_leak)
val recycleBtn = findViewById<Button>(R.id.btn_create_recycled)
// 按钮 1:创建泄漏 ------ 创建 3 个 Bitmap 存到静态列表,不调 recycle
leakBtn.setOnClickListener {
for (i in 1..3) {
val bmp = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
// 存到静态列表中 → 这个引用永远不会被释放 → 内存泄漏
leakedBitmaps.add(bmp)
Log.w(tag, "Leaked bitmap #$i: ${bmp.width}x${bmp.height}")
}
updateStatus(tag, statusText)
}
// 按钮 2:正确用法 ------ 创建 3 个 Bitmap 后主动 recycle
recycleBtn.setOnClickListener {
for (i in 1..3) {
val bmp = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
bmp.recycle() // 主动释放 native 内存
recycledBitmaps.add(bmp) // 虽然还持有引用,但已 recycle
Log.i(tag, "Recycled bitmap #$i: ${bmp.width}x${bmp.height}")
}
updateStatus(tag, statusText)
}
// 按钮 3:主动触发 GC
// 这会让那些"没人引用但没调 recycle"的 Bitmap 被 GC 回收
gcBtn.setOnClickListener {
Log.i(tag, "Requesting GC...")
Runtime.getRuntime().gc()
System.runFinalization() // 触发 finalize() 方法
Runtime.getRuntime().gc() // 再跑一次 GC,确保 finalize 创建的对象也被回收
}
// 按钮 4:查询未回收的 Bitmap
checkBtn.setOnClickListener {
updateStatus(tag, statusText)
BitmapProxyUtils.dumpUnrecycledBitmaps()
}
updateStatus(tag, statusText)
}
private fun updateStatus(tag: String, statusText: TextView) {
val count = BitmapProxyUtils.getUnrecycledBitmapCount()
val size = BitmapProxyUtils.getUnrecycledBitmapSize()
val sizeKB = size / 1024f
val sizeMB = sizeKB / 1024f
val msg = buildString {
append("Unrecycled: $count bitmaps")
append("\nTotal: ")
if (sizeMB >= 1) {
append("%.2f MB".format(sizeMB))
} else {
append("%.2f KB".format(sizeKB))
}
append("\nLeaked list size: ${leakedBitmaps.size}")
append("\nRecycled list size: ${recycledBitmaps.size}")
}
statusText.text = msg
Log.d(tag, msg.replace("\n", ", "))
}
}
日志打印:
kotlin
**026-07-23 17:14:45.323 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=420 h=126 stride=420 format=8 alloc=52920 bytes (51.68 KB)
2026-07-23 17:14:45.323 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=4
2026-07-23 17:14:45.330 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=500 h=500 stride=2000 format=1 alloc=1000000 bytes (976.56 KB)
2026-07-23 17:14:45.330 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=5
2026-07-23 17:14:45.331 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=500 h=500 stride=2000 format=1 alloc=1000000 bytes (976.56 KB)
2026-07-23 17:14:45.331 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=6
2026-07-23 17:14:45.331 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=500 h=500 stride=2000 format=1 alloc=1000000 bytes (976.56 KB)
2026-07-23 17:14:45.331 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=7
2026-07-23 17:14:45.689 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=300 h=300 stride=1200 format=1 alloc=360000 bytes (351.56 KB)
2026-07-23 17:14:45.689 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=8
2026-07-23 17:14:45.689 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=300 h=300 stride=1200 format=1 alloc=360000 bytes (351.56 KB)
2026-07-23 17:14:45.690 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=9
2026-07-23 17:14:45.690 2781-2781 NativeLog com.example.bitmapstudy D create_bitmap_proxy: w=300 h=300 stride=1200 format=1 alloc=360000 bytes (351.56 KB)
2026-07-23 17:14:45.690 2781-2781 NativeLog com.example.bitmapstudy D onBitmapCreated: recorded, total=10
2026-07-23 17:14:49.231 2781-2804 NativeLog com.example.bitmapstudy W pollLoop: 7 unrecycled bitmaps, total size=3350840 bytes (3272.30 KB)
2026-07-23 17:14:54.238 2781-2804 NativeLog com.example.bitmapstudy W pollLoop: 7 unrecycled bitmaps, total size=3350840 bytes (3272.30 KB)**
唯一触碰到 GOT 的地方
// BitmapProxy.cpp:149
stub = shadowhook_hook_sym_name(so, symbol,
(void *) create_bitmap_proxy, nullptr);
这一行代码做的事,翻译成 PLT/GOT 的语言就是:
- 在 so (libhwui.so) 的 .dynsym 里找到 symbol 对应的函数
- 遍历所有已加载 .so 的 GOT 表
- 找到 GOT 中哪些条目指向这个函数
- 把这些 GOT 条目的值从 原始地址 → 改成 create_bitmap_proxy 的地址
- 把原始地址保存到 stub 里
| 对比维度 | GOT (全局偏移表) | PLT (过程链接表) |
|---|---|---|
| 本质 | 数据表(存储内存地址) | 代码段(存储汇编跳转指令) |
| 比喻 | 公司的"员工通讯录" | 公司的"前台接待员" |
| 内容 | 外部函数的真实内存地址 | 跳转到 GOT 表,或跳转到系统解析器的指令 |
| 可写性 | 可读写(运行时地址会被动态填入) | 只读(代码段,运行时不允许修改) |
| 作用 | 记录"目标函数到底在哪" | 负责"把调用请求安全地转交出去" |
- PLT = 一段固定的跳板指令,只读,从生到死不变
- GOT = 一张可改的地址表,动态链接器启动时往里填地址,Hook 运行时往里换地址