一、为什么要定位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一样。
注意事项:
LeakTracer只能在简单的场景下检测Native层代码; 复杂项目,比如依赖多个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 内存泄漏
释放char* 指针 结构体改成class,增加析构函数。数组指针改成使用vector集合来实现,这样就减少了3个指针成员变量。
然后我们定义bean的时候改成定义指针将之前的数组指针都改成vector来实现最后别忘记了是否new出来的NativeFaceFeatureBean指针
2.7 重新运行,查看是否还有内存泄漏?
重复之前的操作。
拷贝 /sdcard/NativeLeakLog.txt 到
leak-tracer-helpers目录 我们将这个/sdcard/NativeLeakLog.txt导出,放到leak-tracer-helpers目录下。拷贝 当前运行编译出来的libndkdemo.so文件 到
leak-tracer-helpers目录 然后,将对应的so文件,放到这个leak-tracer-helpers目录下。在 AndroidLeakTracer\leak-tracer-helpers 目录下 ,右键 使用 Git Bash Hear,然后模拟Linux环境运行下面的脚本
$ ./leak-analyze-addr2line libndkdemo.so NativeLeakLog.txt
报告如下,没有内存泄漏!!!好吧,我们把所有的内存泄漏解决了!!!
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两种实现完成
三、总结和参考链接
总结:
如果对指针掌控不好,那就避免使用指针吧,改用STL的容器来实现数组。 如果使用指针,那么就要记得 delete和delete[],防止内存泄漏。检测内存泄漏,可以学习本篇博客,然后集成 LeakTracer到项目中去动态检测。结构体也得注意加上析构函数,来 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 音视频和 OpenGL ES 干货,都在这了
面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?