在 Android 中,图片是内存占用的大户。为了减少图片对App内存的影响,Android系统在不同版本上做过不同的处理:
- 在Android 8.0以前,图片的宽高数据和像素数据都保存在Java层。
- 从Android 8.0开始,Java层只保存图片的宽高数据,图片的像素数据保存在Native层,不再占用Java Heap内存。
Android 8后面的优化,带来的好处是Android 8.0及以后的设备上出现Java OOM的概率大大降低。但是如果 Android 8.0及以后的设备使用图片不当,还是会出现占用的物理内存过多导致的位于后台时容易被LMK强制关闭的问题。
因此能够监视图片内存的使用非常重要,这篇文章就将介绍使用 NDK 来实现监视图片内存的功能。
实现原理
Bitmap 的创建原理
如下图所示,当在 Android 8.0 以下时,会执行 GraphicsJNI::createBitmap 来创建 Bitmap。而在 Android 8.0及以上时,会执行 Bitmap::createBitmap 来创建 Bitmap。
因此我们可以在不同Android版本上分别拦截这两个函数,就可以获取到图片的宽高等信息。从而达到监控图片内存的效果。
Native Hook
要实现拦截 native 函数的效果,需要使用 native hook 库。Native hook的开源库有字节的ShadowHook,和ByteHook。ShadowHook和ByteHook的区别如下:
比较维度 | ShadowHook | ByteHook |
---|---|---|
实现原理 | Android Inline Hook技术,修改目标函数前几条汇编指令,使其跳转到Hook函数 | Android PLT Hook框架,修改PLT中的地址,指向Hook函数代码地址 |
Hook方式 | 在目标函数被调用时,将其跳转到Hook函数中执行,并在Hook函数中处理完毕后再跳回到原函数继续执行。这种方式需要复制一份原函数的代码并进行修改,会占用一定的内存空间。 | 在目标函数被调用时,将其直接跳转到Hook函数中执行,并不会再跳回到原函数继续执行。这种方式不需要复制原函数的代码,因此不会占用额外的内存空间。 |
兼容性 | 相对较差,需针对不同系统版本和架构处理目标函数汇编代码的变化 | 相对较好,PLT表结构稳定,不随系统版本和架构大幅变化 |
适用场景 | 适用于需对目标函数执行过程详细监控和修改、对Hook精度要求高的场景,如安全领域监控系统关键函数、性能分析中精确测量特定函数 | 适用于兼容性要求高、无需详细监控原函数执行过程的场景,如应用开发中Hook第三方库函数、插件化框架中Hook系统加载插件函数 |
这里使用 ShadowHook 来实现 hook 效果。hook 的生效需要 函数运行时所在动态库名字 和 运行时函数符号的名称。
- 函数运行时所在动态库名字
在 Android 10 以前,我们需要监控的 GraphicsJNI::createBitmap 或者 Bitmap::createBitmap 方法都会被编译成 libandroid_runtime.so
;而在 Android 10 以后,则会编译到 libhwui.so
中去。
- 运行时函数符号的名称
由于编译C++ 代码后函数名称会被编译器修改(这个过程叫作name mangle),所以我们在hook时无法直接使用代码文件中的函数名称,而要使用编译后的函数名称。我们可以使用 readelf 查看某个函数在动态库里的最终名称。
//Android 8.0 以前的图片创建函数符号名称
#define BITMAP_CREATE_SYMBOL_BEFORE_8 "_ZN11GraphicsJNI12createBitmapEP7_JNIEnvPN7android6BitmapEiP11_jbyteArrayP8_jobjecti"
//Android 8.0 及以后的图片创建函数符号名称
#define BITMAP_CREATE_SYMBOL_RUNTIME "_ZN7android6bitmap12createBitmapEP7_JNIEnvPNS_6BitmapEiP11_jbyteArrayP8_jobjecti"
实现图片内存监控
ShadowHook 依赖和配置
首先我们需要增加 ShadowHook 依赖和设置好 ShadowHook 的配置,才能使用 ShadowHook 的功能。代码示例如下:
android {
...
defaultConfig {
ndk {
// 指定一个或多个你需要的 ABI
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
}
}
buildFeatures {
prefab = true
}
// 避免打包时遇到重复的 libshadowhook.so 文件的错误
packaging {
packaging {
jniLibs {
pickFirsts.add("**/libshadowhook.so")
pickFirsts.add("**/libshadowhook_nothing.so")
}
}
}
}
dependencies {
implementation("com.bytedance.android:shadowhook:1.1.1")
}
配置完成后,我们需要初始化 ShadowHook 的功能,代码示例如下:
ShadowHook.init(new ShadowHook.ConfigBuilder()
.setMode(ShadowHook.Mode.SHARED)
.build());
内存监控逻辑
首先我们先创建 BitmapProxy.h 文件,在里面定义函数运行时所在动态库名字、运行时函数符号的名称等信息。代码示例如下:
#define LOG_TAG "NativeLog"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#ifndef NATIVEDEMO_BITMAPPROXY_H
#define NATIVEDEMO_BITMAPPROXY_H
#define BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10 "libhwui.so"
//Bitmap 创建的逻辑在 libandroid_runtime.so 中
#define BITMAP_CREATE_SYMBOL_SO_RUNTIME "libandroid_runtime.so"
//Android 8.0 以前的图片创建函数符号名称
#define BITMAP_CREATE_SYMBOL_BEFORE_8 "_ZN11GraphicsJNI12createBitmapEP7_JNIEnvPN7android6BitmapEiP11_jbyteArrayP8_jobjecti"
//Android 8.0 及以后的图片创建函数符号名称
#define BITMAP_CREATE_SYMBOL_RUNTIME "_ZN7android6bitmap12createBitmapEP7_JNIEnvPNS_6BitmapEiP11_jbyteArrayP8_jobjecti"
// Android 10.0 对应的 API LEVEL
#define API_LEVEL_10_0 29
// Android 8.0 对应的 API LEVEL
#define API_LEVEL_8_0 26
class BitmapProxy {
public:
void startProxy();
void stopProxy();
};
#endif //NATIVEDEMO_BITMAPPROXY_H
然后在 BitmapProxy.cpp 中实现定义的 startProxy
和 stopProxy
方法。
startProxy
的代码示例如下:
/**
* 开始Bitmap代理的函数
*/
void BitmapProxy::startProxy() {
// 获取当前Android设备的API级别
int api_level = android_get_device_api_level();
// 根据API级别选择不同的SO库名称
// 如果API级别大于10.0,则使用BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10,否则使用BITMAP_CREATE_SYMBOL_SO_RUNTIME
auto so = api_level > API_LEVEL_10_0 ? BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10 : BITMAP_CREATE_SYMBOL_SO_RUNTIME;
// 根据API级别选择不同的符号名称
// 如果API级别大于等于8.0,则使用BITMAP_CREATE_SYMBOL_RUNTIME,否则使用BITMAP_CREATE_SYMBOL_BEFORE_8
auto symbol = api_level >= API_LEVEL_8_0 ? BITMAP_CREATE_SYMBOL_RUNTIME : BITMAP_CREATE_SYMBOL_BEFORE_8;
LOGD("api_level: %d, so: %s, symbol: %s", api_level, so, symbol);
// 核心代码:使用ShadowHook的shadowhook_hook_sym_name函数对指定SO库中的指定符号进行Hook
// 将原始函数替换为create_bitmap_proxy函数,并获取ShadowHook的stub信息
auto shadowhookStub = shadowhook_hook_sym_name(so, symbol, (void *) create_bitmap_proxy, nullptr);
LOGD("hook success");
// 将获取到的ShadowHook stub信息存储到静态变量stub中,以便后续取消Hook操作
stub = shadowhookStub;
}
stopProxy
的代码示例如下:
/**
* 停止Bitmap代理的函数
*/
void BitmapProxy::stopProxy() {
// 使用ShadowHook的shadowhook_unhook函数取消之前的Hook操作
// 传入之前存储的stub信息来指定要取消Hook的目标
shadowhook_unhook(stub);
}
完整代码如下:
#include <jni.h>
#include "BitmapProxy.h"
#include "shadowhook.h"
#include <android/log.h>
// 定义一个静态指针变量stub,用于存储ShadowHook的stub信息,后续用于取消Hook操作
static void *stub;
/**
* 创建Bitmap代理对象的函数
*
* @param env JNI环境指针,用于与Java虚拟机进行交互
* @param bitmap 原始的Bitmap对象指针
* @param bitmap_create_flags 创建Bitmap时的标志位
* @param nine_patch_chunk 九切图数据的字节数组
* @param nine_patch_insets 九切图的边距信息对象
* @param density 图片的密度
* @return 创建的Bitmap代理对象
*/
jobject create_bitmap_proxy(JNIEnv *env, void *bitmap,
int bitmap_create_flags, jbyteArray nine_patch_chunk, jobject nine_patch_insets,
int density) {
// 输出日志,标记create_bitmap_proxy函数开始执行
LOGD("create_bitmap_proxy start");
// 声明一个ShadowHook的栈作用域,用于管理Hook相关的栈信息
SHADOWHOOK_STACK_SCOPE();
// 调用ShadowHook的函数,执行原始的create_bitmap_proxy函数,并获取其返回值
// 这里使用SHADOWHOOK_CALL_PREV宏来调用被Hook前的原始函数
jobject bitmap_obj = SHADOWHOOK_CALL_PREV(create_bitmap_proxy, env, bitmap, bitmap_create_flags,
nine_patch_chunk, nine_patch_insets, density);
if (bitmap_obj != nullptr) {
// 获取 Bitmap 类的引用
jclass bitmapClass = env->GetObjectClass(bitmap_obj);
if (bitmapClass != nullptr) {
// 获取 getWidth 方法的 ID
jmethodID getWidthMethod = env->GetMethodID(bitmapClass, "getWidth", "()I");
if (getWidthMethod != nullptr) {
// 调用 getWidth 方法获取宽度
jint width = env->CallIntMethod(bitmap_obj, getWidthMethod);
// 获取 getHeight 方法的 ID
jmethodID getHeightMethod = env->GetMethodID(bitmapClass, "getHeight", "()I");
if (getHeightMethod != nullptr) {
// 调用 getHeight 方法获取高度
jint height = env->CallIntMethod(bitmap_obj, getHeightMethod);
// 获取 getByteCount 方法的 ID
jmethodID getByteCountMethod = env->GetMethodID(bitmapClass, "getByteCount", "()I");
if (getByteCountMethod != nullptr) {
// 调用 getByteCount 方法获取字节数
jint byteCount = env->CallIntMethod(bitmap_obj, getByteCountMethod);
// 将字节数转换为 KB
float sizeInKB = (float)byteCount / 1024.0f;
// 打印宽度、高度和大小
LOGD("Bitmap width: %d, height: %d, size: %.2f KB", width, height, sizeInKB);
}
}
}
// 释放局部引用
env->DeleteLocalRef(bitmapClass);
}
}
// 输出日志,标记create_bitmap_proxy函数执行结束
LOGD("create_bitmap_proxy end");
// 返回创建的Bitmap对象
return bitmap_obj;
}
/**
* 开始Bitmap代理的函数
*/
void BitmapProxy::startProxy() {
// 获取当前Android设备的API级别
int api_level = android_get_device_api_level();
// 根据API级别选择不同的SO库名称
// 如果API级别大于10.0,则使用BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10,否则使用BITMAP_CREATE_SYMBOL_SO_RUNTIME
auto so = api_level > API_LEVEL_10_0 ? BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10 : BITMAP_CREATE_SYMBOL_SO_RUNTIME;
// 根据API级别选择不同的符号名称
// 如果API级别大于等于8.0,则使用BITMAP_CREATE_SYMBOL_RUNTIME,否则使用BITMAP_CREATE_SYMBOL_BEFORE_8
auto symbol = api_level >= API_LEVEL_8_0 ? BITMAP_CREATE_SYMBOL_RUNTIME : BITMAP_CREATE_SYMBOL_BEFORE_8;
// 输出日志,记录当前设备的API级别、选择的SO库名称和符号名称
LOGD("api_level: %d, so: %s, symbol: %s", api_level, so, symbol);
// 使用ShadowHook的shadowhook_hook_sym_name函数对指定SO库中的指定符号进行Hook
// 将原始函数替换为create_bitmap_proxy函数,并获取ShadowHook的stub信息
auto shadowhookStub = shadowhook_hook_sym_name(so, symbol, (void *) create_bitmap_proxy, nullptr);
// 输出日志,标记Hook操作成功
LOGD("hook success");
// 将获取到的ShadowHook stub信息存储到静态变量stub中,以便后续取消Hook操作
stub = shadowhookStub;
}
/**
* 停止Bitmap代理的函数
*/
void BitmapProxy::stopProxy() {
// 使用ShadowHook的shadowhook_unhook函数取消之前的Hook操作
// 传入之前存储的stub信息来指定要取消Hook的目标
shadowhook_unhook(stub);
}
外部使用
Bitmap内存监控完成后,就可以使用 jni 来调用 BitmapProxy.cpp 中定义的方法了。
#include <jni.h>
#include "BitmapProxy.h"
#include <android/log.h>
static BitmapProxy *bitmapProxy = nullptr;
// 实现 startProxyBitmap 方法
extern "C" void Java_com_example_bitmapproxy_BitmapProxyUtils_startProxyBitmap(JNIEnv *env, jclass clazz) {
LOGD("startProxyBitmap called");
if (bitmapProxy == nullptr) {
bitmapProxy = new BitmapProxy();
}
bitmapProxy->startProxy();
}
// 实现 stopProxyBitmap 方法
extern "C" void Java_com_example_bitmapproxy_BitmapProxyUtils_stopProxyBitmap(JNIEnv *env, jclass clazz) {
LOGD("stopProxyBitmap called");
if (bitmapProxy == nullptr) {
return;
}
bitmapProxy->stopProxy();
delete bitmapProxy;
bitmapProxy = nullptr;
}
最后在 CMakeLists.txt 文件中配置,代码示例如下:
cmake_minimum_required(VERSION 3.22.1)
project("bitmapproxy")
find_package(shadowhook REQUIRED CONFIG)
add_library(bitmapproxy SHARED
BitmapProxy.h
BitmapProxy.cpp
BitmapProxyUtils.cpp)
find_library( # Sets the name of the path variable.
android_bitmap_lib
# Specifies the name of the NDK library that
# you want CMake to locate.
jnigraphics )
target_link_libraries(bitmapproxy
shadowhook::shadowhook
android
log
${android_bitmap_lib}
)
当我们创建 Bitmap 是,就可以获取到创建 Bitmap的相关信息了。运行结果如下:
2025-02-25 21:44:23.761 10643-10643 NativeLog com.example.nativedemo D startProxyBitmap called
2025-02-25 21:44:23.761 10643-10643 NativeLog com.example.nativedemo D api_level: 34, so: libhwui.so, symbol: _ZN7android6bitmap12createBitmapEP7_JNIEnvPNS_6BitmapEiP11_jbyteArrayP8_jobjecti
2025-02-25 21:44:23.784 10643-10643 NativeLog com.example.nativedemo D hook success
2025-02-25 21:44:23.825 10643-10643 NativeLog com.example.nativedemo D create_bitmap_proxy start
2025-02-25 21:44:23.825 10643-10643 NativeLog com.example.nativedemo D Bitmap width: 3916, height: 2676, size: 40934.44 KB
2025-02-25 21:44:23.825 10643-10643 NativeLog com.example.nativedemo D create_bitmap_proxy end
前面的文章只是简单的介绍了如何监控图片内存的知识,更多关于图片内存监控的知识可以看 [AndroidBitmapMonitor] 库的源码。