粉丝服务

如何在 NDK 开发的时候定位 Native 层的内存泄漏?

一、为什么要定位Native层的内存泄漏?

最近做一个OpenGL ES项目,使用C++来实现的。在自测阶段,发现内存有泄漏,特别是Native内存泄漏的很明显。如下所示:

刚开始启动应用的时候,只打开相机,渲染相机获取的帧数据中途打开了OpenGL ES特效来渲染,可以看到Native层内存增大很多最后,退出录制页面,到APP主页,这时候不渲染图像了,但是Native内存有66.8MB,比最开始的49.3MB明显多了十几MB,泄漏了!!!!

所以,我们得定位一下Native层的内存泄漏,否则会用起来越来越卡。

二、如何快速定位Native层的内存泄漏?

2.1 LeakTracer简单介绍

其中Android Studio配合MAT可以有快速定位Java内存泄漏,但是Native定位我没找到很好的办法。后来我搜索到了如下几篇文章,了解到 LeakTracer 这个可以检测出Native的内存泄漏。

  • Android NDK 内存泄露检测
  • Android Native内存泄露检测
  • https://github.com/fredericgermain/LeakTracer

我们知道Andorid中Java层代码内存泄露可以借助LeakCanary进行检测;内存泄露检测库LeakTracer,在APP运行的时候进行检测,就像LeakCanary一样。

注意事项:

  1. LeakTracer只能在简单的场景下检测Native层代码;
  2. 复杂项目,比如依赖多个native module, 或者依赖其他module的native代码,或者依赖的不是Native代码而是so库,就容易出现漏报误报的情况。

2.2 LeakTracer 示例代码

最终,参考了如下两个示例代码之后,

  • https://github.com/wangshengyang1996/AndroidLeakTracer
  • https://github.com/lizhangqu/NDKMemoryLeakSample

我自己fork了下项目,如下所示:https://github.com/ouyangpeng/AndroidLeakTracer

在这里插入图片描述

2.3 实际演练一遍集成LeakTracer

最终,我们通过一个之前我的项目,来实际集成一下LeakTracer检测Native内存泄漏,并修复它!!!

https://github.com/ouyangpeng/NDKDemo以下写的内容,都在这两次提交记录里面,读者可以自行查看!!!

  • 1. 添加 leaktracer 检测 native 内存泄漏
  • 2. 解决所有 leaktracer 检测出来的 native 内存泄漏

2.4 集成LeakTracer到项目中

2.4.1 LeakTracer源代码

在这里插入图片描述

如上图所示,我们直接将LeakTracer的源代码复制到目录  app/src/main/cpp/libleaktracer 即可。

2.4.2 LeakTracer 检测控制接口

然后我们新增一个app/src/main/cpp/LeakTracerJni.cpp文件,主要是新增的LeakTracer控制相关的JNI实现,如下所示:

#include <jni.h>
#include <jni.h>
#include <jni.h>
#include \"libleaktracer/include/MemoryTrace.hpp\"

//
// Created by OuyangPeng on 2022/3/17.
//
extern \"C\"
JNIEXPORT void JNICALL
Java_com_oyp_ndkdemo_JNI_startMonitoringAllThreads(JNIEnv *env, jobject clazz) {
    leaktracer::MemoryTrace::GetInstance().startMonitoringAllThreads();
}
extern \"C\"
JNIEXPORT void JNICALL
Java_com_oyp_ndkdemo_JNI_startMonitoringThisThread(JNIEnv *env, jobject clazz) {
    leaktracer::MemoryTrace::GetInstance().startMonitoringThisThread();
}
extern \"C\"
JNIEXPORT void JNICALL
Java_com_oyp_ndkdemo_JNI_stopMonitoringAllocations(JNIEnv *env, jobject clazz) {
    leaktracer::MemoryTrace::GetInstance().stopMonitoringAllocations();
}
extern \"C\"
JNIEXPORT void JNICALL
Java_com_oyp_ndkdemo_JNI_stopAllMonitoring(JNIEnv *env, jobject clazz) {
    leaktracer::MemoryTrace::GetInstance().stopAllMonitoring();
}
extern \"C\"
JNIEXPORT void JNICALL
Java_com_oyp_ndkdemo_JNI_writeLeaksToFile(JNIEnv *env, jobject clazz, jstring filePath) {
    const char *path = env->GetStringUTFChars(filePath, JNI_FALSE);
    leaktracer::MemoryTrace::GetInstance().writeLeaksToFile(path);
    env->ReleaseStringUTFChars(filePath, path);
}

