Android Jni/NDK 开发入门详解

本人为初学者,文章写得不好,如有错误,请大力怼我


如何使用jni进行开发

本文主要针对Android环境进行NDK\Native\Jni开发进行介绍

使用2.2版本之前的Android Studio进行ndk开发是比较繁琐的,如果你还在使用旧版本的Android Studio,那么建议更新到3.0,现阶段3.0已经比较稳定了(虽然旧项目的gradle升级可能需要折腾一下)。下面介绍旧版本的开发流程只是为了能够更加详细地介绍jni。

jni并不是android框架内的概念,所以也会提及其他环境使用jni开发的方法,基本上大同小异,不过你可能还需要查阅其他文章来处理一些细节问题(如Windows下生成dll文件)


AS 2.2之前的做法


1.编写C/C++

  • 首先创建一个java文件,声明一个自定义的native方法,对我们来说,这个方法就是java层到native层的入口,另外,还需要使用静态域将so包加载进来
    package com.linjiamin.jnishare;
    
    /**
    * Created by Albert on 17/11/16.
    */
    
    public class JniUtil {
    
        static {
            System.loadLibrary("sotest");
        }
    
        public static native int sum(int num1,int num2);
    }
  • 开始编写 C/C++代码之前我们需要两个头文件。其中一个是 jni.h,该头文件包含了对jni数据类型和接口的定义(之后还会介绍),现在开始你所编写的所有C/C++代码都需要引入这个头文件。另外你还需要一个根据刚刚编写的native方法签名及类信息生成的头文件。对前者,简单地include进来即可,而对于后者,可以使用javah命令生成,当然你也可以选择亲自编写,使用命令生成的方法如下
    //在终端中
    cd app/src/main/java
    javac com/linjiamin/jnishare/JniUtil.java
    javah com.linjiamin.jnishare.JniUtil
    
    //生成的头文件如下
    
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_linjiamin_jnishare_JniUtil */
    
    #ifndef _Included_com_linjiamin_jnishare_JniUtil
    #define _Included_com_linjiamin_jnishare_JniUtil
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
    * Class:     com_linjiamin_jnishare_JniUtil
    * Method:    sum
    * Signature: (II)I
    */
    JNIEXPORT jint JNICALL Java_com_linjiamin_jnishare_JniUtil_sum 
    (JNIEnv *, jclass, jint, jint);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
  • 简单说一下如何手写这个头文件,预处理指令的写法都是相同的,将完整类名替换进去即可,对于函数签名,从左到右按以下顺序编写,当然还是使用javah方法生成更好
    JNIEXPORT: 在android和linux中是空定义的宏,而在windows下被定义为__declspec(dllexport),具体的作用我们不需要关心
    
    jni数据类型:如jint,jboolean,jstring,它们对应于本地方法的返回类型(int,boolean,String)之后会进一步介绍、
    
    JNICALL : 这是__stdcall等函数调用约定(calling conventions)的宏,这些宏用于提示编译器该函数的参数如何入栈(从左到右,从右到左),以及清理堆栈的方式等
    
    方法名: Java + 完整类名 + 方法名
    
    参数列表:JNIEnv * + jclass\jobject + 所有你定义的参数所对应的jni数据类型 ,JNIEnv*是指向jvm函数表的指针,如果该方法为静态方法则第二个参数为class否则为jobject,它是含有该方法的class对象或实例
    
    注意JNIEXPORT和JNICALL是固定的
  • 函数具体实现如下,相信大家都能看懂
    #include "jni.h"
    #include "com_linjiamin_jnishare_JniUtil.h"
    //
    // Created by Albert Humbert on 17/11/17.
    //
    
    JNIEXPORT jint JNICALL Java_com_linjiamin_jnishare_JniUtil_sum
    (JNIEnv * env, jclass obj, jint num1, jint num2){
      return num1 + num2;
    }


2.使用ndk编译so包


