native开发 内存注意事项
Posted baiiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了native开发 内存注意事项相关的知识,希望对你有一定的参考价值。
前言
本文记录在native开发注意事项,会持续更新。
native开发会遇到的问题:
- JNI层面,各种reference;
- native内存泄露,使用一些开源库检测,如Raphael等;
- native内存管理不当,溢出、重复释放或错误释放,接入asan解决
JNI注意事项
-
JNI_ONLOAD函数只调用一次
-
获取非局部引用的唯一方法是通过 NewGlobalRef 和 NewWeakGlobalRef 函数。
-
请注意,jfieldID 和 jmethodID 属于不透明类型,不是对象引用,且不应传递给 NewGlobalRef。
-
如果您只有一个类具有原生方法,那么合理的做法是应该将对 System.loadLibrary 的调用置于该类的静态初始化程序中。否则,您可能需要从 Application 进行该调用,这样您就知道始终会加载该库,而且总是会提前加载。
-
JNI层中创建的jobject对象默认是局部引用(Local Reference)。当函数从JNI层返回后,Local reference的对象很可能被回收。所以,不能在JNI层中永久保存一个Local Reference的对象。在for循环中可以提前使用deleteLocalReference减少内存使用;
使用newGlobalReference将这个Local Reference对象转换成Global Reference(全局引用)对象。在程序结束时需要使用deleteGlobalReference删除该全局引用;
当然,为了减少内存占用,进程能持有的全局引用对象的总个数有所限制。 -
LocalReference:
本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。我们也可以通过DeleteLocalRef方法手动释放。
没有及时回收Local Reference或许是进程占用内存过多的一个原因,请务必注意这一点。 -
Global Reference:
全局引用,这种对象如不主动释放,它永远不会被垃圾回收。
全局引用可以跨多个线程,在多个函数中都有效。全局引用需要通过NewGlobalRef方法手动创建,对应的释放全局引用的方法为DeleteGlobalRef -
Weak Global Reference:
弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
弱引用也需要自己手动创建,作用和全局引用的作用相似,不同点在于弱引用不会阻止垃圾回收器对引用所指对象的回收。我们可以通过NewWeakGlobalRef方法来创建弱引用,也可以通过DeleteWeakGlobalRef来释放对应的弱引用
for (int i = 0; i < 100; ++i) {
jstring str = mEnv->NewStringUTF("aaa");
// do sth
// 可以在jstring用完就删除,节约内存;
mEnv->DeleteLocalReference(str);
}
native内存泄露
西瓜视频稳定性治理体系建设二:Raphael 原理及实践
使用LeakTracer分析Android native内存泄漏
Android PLT hook 概述
-
native内存泄露的危害:
- OOM
线程无法创建啊等的OOM崩溃; - 虚拟内存耗尽,再分配时会返回null,使用时导致崩溃,或判断为空直接崩溃
测试使用malloc(1024 * 1024)一直分配,最后虚拟内存耗尽时,会返回null指针;
可以通过adb shell cat proc/[pid]/statm
查看内存分配情况;
- OOM
-
native内存检测库设计注意点:
- hook so加载
- hook malloc、free等函数
- unwind获取堆栈
- 内存缓存设计,本地缓存设计、上报设计
native内存操作错误
接入ASan,用于检测原生代码中的内存错误。
-
支持版本
从 API 级别 27 (android O MR 1) 开始,Android NDK 可支持 Address Sanitizer(也称为 ASan)。 -
ASan 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。ASan 可以检测以下问题:
堆栈和堆缓冲区上溢/下溢
释放之后的堆使用情况
超出范围的堆栈使用情况
重复释放/错误释放 -
接入
// build.gradle文件中添加:
defaultConfig {
externalNativeBuild {
cmake {
# Can also use system or none as ANDROID_STL.
arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
}
}
}
// CMakeLists.txt 文件中添加:
target_compile_options(${libname} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
set_target_properties(${libname} PROPERTIES LINK_FLAGS -fsanitize=address)
// 新建wrap.sh文件
#!/system/bin/sh
HERE="$(cd "$(dirname "$0")" && pwd)"
export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)
if [ -f "$HERE/libc++_shared.so" ]; then
# Workaround for https://github.com/android-ndk/ndk/issues/988.
export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
else
export LD_PRELOAD="$ASAN_LIB"
fi
"$@"
// 在ndk目录下使用`find . -name "*asan*.so"` 找到asan的so,并拷贝wrap.sh到相应目录下
<project root>
└── app
└── src
└── main
├── jniLibs
│ ├── arm64-v8a
│ │ └── libclang_rt.asan-aarch64-android.so
│ ├── armeabi-v7a
│ │ └── libclang_rt.asan-arm-android.so
│ ├── x86
│ │ └── libclang_rt.asan-i686-android.so
│ └── x86_64
│ └── libclang_rt.asan-x86_64-android.so
└── resources
└── lib
├── arm64-v8a
│ └── wrap.sh
├── armeabi-v7a
│ └── wrap.sh
├── x86
│ └── wrap.sh
└── x86_64
└── wrap.sh
官方文档:asan
Native (C++) 开发中如何使用 ASan 检测内存错误
Android Address Sanitizer (ASan) 原理简介
以上是关于native开发 内存注意事项的主要内容,如果未能解决你的问题,请参考以下文章
Native (C++) 开发中如何使用 ASan 检测内存错误
react-native踩坑合集,来源于真实企业开发(建议收藏)