服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

如何在 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. 如果使用指针,那么就要记得deletedelete[],防止内存泄漏。
  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 给小姐姐做了几个抖音滤镜


相关阅读

  • 《三体漫画》:独家收藏的宇宙奇想

  • 《三体漫画》正式出版!刘慈欣官方授权,改编自《三体》第一部。全书共10册,“考据式”改编补足海量细节(扫码即可购买,适合年龄:10岁+) 本书简介 《三体漫画》,当然就是由《三体》改
  • 行情该下跌了!

  • 内资:艹,老外每天100亿,这钱怎么花不完的?外资:淦,每天100亿买进,为什么A股还不涨?市场就是这么个市场~段子背后,我想聊点有用的...首先是外资,我11月大底的时候写过这篇长文【战点】
  • 大家好!时隔一个春节假期,距离上一期发车已经有接近1个月的时间了。快来看看本期的跟车指南吧!今年1月份A股市场主要指数普遍上涨,成长板块相对涨幅靠前,市场情绪整体较高。我们
  • 在经历了接近3个月的反弹后,A股在3300点上下震荡,近期市场出现了较为明显的调整。调整的主要原因可能是前期市场反弹迅速,出现正常回调和部分短期资金的获利了结,且市场缺乏新的

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

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

  • 一、为什么要定位Native层的内存泄漏?最近做一个OpenGL ES项目,使用C++来实现的。在自测阶段,发现内存有泄漏,特别是Native内存泄漏的很明显。如下所示:刚开始启动应用的时候,只打
  • Android-Native 开发之利用 AAudio 播放音频

  • 前言谈到在Android C/C++层实现音频播放/录制功能的时候,大家可能首先会想到的是利用opensles去做,这确实是一直不错的实现方式,久经考验,并且适配比较广。但如果你的项目最低版