包结构
  • 现在在main包下创建一个jni包,将你的头文件和c/c++文件放进去,然后,你还需要两份mk文件,mk文件是makefile文件的一部分,makefile包含c/c++编译器的编译命令、顺序和规则,如果你不了解makefile是什么,那也没什么关系,后面会讲解Android.mk和Application.mk文件的书写规范
  • 注意请在Android Library中进行ndk开发,不要使用Java Library,前者会生成aar包,可以包含so以及其他资源文件,后者会生成jar,jar通常只能调用外部so包,网上也有文章将jar当中的so包用文件流写到本地调用的,建议不要尝试这种骚操作


编写Android.mk
  • 一个最基本的Android.mk如下
    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := libsotest
    LOCAL_SRC_FILES := com_linjiamin_jnishare_JniUtil.cpp
    include $(BUILD_SHARED_LIBRARY)
  • LOCAL_PATH := $(call my-dir),Android.mk必须以该属性开头,它用于指定源文件的路径,my-dir是一个返回Android.mk文件所在目录的宏
  • include $(CLEAR_VARS) ,指定负责文件的清理的makefile文件,一般固定这么写就好
  • LOCAL_SRC_FILES,需要编译的c/c++文件,如无后缀则默认为cpp文件
  • include $(BUILD_SHARED_LIBRARY) ,收集上次清理后的源文件信息,并决定如何编译
  • LOCAL_C_INCLUDES,头文件的搜索路径
  • TARGET_ARCH,指定AB,如armeabi,armeabi-v7a


编写Application.mk
  • 一个典型的Application.mk文件如下
    APP_PLATFORM = android-24
    APP_ABI := armeabi,armeabi-v7a,x86_64,arm64-v8a
    APP_STL := stlport_static
    APP_OPTIM := debug
  • APP_PLATFORM,ndk版本号,你可以在ndk-bundle文件夹中查看所本地ndk版本
    # for mac
    /Users/alberthumbert/Library/Android/sdk/ndk-bundle/platforms
  • APP_ABI,指定APP_ABI版本,这会决定ndk编译出的so包数量,关于ABI的介绍见下文,推荐至少包含armeabi或armeabi-v7a
  • APP_STL 如何连接c++标准库 ,包括 stlport_static ,stlport_shared ,system,分别表示静态,动态,系统默认
  • APP_OPTIM,包括debug,和release,这会决定so中是否包含调试信息
  • APP_MODULES,填写so包的名字,如果没有这个属性,则按照Android.mk中的进行命名,注意如果文件中含有多个该属性,则会按照先后顺序为你编译出来的so文件命名
  • 填写完这两个mk文件之后,需要在gradle中指定so库的路径,gradle会自动将so文件打包进来,在andorid闭包中添加
    sourceSets.main {
    jniLibs.srcDir 'src/main/libs'
    jni.srcDirs = []
    }
  • 如无意外这个时候我们的项目就可以运行起来了,打印log如下
    11-17 16:33:37.563 3824-3824/? D/JniUtil: test: 2


人生苦短,我用AS 3.0


自动生成函数

  • 如果出于不幸、粗心、经验不足等原因,你的项目无法运行,请不要怀疑你的智商,下面为你带来傻瓜式的ndk开发流程
  • 最新版的AS,可以为你使用ndk开发提供很大的方便,请确保你在SDK Tools中下载了CMake、LLDB、NDK
  • 首先创建一个新项目,并勾选Include C++ support
  • C++ Standard中可以选择使用的C++标准,默认是CMake所使用的标准,Exceptions Support可以启用C++异常处理,一般这些选项使用默认的就可以了
  • 项目创建完毕之后你可以看见官方已经为你做好了很多工作,并且带了一个c++的hello world示例,你需要关注的主要有cpp和External Build Files 两个目录,前者用于放置你的C++源文件,后者根据不同的ABI版本放置了CMake脚本
  • 接着我们直接在MainActivity中添加一个native方法,然后选中该方法,按下alt+enter,让IDE为我们自动生成C++函数
        public native boolean booleanFromJNI();
  • 在native-lib.cpp中可以看见自动生成的函数,我们只需要实现该函数即可
    JNIEXPORT jboolean JNICALL
    Java_com_linjiamin_myapplication_MainActivity_booleanFromJNI(JNIEnv *env, jobject instance) {
    
        // TODO
    
    }
  • 注意使用上面的方法你可以在任意一个java文件中声明native方法,IDE会自动在native-lib.cpp中为你生成对应的函数签名,当然,你也不是非要把所有的C/C++代码都写在一个文件里,下面来讲解一下CMakeList的基本写法


