native开发 内存注意事项

Posted baiiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了native开发 内存注意事项相关的知识,希望对你有一定的参考价值。

前言

本文记录在native开发注意事项,会持续更新。

native开发会遇到的问题:

  1. JNI层面,各种reference;
  2. native内存泄露,使用一些开源库检测,如Raphael等;
  3. native内存管理不当,溢出、重复释放或错误释放,接入asan解决

JNI注意事项

官方文档:JNI tips
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);

JNI 局部引用溢出

Android JNI 局部引用溢出(local reference table overflow (max=512))

  • 局部引用:
    当在Native层调用JNI函数(例如:FindClass、NewObject、GetObjectClass和NewCharArray等等)创建Java对象时,该Java对象会被添加到局部引用表中。通过添加到局部引用表中,可以阻止GC回收局部引用表引用的对象。
    当从Native函数返回Java层后,局部引用所引用的对象会被JVM自动释放。当然也可以手动调用DeleteLocalRef来释放局部引用。(*env)->DeleteLocalRef(env,local_ref)。

  • 局部引用有如下特点:
    Native方法中的局部引用有效期是Native方法的调用期间。
    调用期间产生的局部引用除非手动删除,不然就会一直累加
    在Native方法中创建子线程并运行,然后在子线程中通过JNI函数创建的Java对象所产生的局部引用只有在这个线程结束运行才会自动删除。

native内存泄露

西瓜视频稳定性治理体系建设二:Raphael 原理及实践
使用LeakTracer分析Android native内存泄漏
Android PLT hook 概述

  • native内存泄露的危害:

    1. OOM
      线程无法创建啊等的OOM崩溃;
    2. 虚拟内存耗尽,再分配时会返回null,使用时导致崩溃,或判断为空直接崩溃
      测试使用malloc(1024 * 1024)一直分配,最后虚拟内存耗尽时,会返回null指针;
      可以通过adb shell cat proc/[pid]/statm查看内存分配情况;
  • native内存检测库设计注意点:

    1. hook so加载
    2. hook malloc、free等函数
    3. unwind获取堆栈
    4. 内存缓存设计,本地缓存设计、上报设计

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开发 内存注意事项

Native开发过程中容易忽略的注意事项

react-native注意事项,开发前必读文档

react-native注意事项,开发前必读文档

Native (C++) 开发中如何使用 ASan 检测内存错误

什么可能导致此内存问题?