JNI知识总结

概念

JNI:Java本地调用 ,是Java Native Interface的缩写。JNI是一种技术,可以做到以下两点:

  • Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
  • ·Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。

为什么需要jni

  • C/C++语言已经有了很多成熟的模块,Java只需要直接调用即可。还有一些追求效率和速度的场合,需要Native参与。
  • Java语言是平台无关,但是承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体平台上,所以虚拟机本身无法做到平台无关,JNI技术可以针对Java层屏蔽不同操作系统之间的差异,这样就能够实现平台无关特性。


    image.png

基本使用

  • 在Java代码里面声明Native方法原型,比如
public native String stringFromJNI();
  • java静态代码加载so库( JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数)
static {
        System.loadLibrary("JniDemo");
    }

JniDemo是JNI库的名字。实际加载动态库的时候会拓展成libJniDemo.so,在Windows平台上将拓展为JniDemo.dll。

  • java代码调用native函数
public class MainActivity extends AppCompatActivity {

    // Used to load the 'JniDemo' library on application startup.
    static {
        System.loadLibrary("JniDemo");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI()); // java代码调用native函数
    }

    /**
     * A native method that is implemented by the 'JniDemo' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}
  • 在C/C++代码里面声明JNI方法原型并实现
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
  1. extern "C"根据需要动态添加,如果是C++代码,则必须要添加extern “C”声明,如果是C代码,则不用添加。(extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。)
  2. JNIEXPORT 这个关键字说明这个函数是一个可导出函数,C/C++ 库里面的函数有些可以直接被外部调用,有些不可以,原因就是每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。 JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
  3. 说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别,实际发现不加这个关键字,Java也是可以调用这个JNI函数的。
  4. jstring 这个函数的返回值是jstring
  5. Java_com_example_JniDemo_MainActivity_stringFromJNI(JNIEnvenv, jobject thiz)这是完整的JNI函数声明,JNI函数名的原型如下:
    Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv
    env, jobject thiz), env参数是一个指向JNIEnv函数表的指针,thiz参数代表的就是声明这个JNI方法的Java类的引用

JNI调用流程

通过原生Android代码分析流程
java: MediaCrypto.java
jni:android_media_MediaCrypto.cpp

MediaCrypto.java 部分代码

 99        private static native final void native_init();  //声明一个native函数。native为Java的关键字,表示它将由JNI层完成。
100         
101  
102     
104      static {
105          System.loadLibrary("media_jni");  //加载对应的JNI库,media_jni是JNI库的名字
106          native_init(); //调用 jni native_init函数
107      }
108  
  • 加载JNI库
    Java要调用Native函数,就必须通过一个位于JNI层的动态库才能做到。加载动态库的时机原则上是在调用native函数前,任何时候、任何地方加载都可以。通行的做法是,在类的static语句中加载,通过调用System.loadLibrary方法就可以了。

  • Java的native函数
    从上面代码中可以发现,native_init函数前有Java的关键字native,它表示这两个函数将由JNI层来实现。、

  • 只要完成下面两项工作就可以使用JNI了:
    · 1. 加载对应的JNI库。
    · 2. 声明由关键字native修饰的函数。

android_media_MediaCrypto.cpp  部分代码

 static void android_media_MediaCrypto_native_init(JNIEnv *env) {  //这个函数是native_init的JNI层实现。
160      jclass clazz = env->FindClass("android/media/MediaCrypto");
161      CHECK(clazz != NULL);
162  
163      gFields.context = env->GetFieldID(clazz, "mNativeContext", "J");
164      CHECK(gFields.context != NULL);
165  }

android_media_MediaCrypto_native_init是native_init的jni 层实现, java层和native层需要将这2个函数绑定形成关联关系,系统才能找到它们,也就是接下来要说的注册.

注册

动态注册

在JNI技术中,用JNINativeMethod的结构来记录对应关系,其定义如下

typedef struct {
   const char* name;      //Java中native函数的名字,不用携带包的路径。例如“native_init“。
   const char* signature;//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
   void*      fnPtr;  //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;

如何使用这个数据结构呢,看下对应 android_media_MediaCrypto.cpp 文件代码

6  static const JNINativeMethod gMethods[] = {
307      { "release", "()V", (void *)android_media_MediaCrypto_release },
308      { "native_init",  Java中native函数的函数名。
                "()V",  native_init签名信息,后面再做介绍
                (void *)android_media_MediaCrypto_native_init //JNI层对应函数指针。
             }, 
310  
312      ...
324  };
325  
326  int register_android_media_Crypto(JNIEnv *env) { //注册JNINativeMethod数组
327      return AndroidRuntime::registerNativeMethods(env,
328                  "android/media/MediaCrypto", gMethods, NELEM(gMethods));
329  }
  • 定义一个JNINativeMethod数组,其成员就是所有native函数的一一对应关系。
  • 注册JNINativeMethod数组 registerNativeMethods .第二个参数表明是Java中的哪个类
    AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,实际上是调用JNIEnv的RegisterNatives函数完成注册的
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
320      const JNINativeMethod* methods, int numMethods)
321  {
322      ALOGV("Registering %s's %d native methods...", className, numMethods);
323      jclass clazz = (*env)->FindClass(env, className);
324      ALOG_ALWAYS_FATAL_IF(clazz == NULL,
325                           "Native registration unable to find class '%s'; aborting...",
326                           className);
327      int result = (*env)->RegisterNatives(env, clazz, methods, numMethods);
328      (*env)->DeleteLocalRef(env, clazz);
329   ...
347  }

注册函数register_android_media_Crypto调用时机

1463  jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
1464  {
1465      JNIEnv* env = NULL;
1466      jint result = -1;
1467  
1468      if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
1469          ALOGE("ERROR: GetEnv failed\n");
1470          goto bail;
1471      }
1472      assert(env != NULL);
1473  
1474      if (register_android_media_ImageWriter(env) != JNI_OK) {
1475          ALOGE("ERROR: ImageWriter native registration failed");
1476          goto bail;
1477      }
              ...
1554      if (register_android_media_Crypto(env) < 0) {
1555          ALOGE("ERROR: MediaCodec native registration failed");
1556          goto bail;
1557      }
           }

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。所以,如果想使用动态注册方法,就必须要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工作

静态注册

静态注册是根据函数名来找对应的JNI函数,它要求JNI层函数的名字必须遵循特定的格式, 函数名规则如下
Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv* env, jobjectthiz)
//native_init对应的JNI函数

//Java层函数名中如果有一个”_”的话,转换成JNI后就变成了”_l”。
JNIEXPORT void JNICALL Java_android_media_MediaCrypto_native_1init(JNIEnv* env, jclass thiz); 

当Java层调用native_init函数时,它会从对应的JNI库查找Java_android_media_MediaCrypto_native_linit,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaCrypto_native_linit建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
对应的jni头文件可以手写,不过比较麻烦, 可以使用 javah工具自动生成

  • 先编写Java代码,然后编译生成.class文件。
  • 使用Java的工具程序javah,如javah–o output packagename.classname ,这样它会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。头文件的名字一般都会使用packagename_class.h的样式,例如MediaCrypto对应的JNI层头文件就是android_media_MediaCrypto.h

弊端:

  1. 编译所有声明了native函数的Java类,每个生成的class文件都得用javah生成一个头文件。
  2. javah生成的JNI层函数名特别长,书写起来很不方便。
  3. 初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。

jdk10已经移除javah工具,相应的功能已经集成到javac中,你可以试试javac -h替代javah。

数据类型

在Java中调用native函数传递的参数是Java数据类型,这些参数类型到了JNI层会变成JNI对应的数据类型
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。

基本数据类型转换关系表
捕获.PNG
引用类型对照表
捕获.PNG

除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。

静态JNI方法和实例JNI方法参数区别
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI2(JNIEnv *env, jclass clazz) {
    // TODO: implement stringFromJNI2()
}

普通的JNI方法对应的JNI函数的第二个参数是jobject类型,而静态的JNI方法对应的JNI函数的第二个参数是jclass类型

JNIEnv的认识

概念

在JNI世界里离不开JNIEnv,JNIEnv是一个和线程相关的,代表JNI环境的结构体


image.png

JNIEnv提供了一些JNI系统函数,通过这些函数可以

  • 调用Java函数【jni层调用java层】
  • 操作jobject对象等很多事情【jni层调用native】

JNIEnv,是一个和线程有关的变量。线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。当后台线程收到一个网络消息,需要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中来用。

前面提到过JNI_OnLoad函数,第一个参数是JavaVM,它是虚拟机在JNI层的代表。不论检查中多少个线程,JavaVM独此一份,在任意地方都可以使用它。

如果我们需要在其他线程访问JVM,那么必须先调用AttachCurrentThread将当前线程与JVM进行关联,然后才能获得JNIEnv对象。

JavaVMAttachArgs args = {JNI_VERSION_1_4, NULL, NULL};
JavaVM* vm = AndroidRuntime::getJavaVM();
int result = vm->AttachCurrentThread(&env, (void*) &args);

当然,我们在必要时需要调用DetachCurrentThread来解除链接。

 JavaVM* vm = AndroidRuntime::getJavaVM();
int result = vm->DetachCurrentThread();

使用

通过JNIEnv操作jobject

Java的引用类型除了少数几个外,最终在JNI层都用jobject来表示对象的数据类型,操作jobject的本质是操作这些对象的成员变量和成员函数
JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数

通过JNIEnv的下面两个函数可以得到:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
  • jclass代表Java类(成员函数和成员变量都是类的信息)
  • name表示成员函数或成员变量的名字
  • sig为这个函数和变量的签名信息
 static void android_media_MediaCrypto_native_init(JNIEnv *env) {
160      jclass clazz = env->FindClass("android/media/MediaCrypto");
161      CHECK(clazz != NULL);
162  
163      gFields.context = env->GetFieldID(clazz, "mNativeContext", "J");
164      CHECK(gFields.context != NULL);
165  }
  • 先找到android/media/MediaCrypto类在JNI层中对应的jclass实例
  • 取出MediaCrypto类中变量mNativeContext的jfieldID
  • 将变量mNativeContext的jfieldID 保存起来,因为每次操作jobject前都去查询jmethoID或jfieldID的话将会影响程序运行的效率

接下来就是使用

 static sp<JCrypto> setCrypto(
142          JNIEnv *env, jobject thiz, const sp<JCrypto> &crypto) {
143      sp<JCrypto> old = (JCrypto *)env->GetLongField(thiz, gFields.context);
144      if (crypto != NULL) {
145          crypto->incStrong(thiz);
146      }
147      if (old != NULL) {
148          old->decStrong(thiz);
149      }
150      env->SetLongField(thiz, gFields.context, (jlong)crypto.get());
151  
152      return old;
153  }
  • 调用JNIEnv的GetLongField函数,第一个是代表MediaCrypto的jobject对象,第二个参数是mNativeContext的jfieldID
  • 通过JNIEnv输出的GetLongField,再把jobject和jMethodID(如果对应参数)传进去,JNI层就能够调用Java对象的字段了!

实际上JNIEnv输出了一系列类似GetLongField的函数,形式如下:
//获得fieldID后,可调用Get<type>Field系列函数获取jobject对应成员变量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//下面我们列出一些参加的Get/Set函数。
GetObjectField() SetObjectField()
GetBooleanField() SetBooleanField()

如果想调用Java中的static 字段,则用JNIEnv输出的GetStatic<Type>Field系列函数。

同理 通过JNIEnv操作jobject的成员函数和字段类似
形式如下:
NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)

调用Java中的static函数
JNIEnv输出的CallStatic<Type>Method系列函数

jstring

Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。Java中的String包含很多成员函数,但是jstring是一种独立的数据类型,并没有提供成员函数供操作。

操作jstring得依靠JNIEnv提供的帮助。
可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String

  • C/C++字符串转JNI字符串
    NewString函数用来生成Unicode JNI字符串
    NewStringUTF函数用来生成UTF-8 JNI字符串
  • JNI字符串转C/C++字符串
    GetStringChars函数用来从jstring获取Unicode C/C++字符串
    GetStringUTFChars函数用来从jstring获取UTF-8 C/C++字符串
  • 如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用
    ReleaseStringChars或ReleaseStringUTFChars函数对应地释放资源,否则会导致JVM内存泄露。

JNI类型签名

动态注册中的数组信息

static const JNINativeMethod gMethods[] = {
307      { "release", "()V", (void *)android_media_MediaCrypto_release },
308      { "native_init", "()V", (void *)android_media_MediaCrypto_native_init },
309  
310      { "native_setup", "([B[B)V",
311        (void *)android_media_MediaCrypto_native_setup },
312  
313      { "native_finalize", "()V",
314        (void *)android_media_MediaCrypto_native_finalize },
315  
316      { "isCryptoSchemeSupportedNative", "([B)Z",
317        (void *)android_media_MediaCrypto_isCryptoSchemeSupportedNative },
318  
319      { "requiresSecureDecoderComponent", "(Ljava/lang/String;)Z",
320        (void *)android_media_MediaCrypto_requiresSecureDecoderComponent },
321  
322      { "setMediaDrmSession", "([B)V",
323        (void *)android_media_MediaCrypto_setMediaDrmSession },
324  };

(void *)android_media_MediaCrypto_native_init 为函数native_init的签名信息,由参数类型和返回值类型共同组成
格式为:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示

  • 括号内是参数类型的标示,最右边是返回值类型的标示,
  • 当参数的类型是引用类型时,其格式是”L包名;”(标示最后有一个“;”),其中包名中的”.”换成”/”
  • 如果Java类型是数组,则标示中会有一个“[”

因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名,是没法找到具体函数的。为了解决这个问题,JNI技术中就使用了参数类型和返回值类型的组合,作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数了。

类型标示示意表
捕获.PNG

签名信息可以通过以下几种方式获取:

  • 对照java函数手写签名,不过容易写错
  • 通过AndroidStudio 新建C++工程 自动生成
  • 通过javap工具,javap –s -p xxx,其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,而默认只会打印public成员和函数的签名信息。
垃圾回收
  • Java中创建的对象最后是由垃圾回收器来回收和释放内存的(引用计数和可达性)
  • JNI层使用save_thiz = thiz 这样的语句,是不会增加引用计数的

从上面2条结论可得知,当jni层通过 赋值 “=” 保存Java层传入的jobject对象,在某个对象调用时,java层可能已经释放对象

但是JNI规范已很好地解决了这一问题,JNI技术一共提供了三种类型的引用,它们分别是:

  • Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
    如果不调用DeleteLocalRef,pathStr将在函数返回后被回收;如果调用DeleteLocalRef的话,pathStr会立即被回收。

  • Global Reference:(env->NewGlobalRef(client)) )全局引用,这种对象如不主动释放,就永远不会被垃圾回收,调用DeleteGlobalRef释放这个全局引用。

  • Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。

JNI常用函数

参考 https://blog.csdn.net/qinjuning/article/details/7595104

异常处理

当JNI函数调用的Java方法出现异常的时候,并不会影响JNI方法的执行,但是我们并不推荐JNI函数忽略Java方法出现的异常继续执行,这样可能会带来更多的问题。我们推荐的方法是,当JNI函数调用的Java方法出现异常的时候,JNI函数应该合理的停止执行代码。

  • ExceptionOccurred函数用来判断JNI函数调用的Java方法是否出现异常
  • ExceptionClear函数用来清除JNI函数调用的Java方法出现的异常
    Java代码:


    捕获.PNG

    JNI代码:


    捕获.PNG

    JNI通过ThrowNew函数抛出Java类型的异常
    捕获.PNG

参考文章

JNI完全指南
深入理解JNI
JNI基础语法

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

推荐阅读更多精彩内容

  • 一、jni是什么 java代码要使用native的代码,需要一个桥梁将他们连接起来,这个桥梁就是jni。 二、JN...
    会思考的鸭子阅读 456评论 0 7
  • 个人主页:https://chengang.plus/文章将会同步到个人微信公众号:Android部落格 1、创建...
    cg1991阅读 787评论 0 1
  • 1.jni简介 native c/c++代码是和平台相关联的,是可以直接运行的,在java出现以前很多软件由nat...
    太白新星阅读 444评论 0 0
  • 目录 JNI概述 MediaRecorder框架中的JNI Java Framework层的MediaRecord...
    慕涵盛华阅读 2,271评论 0 3
  • JNI与NDK的关系 NDK可以为我们生成了C/C++的动态链接库,JNI是java和C/C++沟通的接口,两者与...
    private_object阅读 1,249评论 0 2