编写CMakeList

  • 下面是一份官方写好的CMakeList.txt,这个文件可以在你当前项目的app目录下看到
    add_library( # 设置编译出来的so包的名字. 不需要添加lib前缀
                 native-lib
    
                 # 设置为共享链接库. 有SHARED,STATIC两种可选
                 SHARED
    
                 # 设置源文件的相对路径,可将多个源文件进行编译
                 src/main/cpp/native-lib.cpp )
    
    # Searches for a specified prebuilt library and stores the path as a
    # variable. Because CMake includes system libraries in the search path by
    # default, you only need to specify the name of the public NDK library
    # you want to add. CMake verifies that the library exists before
    # completing its build.
    
    find_library( # 设置外部引用库.这个库支持你在c/c++中打印log,具体请见  android/log.h
                log-lib
                # 外部引用库的名称
                log )
    
    # Specifies libraries CMake should link to your target library. You
    # can link multiple libraries, such as libraries you define in this
    # build script, prebuilt third-party libraries, or system libraries.
    
    target_link_libraries( # 指定被链接的库.
                        native-lib
    
                        # 链接log-lib到native-lib
                        ${log-lib} )
  • 现在我们想要将不同的源文件编译成多份so包,例如我在cpp目录下添加一份test-lib.cpp文件,代码如下
    extern "C"
    JNIEXPORT jboolean JNICALL
    Java_com_linjiamin_myapplication_JniUtil_booleanFromJNI(JNIEnv *env, jobject instance) {
    
    //上面提到的log库可以这么使用,而且你应该使用宏让它好看些
    __android_log_print(ANDROID_LOG_DEBUG,"stringFromJNI","%d",0);
    return (jboolean) true;
    
    }
  • 那么可以在刚刚的CMakeList中设定我们的so包,在最后加上
    add_library( test-lib SHARED src/main/cpp/test-lib.cpp )
  • 编译之后 可以看到 build/intermediates/cmake/debug/obj/ 路径下不同的ABI目录中都有了两份so文件,分别是libnative-lib.so,libtest-lib.so
  • 如果你在不同的路径下放置了源文件,并且希望对于每一个特定的路径都有一份自己特定的CMakeList文件来描述这些源文件的打包规则(这看起来是个好习惯),可以使用add_subdirectory("目录名")方法指定子路径,子路径当中的放置CMakeList会被执行


使用g++编译so包

  • 对于非安卓开发者,这里再简单介绍一个使用g++编译so包的方法,使用这种方法你无需ndk环境,也不用编写mk、CMakeList文件,完全使用命令行进行编译,当然我更推荐你去学习cmake
  • 编写java文件并用javah指令生成头文件,再编写cpp文件,这个流程对于不同平台的jni开发都是相同的(虽然Intelligent Idea这种IDE可以为你自动生成头文件),那么现在需要一份对应平台下的jni.h文件,可以在你的jdk当中查找,编译器可能还会提示你需要一份jni_md.h文件,它也在jdk当中
    $ cd /Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk
    $ find . -iname jni.h
    ./Contents/Home/include/jni.h
  • 我这里用的是ndk当中的jni.h文件,下载ndk之后在sdk当中可以找到
    $ cd /Users/alberthumbert/Library/Android/sdk/ndk-bundle
    $ find . -iname jni.h
    ./sysroot/usr/include/jni.h
  • 现在假定你以及有了一份java文件,两份头文件,一份cpp或c文件,那么可以使用如下命令将他们编译成so文件,注意最后的参数不一定是必要的,如果你想编译安卓平台可用的so包那么建议加上
    g++ com_linjiamin_jnishare_JniUtil.cpp -fPIC -shared -o libsotest.so -Wl,--hash-style=sysv

