Android NDK 示例(六)图片内存监控

321 阅读10分钟

在 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。

image.png

因此我们可以在不同Android版本上分别拦截这两个函数,就可以获取到图片的宽高等信息。从而达到监控图片内存的效果。

Native Hook

要实现拦截 native 函数的效果,需要使用 native hook 库。Native hook的开源库有字节的ShadowHook,和ByteHook。ShadowHook和ByteHook的区别如下:

比较维度ShadowHookByteHook
实现原理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 中实现定义的 startProxystopProxy 方法。

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] 库的源码。

参考