对应的Kotlin代码,如下所示:

在原来的app/src/main/java/com/oyp/ndkdemo/JNI.kt中,添加如下代码

 /**
     * starts monitoring memory allocations in all threads
     */
    external fun startMonitoringAllThreads()

    /**
     * starts monitoring memory allocations in current thread
     */
    external fun startMonitoringThisThread()

    /**
     * stops monitoring memory allocations (in all threads or in
     * this thread only, depends on the function used to start
     * monitoring
     */
    external fun stopMonitoringAllocations()

    /**
     * stops all monitoring - both of allocations and releases
     */
    external fun stopAllMonitoring()


    /**
     * writes report with all memory leaks
     */
    private external fun writeLeaksToFile(filePath: String)

    /**
     * writes report with all memory leaks
     */
    fun writeLeaksResultToFile(filePath: String?) {
        if (filePath == null) {
            throw NullPointerException(\"filePath is null\")
        }
        val file = File(filePath)
        require(!(file.exists() && file.isDirectory)) { \"can not write data to a directory\" }
        require(!(!file.parentFile.exists() && !file.parentFile.mkdirs())) { \"can not create parent folder for file \'\" + file.absolutePath + \"\'\" }
        writeLeaksToFile(filePath)
    }

2.4.3 CMakeLists.txt的修改

app/src/main/cpp/CMakeLists.txt文件只需要添加刚刚新增的

2.4.4 别忘记添加sdcard写权限

别忘记添加sdcard写权限,因为最终我们检测到的native内存泄漏需要输出到sdcard的某个文件中,此案例我们输出到sdcard/NativeLeakLog.txt中

现在app/src/main/AndroidManifest.xml描述文件添加如下代码

<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>

然后在app/src/main/java/com/oyp/ndkdemo/MainActivity.kt中申请动态权限,如下所示:从上面,我们可以知道,我们在原有的JNI方法执行之前开启了检测,执行之后关闭检测并输出报告到sdcard/NativeLeakLog.txt文件中。

至此,我们集成完毕,下面来运行一下,看看有什么内侧泄漏吧!!!

2.5 运行一下,查看输出报告

重新编译代码,运行程序,最终会生成 /sdcard/NativeLeakLog.txt 检测报告,如下图所示:

2.5.1. 拷贝 /sdcard/NativeLeakLog.txt 到leak-tracer-helpers目录

我们将这个/sdcard/NativeLeakLog.txt导出,放到 leak-tracer-helpers 目录下。

2.5.2 拷贝 当前运行编译出来的libndkdemo.so文件 到leak-tracer-helpers目录

然后,将对应的so文件,放到这个leak-tracer-helpers 目录下。最终,如下所示:

2.5.3 在 AndroidLeakTracer\\leak-tracer-helpers 目录下 ,右键 使用 Git Bash Hear,然后模拟Linux环境运行下面的脚本

这里有使用到addr2line工具,关于这个工具,可以参考我另外一篇文章

  • 【我的Android进阶之旅】如何在Android Studio开发NDK的时候,通过addr2line来定位出错代码的位置 https://blog.csdn.net/ouyang_peng/article/details/123333538
$ ./leak-analyze-addr2line libndkdemo.so NativeLeakLog.txt

这些就是我这个项目中的Native内存泄漏!!!

2.5.4 分析报告对应的源代码

按照上面的报告,去查看源代码,确实是这些指针没有释放

下面3个指针,我在转换数据的时候,都没有释放。

native-lib.cpp:325native-lib.cpp:410native-lib.cpp:385native-lib.cpp:88native-lib.cpp:278

native-lib.cpp:105native-lib.cpp:300native-lib.cpp:440

最终分析代码,确实是这些指针都没有释放,导致了内存泄漏。那么我们修复它就好了!!!

2.6 修复内存泄漏

修复的代码,可以参考这一次提交记录查看:

  • 2. 解决所有 leaktracer 检测出来的 native 内存泄漏
  1. 释放char* 指针
  2. 结构体改成class,增加析构函数。数组指针改成使用vector集合来实现,这样就减少了3个指针成员变量。

然后我们定义bean的时候改成定义指针将之前的数组指针都改成vector来实现最后别忘记了是否new出来的NativeFaceFeatureBean指针

2.7 重新运行,查看是否还有内存泄漏?

重复之前的操作。

  1. 拷贝 /sdcard/NativeLeakLog.txt 到leak-tracer-helpers目录 我们将这个/sdcard/NativeLeakLog.txt导出,放到 leak-tracer-helpers 目录下。

  2. 拷贝 当前运行编译出来的libndkdemo.so文件 到leak-tracer-helpers目录 然后,将对应的so文件,放到这个leak-tracer-helpers 目录下。

  3. 在 AndroidLeakTracer\\leak-tracer-helpers 目录下 ,右键 使用 Git Bash Hear,然后模拟Linux环境运行下面的脚本

$ ./leak-analyze-addr2line libndkdemo.so NativeLeakLog.txt
  1. 报告如下,没有内存泄漏!!!好吧,我们把所有的内存泄漏解决了!!!

2.8 修复内存泄漏方法2

也可以直接不改结构体为class,直接增加结构体的析构函数,去释放指针。

给结构体加上析构函数和构造函数,和class类似。 但是我们还是维持着指针吧,这样改动比之前小一点

struct NativeFaceFeatureBean {
    NativeFaceFeatureBean() {
        faceId = 0;
        yaw = 0;
        pitch = 0;
        roll = 0;
        boundingBox = nullptr;
        landmarks = nullptr;
        visibilities = nullptr;
    }

    ~NativeFaceFeatureBean() {
        if (boundingBox) {
            delete[] boundingBox;
            boundingBox = nullptr;
        }
        if (landmarks) {
            delete[] landmarks;
            landmarks = nullptr;
        }
        if (visibilities) {
            delete[] visibilities;
            visibilities = nullptr;
        }
    }
    int faceId;
    float yaw;
    float pitch;
    float roll;
    NativePointF *boundingBox;
    NativePointF *landmarks;
    float *visibilities;
};

还原成结构体,和class类似数组指针还原成数组指针

完整代码如下

typedef struct {
    float x;
    float y;
} NativePointF;

struct NativeFaceFeatureBean {
    NativeFaceFeatureBean() {
        faceId = 0;
        yaw = 0;
        pitch = 0;
        roll = 0;
        boundingBox = nullptr;
        landmarks = nullptr;
        visibilities = nullptr;
    }

    ~NativeFaceFeatureBean() {
        if (boundingBox) {
            delete[] boundingBox;
            boundingBox = nullptr;
        }
        if (landmarks) {
            delete[] landmarks;
            landmarks = nullptr;
        }
        if (visibilities) {
            delete[] visibilities;
            visibilities = nullptr;
        }
    }
    int faceId;
    float yaw;
    float pitch;
    float roll;
    NativePointF *boundingBox;
    NativePointF *landmarks;
    float *visibilities;
};

extern \"C\"
JNIEXPORT void JNICALL
Java_com_oyp_ndkdemo_JNI_nativeSetFaceFeatureBean(JNIEnv *env, jobject thiz, jobject feature) {
    LOGD(\"=================================Java_com_oyp_ndkdemo_JNI_nativeSetFaceFeatureBean=================================\")
    // 获取FaceFeatureBean类
    jclass jFeature = env->GetObjectClass(feature);

    // 获取FaceFeatureBean对象的methodID
    jmethodID getFaceId = env->GetMethodID(jFeature, \"getFaceId\", \"()I\");
    jmethodID getYaw = env->GetMethodID(jFeature, \"getYaw\", \"()F\");
    jmethodID getPitch = env->GetMethodID(jFeature, \"getPitch\", \"()F\");
    jmethodID getRoll = env->GetMethodID(jFeature, \"getRoll\", \"()F\");

    // 执行方法 拿到属性
    jint faceId = env->CallIntMethod(feature, getFaceId);
    jfloat yaw = env->CallFloatMethod(feature, getYaw);
    jfloat pitch = env->CallFloatMethod(feature, getPitch);
    jfloat roll = env->CallFloatMethod(feature, getRoll);
//    LOGD(\"faceId = %d,yaw = %f,pitch = %f,roll = %f\", faceId, yaw, pitch, roll)


    auto* faceFeatureBean = new NativeFaceFeatureBean();
    faceFeatureBean->faceId = faceId;
    faceFeatureBean->yaw = yaw;
    faceFeatureBean->pitch = pitch;
    faceFeatureBean->roll = roll;


    jmethodID getVisibilities = env->GetMethodID(jFeature, \"getVisibilities\", \"()Ljava/util/List;\");
    jobject visibilities = env->CallObjectMethod(feature, getVisibilities);

    // 遍历 visibilities,拿到List的各个Item
    // 获取ArrayList对象
    jclass jcs_alist = env->GetObjectClass(visibilities);
    // 获取ArrayList的methodid
    jmethodID alist_get = env->GetMethodID(jcs_alist, \"get\", \"(I)Ljava/lang/Object;\");
    jmethodID alist_size = env->GetMethodID(jcs_alist, \"size\", \"()I\");
    jint visibilitiesSize = env->CallIntMethod(visibilities, alist_size);

    faceFeatureBean->visibilities = new float[visibilitiesSize];

    for (int i = 0; i < visibilitiesSize; i++) {
        // 获取 Float 对象
        jobject float_obj = env->CallObjectMethod(visibilities, alist_get, i);
        // 获取 Float 类
        jclass float_cls = env->GetObjectClass(float_obj);
        jmethodID getFloatValue = env->GetMethodID(float_cls, \"floatValue\", \"()F\");
        jfloat floatValue = env->CallFloatMethod(float_obj, getFloatValue);
//        LOGD(\"visibilities列表中 第 %d 个值为:floatValue = %f\", i + 1, floatValue)

        faceFeatureBean->visibilities[i] = floatValue;
    }


    jmethodID getBoundingBox = env->GetMethodID(jFeature, \"getBoundingBox\",
                                                \"()[Landroid/graphics/PointF;\");
    jobject boundingBox = env->CallObjectMethod(feature, getBoundingBox);
    auto *boundingBoxArray = reinterpret_cast<jobjectArray *>(&boundingBox);

    const jint boundingBoxSize = env->GetArrayLength(*boundingBoxArray);

    faceFeatureBean->boundingBox = new NativePointF[boundingBoxSize];

    for (int i = 0; i < boundingBoxSize; i++) {
        jobject point = env->GetObjectArrayElement(*boundingBoxArray, i);
        //1.获得实例对应的class类
        jclass jcls = env->GetObjectClass(point);
        //2.通过class类找到对应的field id
        //num 为java类中变量名,I 为变量的类型int
        jfieldID xID = env->GetFieldID(jcls, \"x\", \"F\");
        jfieldID yID = env->GetFieldID(jcls, \"y\", \"F\");

        jfloat x = env->GetFloatField(point, xID);
        jfloat y = env->GetFloatField(point, yID);
//        LOGD(\"boundingBoxArray数组中 第 %d 个值为:x = %f , y = %f\", i + 1, x, y)

        faceFeatureBean->boundingBox[i].x = x;
        faceFeatureBean->boundingBox[i].y = y;
    }

    jmethodID getLandmarks = env->GetMethodID(jFeature, \"getLandmarks\",
                                              \"()[Landroid/graphics/PointF;\");
    jobject landmarks = env->CallObjectMethod(feature, getLandmarks);
    auto *landmarksArray = reinterpret_cast<jobjectArray *>(&landmarks);
    const jint landmarksSize = env->GetArrayLength(*landmarksArray);

    faceFeatureBean->landmarks = new NativePointF[landmarksSize];

    for (int i = 0; i < landmarksSize; i++) {
        jobject point = env->GetObjectArrayElement(*landmarksArray, i);
        //1.获得实例对应的class类
        jclass jcls = env->GetObjectClass(point);
        //2.通过class类找到对应的field id
        //num 为java类中变量名,I 为变量的类型int
        jfieldID xID = env->GetFieldID(jcls, \"x\", \"F\");
        jfieldID yID = env->GetFieldID(jcls, \"y\", \"F\");

        jfloat x = env->GetFloatField(point, xID);
        jfloat y = env->GetFloatField(point, yID);
//        LOGD(\"landmarksArray 第 %d 个值为:x = %f , y = %f\", i + 1, x, y)
        faceFeatureBean->landmarks[i].x = x;
        faceFeatureBean->landmarks[i].y = y;
    }
    showNativeFaceFeatureBean(*faceFeatureBean, boundingBoxSize, landmarksSize, visibilitiesSize);

    delete faceFeatureBean;
}

重新编译运行,也是没有内存泄漏了!

最后,我将两种解决内存泄漏的方法都写在代码里,读者可以自行查阅。

  • https://github.com/ouyangpeng/NDKDemo/blob/main/app/src/main/cpp/native-lib.cpp
  • 解决所有 leaktracer 检测出来的 native 内存泄漏,分别使用struct和class两种实现完成

三、总结和参考链接

总结:

  1. 如果对指针掌控不好,那就避免使用指针吧,改用STL的容器来实现数组。
  2. 如果使用指针,那么就要记得delete和delete[],防止内存泄漏。
  3. 检测内存泄漏,可以学习本篇博客,然后集成LeakTracer到项目中去动态检测。
  4. 结构体也得注意加上析构函数,来delete和delete[]掉指针成员变量

参考链接:

  • Android NDK 内存泄露检测
  • Android Native内存泄露检测
  • https://github.com/fredericgermain/LeakTracer
  • https://github.com/wangshengyang1996/AndroidLeakTracer
  • https://github.com/lizhangqu/NDKMemoryLeakSample

我自己的项目,如下所示:

  • https://github.com/ouyangpeng/AndroidLeakTracer
  • https://github.com/ouyangpeng/NDKDemo
  • https://github.com/ouyangpeng/NDKDemo/blob/main/app/src/main/cpp/native-lib.cpp
  • 解决所有 leaktracer 检测出来的 native 内存泄漏,分别使用struct和class两种实现完成
  • 【我的Android进阶之旅】如何在Android Studio开发NDK的时候,通过addr2line来定位出错代码的位置 https://blog.csdn.net/ouyang_peng/article/details/123333538

-- END --


进技术交流群,扫码添加我的微信:Byte-Flow 



获取相关资料和源码



推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

抖音传送带特效是怎么实现的?

所有你想要的图片转场效果,都在这了

面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?

我用 OpenGL ES 给小姐姐做了几个抖音滤镜


本栏目精选

热门推荐

频道导航