*注意,nix平台使用lib表示一个so库,所以so文件必须以lib开头,但加载时请将lib前缀去掉**

  • 你可以用绝对路径加载一个so文件
    System.load("\***\***\***.so")
  • 你也可以将so文件放入系统加载路径当中,调用 System.getProperty("java.library.path")方法得到系统加载路径,想要修改这个路径,可以在修改.bashrc(或者当前使用的其他shell)中添加
    export PATH=PATH:/XXX
  • 运行jar时动态指定路径也是可以的,这样会暂时覆盖上面写入的属性
    java -jar -Djava.library.path=/**/**   **.jar
  • 重新在java代码中加载so文件,*nix平台注意去掉lib前缀
    System.loadLibrary ("***");


ABI与so

  • 这里再啰嗦一下别的东西,可以先跳过,之后再倒回来看
  • 由于目前我们的项目很简单,没有用到第三方so库和也没有去除多余so库为apk瘦身,因此不用考虑兼容问题,但实际开发项目时通常没这么简单。我们知道,编译出来的so是二进制文件,由于不同的CPU支持不同的指令集,所以我们需要考虑兼容性的问题。一个包含多种指令集及其相关约定的实现被称之为ABI,一个CPU架构支持一种到多种ABI。安卓平台就是针对ABI进行编译和打包的。
  • 可能看了上面这一段会比较晕,那么我就举一个例子来说明,比如ARMv7架构的CUP,支持 armeabi和armeabi-v7a两种ABI,而armeabi这种ABI支持Thumb-1,ARMV5TE等指令集,armeabi-v7a这种ABI又支持Thumb-2和VFPv3-D16等指令集,也就是说,一种CPU架构对应多种ABI类型,一种ABI类型对于多种指令集
  • 一个so文件只支持一种ABI,因此你会发现在lib下每一个包都是以ABI来命名的,同名的so文件被按照其支持的ABI进行分类
  • 目前ABI一共有七种,那么是不是意味者我们的每一个so都需要编译成七种,然后全都打包进apk当中呢,答案是否定的。目前CUP流行的架构主要有ARM系列,x86,x86_64,但移动设备大部分都是ARMv7架构,少数是ARM架构,由于ARMv7架构兼容armeabi,因此类似淘宝、微信、饿了么的国内大厂通常只使用armeabi一种ABI,Facebook,Twitter等外国大厂则是只保留了armeabi-v7a,这是十分合理的,apk只保留一种通用的ABI,而最适应的so可以在外部去下载
  • 那么是不是只要编译一种so文件就可以了呢?不完全正确,如果你引用了第三方的ndk,而第三方在兼容性做得比较好的情况下适配了多种ABI,又或者目前你的lib下so包的数量参差不齐。当一个apk安装时就有可能查找到了最适用的ABI的路径存在,但里面又没有想要的so,这时它不会自动去查找其他ABI版本的so,而是会crash,为了解决这个问题,请在lib包下只保留一个包(通常是armeabi或者armeabi-v7a),或者每个名字的so在不同包下都存在对应版本,并且在app的gradle的defaultConfig闭包中添加你所适配好的ABI,这样安装时只会从你所指定的ABI中查找so包
    ndk{
        abiFilters  "armeabi-v7a", "x86", "armeabi"
    }


什么是jni

现在我们已经可以进行简单的ndk开发了,但为了加深理解认识,让我再来啰嗦一下jni

看过这一部分之后你对jni应该会有更近一步的感性认识


jni.h


jni数据类型

  • 现在来看看c/c++层的数据类型是怎么对应到java层的
  • 首先是基本类型,根据java中的定义定义了j*类型
    /* Primitive types that match up with Java equivalents. */
    typedef uint8_t  jboolean; /* unsigned 8 bits */
    typedef int8_t   jbyte;    /* signed 8 bits */
    typedef uint16_t jchar;    /* unsigned 16 bits */
    typedef int16_t  jshort;   /* signed 16 bits */
    typedef int32_t  jint;     /* signed 32 bits */
    typedef int64_t  jlong;    /* signed 64 bits */
    typedef float    jfloat;   /* 32-bit IEEE 754 */
    typedef double   jdouble;  /* 64-bit IEEE 754 */
  • 对于引用类型,c和c++有区别,在c++中 jobject是类,而jstring和各种类型的数组都是jobject的子类的指针,在c中jobject是一个void*指针,而其他引用类型其实都是jobject
    class _jobject {};
    class _jclass : public _jobject {};
    class _jstring : public _jobject {};
    class _jarray : public _jobject {};
    class _jobjectArray : public _jarray {};
    class _jbooleanArray : public _jarray {};
    class _jbyteArray : public _jarray {};
    class _jcharArray : public _jarray {};
    class _jshortArray : public _jarray {};
    class _jintArray : public _jarray {};
    class _jlongArray : public _jarray {};
    class _jfloatArray : public _jarray {};
    class _jdoubleArray : public _jarray {};
    class _jthrowable : public _jobject {};
    
    typedef _jobject*       jobject;
    typedef _jclass*        jclass;
    typedef _jstring*       jstring;
    typedef _jarray*        jarray;
    typedef _jobjectArray*  jobjectArray;
    typedef _jbooleanArray* jbooleanArray;
    typedef _jbyteArray*    jbyteArray;
    typedef _jcharArray*    jcharArray;
    typedef _jshortArray*   jshortArray;
    typedef _jintArray*     jintArray;
    typedef _jlongArray*    jlongArray;
    typedef _jfloatArray*   jfloatArray;
    typedef _jdoubleArray*  jdoubleArray;
    typedef _jthrowable*    jthrowable;
    typedef _jobject*       jweak;
    
    
    /*
     * Reference types, in C.
     */
    typedef void*           jobject;
    typedef jobject         jclass;
    typedef jobject         jstring;
    typedef jobject         jarray;
    typedef jarray          jobjectArray;
    typedef jarray          jbooleanArray;
    typedef jarray          jbyteArray;
    typedef jarray          jcharArray;
    typedef jarray          jshortArray;
    typedef jarray          jintArray;
    typedef jarray          jlongArray;
    typedef jarray          jfloatArray;
    typedef jarray          jdoubleArray;
    typedef jobject         jthrowable;
    typedef jobject         jweak;
  • jvalue是一个比较特殊的联合体,一般在需要调用java层方法时做为方法参数传入,如 void CallVoidMethodA(jobject obj, jmethodID methodID, jvalue* args) ,通过jobject和表示其方法的jmethodID即可特定一个具体的方法,然后将我们的jvalue作为函数列表传入
    typedef union jvalue {
            jboolean    z;
            jbyte       b;
            jchar       c;
            jshort      s;
            jint        i;
            jlong       j;
            jfloat      f;
            jdouble     d;
            jobject     l;
        } jvalue;
  • 在这一方面我们可以讨论的内容比较少,总的来说,由于C/C++ 中基本类型的字节数依赖与实现,所以在native层转换到java层是不能直接使用原本的int,long等类型而是根据java中的约定使用jni.h指定了相同长度与有符号的类型,而java中的类则可以使用类或结构体的指针来解决


常用的接口

在讲解JNIEnv和JavaVM之前先来尝试一下各种jni的基本操作,版本较新的AS已经支持了对C/C++ 的智能提示和代码补全功能,你可以很方便地试用JNIEnv提供的接口

这里只介绍几个例子,以后有时间我会另写文章介绍这些接口,强烈推荐你使用AS把可调用的函数浏览并选择性地使用一遍


修改成员变量

  • 通过之前的例子你应该已经知道怎么从native层中获取一个变量了,现在再进一步,我们使用native方法直接改变成员变量的值,在MainActivity中定义一个native方法
    public class MainActivity extends AppCompatActivity {
    
        public String mString = null;
    
    static {
            System.loadLibrary("native-lib");
        }
    
        private static final String TAG = "MainActivity";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Log.d(TAG, "onCreate: "+mString);
            getFieldFromJNI();
            Log.d(TAG, "onCreate: "+ mString);
        }
    
        public native String getFieldFromJNI();
    
    }
  • 在来看C++实现,注意你可能在阅览其他文章时发现调用jni函数时有两种不同的写法 env-> 和 (*env)->,这是由于C与C++的实现有所差异,不影响我们使用。如果你使用过反射改变成员变量的值,应该可以毫不费力地理解下面这段代码
    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_linjiamin_jnilearning_MainActivity_getFieldFromJNI(JNIEnv *env, jobject instance) {
    
        jclass clazz = env->GetObjectClass(instance);
        //获取调用者的class对象
        jfieldID  jfID = env ->GetFieldID(clazz,"mString","Ljava/lang/String;");
        //获取成员变量的键
        jstring strValue = (jstring) env->GetObjectField(instance, jfID);
        //获取成员变量的值,不做操作
        char chars[10] = "whatever";
        jstring newValue = env->NewStringUTF(chars);
        //创建一个String对象
        env->SetObjectField(instance,jfID,newValue);
        //设置新的值
        return strValue;
    }


创建引用


引用类型
  • 了解jni中的引用类型,有助于你编写高效的代码并且解决内存泄漏等问题,jni中的引用类型可以分为三种,局部引用,全局引用,弱(全局)引用,通常jvm会在函数返回后自动为你释放局部引用,但你需要自行管理全局引用的生命周期


局部引用
  • 其实我们之前已经接触过局部引用了,调用jni函数通常会创建新对象的实例并返回一个局部引用,局部引用只在一次native函数的调用周期中存在,在函数结束时被释放
  • 通常我们需要避免返回全局引用,而是返回创建出来的局部引用,例如这样
    return (jstring) env->NewLocalRef(someValue);
  • 我们刚刚说过,局部引用在函数的调用过程中存在,也就是说如果不进行人为的销毁操作,它将一直存在,在任意native函数中执行下面这段代码,你将接受到一个异常,跟java不同,gc不会及时回收通过这种方法创建出来的变量
    for(int i = 0;i<1000000000;i++){
        jstring newValue = env->NewStringUTF(chars);
    }
  • 你可以调用 DeleteLocalRef函数销毁一个局部引用,这个函数现在就可以执行了,不过他会比一般函数耗时些
        for(int i = 0;i<1000000000;i++){
            jstring newValue = env->NewStringUTF(chars);
            env->DeleteLocalRef(newValue);
        }


全局引用
  • 之前说过局部引用在函数的调用过程中存在,我们不能直接使用显式赋值的方式将局部引用强行将其缓存起来
    jobject gInstance;
    
    …
    
    extern "C" JNIEXPORT void JNICALL 
    Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
    methodID = env->GetMethodID(clazz,"sayHello","()V");
    gInstance = instance;
    ...
    }
  • 上面的例子将会报错,不过对于jni开发,你可能常常只能收到含糊的报错信息,甚至收不到报错信息
    JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0x7fff871642e0
  • 我们可以使用 NewGlobalRef 函数来创建一个全局引用,但是注意,你需要对自己的行为负责,全局引用只有在你的手动调用 DeleteGlobalRef 函数之后才会被释放,你可以在JNI_OnLoad 中进行缓存工作,在JNI_OnUnload函数中进行缓存的清除
    jobject gInstance;
    
    …
    extern "C" JNIEXPORT void JNICALL 
    Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
    
    methodID = env->GetMethodID(clazz,"sayHello","()V");
    gInstance = env->NewGlobalRef(instance);
    ...
    }
    
    …
    
    void fun() {
    ...
    ...
    env->DeleteGlobalRef(gInstance);
    }


弱引用
  • 弱引用和全局引用大体上类似,但是当内存不足时它会被GC回收,通过 NewWeakGlobalRef 函数可以创建一个弱引用,和Java层的弱引用一致,它不会阻止自己所指向的对象被GC回收
    gInstance = env->NewWeakGlobalRef(instance);
  • 但是这不意味着你可以不用管理弱引用的生命周期,在不需要它时请主动释放弱引用,注意,弱引用的释放不会导致它所指向的对象被GC回收
    env->DeleteWeakGlobalRef(gInstance)
  • 最好在使用弱引用时判断它的对象是否已被释放,你可能会理所当然地使用 == 进行判断,这种方法是错误的,除非这个引用从来就没有被初始化过,不然表达式将永远为真,解决方案是使用jni提供的接口进行比较,有的文章也推荐再次使用NewWeakGlobalRef来达到这样的效果,个人认为这两种方案除了在可读性上的区别外没什么不同
    if (env->IsSameObject(gInstance,NULL)) {
        __android_log_print(ANDROID_LOG_DEBUG,"fun","%s","instance is NULL");
    }
    
        //或者
    
    if (!gInstance || !env->NewWeakGlobalRef(gInstance)) {
        __android_log_print(ANDROID_LOG_DEBUG,"fun","%s","instance is NULL");
    }


JNIEnv,JavaVM 以及多线程

  • 你可能已经意识到,目前为止我们都是通过JNIEnv来使用jni的,实际上JNIEnv提供了Native函数的基础环境,具体来说,它包含了一个指向函数表的指针,这也就是为什么我们需要通过JNIEnv才能调用native方法,JNIEnv也代表了具体的进程环境,因此不允许跨进程调用,最好的做法是永远不要缓存JNIEnv,你可以通过JavaVM来创造它的实例
  • JavaVM是java虚拟机的代表,它可以跨线程调用,它是一个全局对象,典型的jni环境中一个进程可以有多个JavaVM,但是在安卓环境当中他在每个进程中只有一个实例,通常你可以在JNI_OnLoad 函数,或其可以获取JNIEnv的地方得到它_
        env->GetJavaVM(&gVm);
  • 下面在C++线程中模仿耗时操作,并调用Java层方法传回数据,首先定义接受数据的方法和一个native方法,这里的参数列表稍微定义得复杂一点,方便之后演示jvalue的使用方法
    public void resultCallback(boolean isSuccess,int result,String data){
        Log.d(TAG, "resultCallback: "+ isSuccess + " "+result + " " +data);
    }
    
    public native void useCPlusThread();
  • native方法的实现如下,这里我们通过GetJavaVM方法得到了JavaVM对象,JavaVM用于我们之后获取JNIEnv,同时我们把调用者通过全局引用缓存起来,注意这里的methodID不需要使用NewGlobalRef,它是一个结构体,直接赋值即可,由于java支持重载,需要输入方法函数列表的标识才可以特定一个方法,每个基本类型都有其对应的缩写,而对于类我们需要通过包名和类名来指定。然后我们开启五个线程进行耗时操作
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
    
    env->GetJavaVM(&gVm);
    jclass clazz = env->GetObjectClass(instance);
    
    
    methodID = env->GetMethodID(clazz, "resultCallback", "(ZILjava/lang/String;)V");
    gInstance = env->NewGlobalRef(instance);
    
    pthread_t pthread[5];
    
    for(int i = 0;i<5;i++){
        pthread_create(&pthread[i], NULL, &fun, NULL);
    }
    }
  • 线程方法的实现如下,linux系统的sleep函数定义在unistd.h文件中,我们使用它来模仿耗时操作,像刚刚说过的一样JNIEnv不能跨进程调用,那么这里使用AttachCurrentThread函数得到实例,这个函数同时也会将当前线程绑定到JavaVM上,然后我们使用CallVoidMethodA来调用刚刚缓存起来的实例的方法,也就是java层的resultCallback方法,jvalue数组可以作为参数列表传入,另外你也可以使用更为简便的CallVoidMethod函数,最后记得使用DetachCurrentThread函数解绑,除非你使用DeleteLocalRef函数释放引用,不然你通过JNIEnv获取的局部引用在你调用DetachCurrentThread之前都不会被销毁,并且在函数结束后造成内存泄漏
    void *fun(void *arg) {
    sleep(3);
    
    JNIEnv *env;
    if (gVm->AttachCurrentThread(&env, NULL) != JNI_OK) {
        __android_log_print(ANDROID_LOG_DEBUG, "callJniInDifferentThread", "%s", "attach failed");
        return NULL;
    }
    
    jvalue * args = new jvalue[3];
    args[0].z = (jboolean) true;
    args[1].i = 1000;
    args[2].l = env->NewStringUTF("some data");
    
    env->CallVoidMethodA(gInstance, methodID, args);
    
    if (gVm->DetachCurrentThread() != JNI_OK) {
        __android_log_print(ANDROID_LOG_DEBUG, "callJniInDifferentThread", "%s", "detach failed");
    }
    
    return NULL;
    
    }
  • 注,各种类型对应的缩写如下,请使用一下的缩写特定具体的方法
类型 缩写
Boolean Z
Byte B
Char C
Short S
Int I
Long L
Float F
Double D
Void V
Object 以"L"开头,以";"结尾,中间是用"/" 隔开的包及类名。比如:Ljava/lang/String;如果是嵌套类,则用$来表示嵌套。例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
返回值与参数 例 (IB)L 表示返回类型为long,参数为int和byte的函数


内存泄漏


局部引用的内存模型

  • 刚刚提到JVM在一定程度上会为你管理局部引用的生命周期,但这不意味着局部引用等于局部变量。每当线程从Java层切换到native层时,JVM会创建局部引用表,它们维系了你的C/C++变量和Java层变量。
  • 下图中出现的本地方法栈是和虚拟机栈类似的一种概念,但它用于运行native方法,注意,规范只约定了jni的操作和使用方法,对实现没有明确的要求,有些虚拟机会将虚拟机栈与本地方法栈合并实现,这里只大致地描绘本地方法栈的结构。当java层切入到native层(以下简称J2N过程,反之为N2J),或者在native函数中调用了jni接口时会导致本地方法栈的入栈操作,本地引用表在J2N时创建,并在N2J时销毁,在这个过程中,每当局部引用被合法创建,该局部引用都会被添加到表中并映射到java堆中的一个对象
  • 看回前面这个例子,我们在循环中不断创建新的局部引用,并且赋值给变量newValue,这些不断创建的引用并不会立即释放,并且我们之后也无法获取到这些还留在表中的引用,所以他们都导致了内存泄漏。一般情况下局部引用表分配到的内存空间很小,这种内存泄漏很容易就会导致内存溢出,虚拟机崩溃。为了编写更加安全流畅的代码,我建议你遵循下面几个规范
    for(int i = 0;i<1000000000;i++){
        jstring newValue = env->NewStringUTF(chars);
    }


引用的使用规范

  • native编程首先需要遵循C/C++自身的内存管理机制,除了局部引用以外,JVM不会为你做更多的内存释放工作,所以当你使用malloc函数分配内存空间后必须使用free函数进行释放,这和其他平台上的C/C++编程没什么不同
  • 全局变量对java层对象的引用一直有效,请在不用时进行删除,否它所指向的对象将一直留在堆中
  • 和刚刚介绍局部引用时说的一样,在函数返回之前,局部引用不会自动释放,如果创建过多的引用将会导致内存溢出的风险,如果你的函数只会创建为数不多的局部引用,那么完全可以将删除引用的操作交给JVM去处理,但如果你的函数会创建大量的引用,特别是在开启循环的请况下,请自行调用DeleteLocalRef函数


推荐阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容