NDK开发总结

前言

NDK开发差不多结束了, 估计后面也不会再碰了诶, 想着还是写个总结什么的,以后捡起来也方便哈。既然是总结,我这里就不会谈具体的细节,只会记录下我觉得重要的东西, 所以这篇随笔不是为萌新学习新知识准备的, 而是复习用的, 有些知识默认读者知道,就算忘了也能根据提示想起来。这里虽然是总结有些地方还是很细的2333.

方法论

1、 我在实践中大概是这样的流程, 想好大概的 java 和 jni 代码交互流程, 然后编写 jni 接口代码, 然后在接口代码里面调用 c++ 或者 c 写的方法, 如果不跨线程的话, 我会传 JNIEnv 指针给本地代码层。这样相当于分了三层, java 层, 中间层, 本地层, 这里的中间层指的按照 jni 规范命名的方法, 本地层不考虑 java 层逻辑, 而是设计的实现中间层逻辑的各种类的集合。

2、有些项目可能会使用三方的 c/c++ sdk, 这些 sdk 可能并没有按 java 和 jni 交互的规范设计, 所以 java 层无法直接调用 sdk 里面的方法, 但是计算机里面有个重要的方法, 什么问题都能够通过加个中间层解决, 也可以认为是设计模式里面的适配器思想的范版,具体方法是 我们可以在自己的 c/c++ 代码里面封装第三方的 sdk , 然后 java 层调用我们的 c/c++ 代码来间接的使用三方的 sdk 的效果。

知识点

一、Java 和 c/c++接口

本地方法通过 native 关键字来定义, 暗示编译器这个方法的通过其他语言实现, 这个方法通过分号终止, 因为本地方法没有方法体。

虽然我们定义了本地方法, 但是窝们还没有告诉 java 虚拟机怎么找到这个方法的实现, 这是后我们就要通过下面 这种方式告诉虚拟机去加载哪个动态库了。

static{

        System.loadLibrary("hello-jni");

 }

// loadLibrary方法在静态代码块里面调用, 因为我们想本地方法在类被加载,第一次被初始化的时候动态库能够加载进来了。

java 技术的一个设计目标是平台无关性, java 框架的 api 作为一部分, loadLibrary 的设计也一样, 这里动态库的名字是 libhello-jni.so, 但是在这个方法里面只需要写库的名字就行了, 也就是模块的名字(), hello-jni, 系统在用的时候会添加前缀和后缀。 loadlibrary 搜索的路径在 System property 里面的 key java.library.path 里面定义了, loadLibrary 方法会搜索这个列表寻找动态库 .java library 的路径在 android 里面是 /vendor/lib 和 /system/lib;

要想虚拟机正确的找到本地方法,本地方法需要按照严格的规则命名函数, 这样虚拟机才能找到

栗子:

// java:

 package com.demo;

 class Sample{

    static{

                 System.loadlibrary("hello-jni");

            }

           public native String stringFromJNI();

}

 //ndk:

  jstring Java_com_demo_Sample(JNIEnv *env, jobject thiz){};

名为 stringFromJNI 的本地方法, 在 c/c++ 层有一个精确的c层方法对, Java_com_demo_Sample, 试想如果 java 层方法和 c 层方法的名称没有精确的规则对应,虚拟机根据 java 层本地方法拿什么去匹配 c/c++ 层代码, 或者设计者可以设计用注解注明 c 层代码名, 但是设计者没有这么做。

二、 数据类型

我们都知道 java 有两种数据类型

* 原始类型: boolean, byte, char, short, int, long, float, double

* 引用数据类型: String, 或者其他的类

1、原始类型

java 原始类型和 c 类型对比

JavaType JNIType C/C++Type Size
Boolean jboolean unsigned char unsigned 8 bits
Byte jbyte char singned 8 bits
Char jchar unsigned short unsigned 16 bits
Short jshort short signed 16 bits
int jint int  signed 32 bits
Long jlong long long signed 64 bits
Float jfloat float 32 bits
Double jdouble double 64 bits

 

2、java 引用类型

 

java type Native Type
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.String jstring
other object jobject
java.lang.Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
float[] jfloatArray
double[] jdoubleArray
other arrays jarray
   

 

原始类型在 c/c++ 里面是直接可以使用的, 因为他们对应着 c/c++ 里面的类型, 但是引用类型 c/c++ 不可以直接操作, 如果想操作的话必须使用 JNI 提供的接口去操作这些引用类型。

三、引用类型操作

1. 字符串操作

 创建String

jstring javastring = env->NewStringUTF("Hello world!");

如果内存不够用了, 这个方法将会返回 NULL, 同事虚拟机会抛出一个异常, 所以我们的方法应该返回而不应该继续处理;

2. java 字符串转 C 字符串

const jbyte* str;

  jboolean iscopy;

  str = env->GetStringUTFChars(javastring, &iscopy);

  if (NULL != str){

      printf("java string:%s", str);

  if ( JNI_TRUE == iscopy){

      printf("this c string is copy from java string.");

  }else{

     printf("c string is one width java string.");

        }

 }

注意 GetStringChars 和 GetStringUTFChars 方法需要调用 ReleaseStringChars 和 ReleaseStringUTFChars 释放内存,有一个设计规则,谁申请的内存,那么谁就赋值释放, 这里调用 env 获得字符串的过程中,env 申请了内存,所以我们要调用 env 的方法去释放它。

3. 数组操作(注意数组是引用类型)

新建一个数组可以调用本地方法,类似于 New<Type>Array 方法的形式构建, <Type> 可以使 int, char, boolean 等等,比如 NewIntArray;

 jintArray javaArray;

  javaArray = env->NewIntArray(10);

  if (NULL != javaArray){

   ...
}

和 NewString 方法类似, 如果内存不够用了, 那么 New<Type>Array 方法将会返回 NULL, 虚拟机将会抛出异常, 所以本地方法应该要立刻返回,而不应该继续执行了。

--操作数组元素

调用 Get<Type>ArrayRegion 方法可以复制一个 java 的原始类型数组成为对应的 C 数组. 可能有人会想,原始类型数组肿么操作要这么麻烦, 还要转成 jni 对应数组才行啊, 如果这么想的话,那么可能你忘了 java 数组是引用类型的事情, 引用类型我们是不能再 c 里面操作的, 但是窝们可以操作原始类型, 所以将 java 原始类型数组转化成 jni 类型数组, 我们就可以做对应操作了。

jint nativeArray[10];

env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);

当然, get 到了数据做完修改我们也会需要 set 回去咯, 这时候调用 Set<Type>ArrayRegion 方法就可以了,嘛, 这里设计的还是很对称的啦。

注意一点, 当数组很大的时候, 复制数组会造成性能问题, 所以我们应该get我们需要修改的范围,然后设置回去,  当然Jni提供了一系列不同的方法,可以直接通过指针的方式操作数组, 而不用复制他们。

---直接通过指针操作数组

 Get<Type>ArrayElements 方法 允许本地代码直接通过指针操作数组元素, isCopy允许调用者声明是否返回一个c数组指针指向复制或者在堆空间上的固定数组。

jint *nativeDirectArray;

jboolean iscopy;

nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);

同样的,我们需要调用 Release<Type>ArrayElements 方法去释放内存, 否则会造成内存泄漏。

比如不用的时候应该调用 env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);

第三个参数可以是下面的值:

Release Mode Action
0 Copy back the content and free the native array
JNI_COMMIT

Copy back the content but do not freee the array.

This can be used for periodically updating a Java array

JNI_ABORT free the native array without copyting its content.

 

---直接新建一个字节缓冲区

本地代码可以直接创建一个字节缓冲区, 这个缓冲区可以给java应用直接使用, 缓冲区的内容直接使用 c/c++ 层字节数组。

unsigned char * buffer = (unsigned char *) (unsigned char *) malloc(1024);

      ....

jobject directBuffer;

directBuffer = env->NewDirectByteBuffer(buffer, 1024);

注意:

当然这里的内存不是由 java 虚拟机申请的了, 所以本地代码需要自己管理这些分配的内存。比如我们可以写个 recycle 的本地方法,在 java 层调用这个方法释放内存。

同理我们也可以获得 java 应用创建的字节缓冲区。调用 GetDirectBufferAddress 方法会返回一个c字符指针。

4. 访问属性

java 有两种类型的属性, 实例的属性和静态属性, 每种属性都有对应的方法获取。

其实步骤都是获取对应的属性的id, 然后获取属性值。

JNI 提供了方法去获得者两种属性例:

public class JavaClass{

       private String instanceField = "instance Field";

       private static String staticField = "static Field";

}

1) 获取非静态属性 id

jfieldID instanceFieldId;

instanceFieldId = env->GetFieldID(clazz, "instanceField", "Ljava/lang/String");         

2) 获取静态属性id

jfieldID staticFieldId;

staticFieldId = env->GetStaticFieldId(clazz, "staticField", "Ljava/lang/String;");

最后一个参数是属性的描述, 这个是java虚拟机规范里面的, 可以看下我前面的博客查查肿么写。

获取属性通过 Get<Type>Field, 或者 GetStatic<Type>Field 方法得到, type 是属性的类型。 如果内存满了, 者两个会返回 NULL。

小提示:

获取一个属性需要调用 2 个或者 3 个 JNI 方法的调用, 建议尽量在本地方法里面获取参数,然后返回到 java 层, 尽量少的直接用 java 层的类的属性来获取参数。

5. 调用方法

跟获取属性一样, 也要先获取 id, 然后才能执行方法, 我们有两种获取方法 id 的方式, 一种是对 class 的,也就是静态方法的 id,一种是实例的,也就是非静态方法的 id.

public class JavaClass{

 private String instanceMethod(){

      return "Instance Method";

 }

 private static String staticMethod(){

     rerturn "static Method";

 }

}        

 

jmethodID instanceMethodId;

instanceMethodId = env->GetMethod(clazz, "instanceMethod", "()Ljava/lang/String;");

jmethodID staticMethodId;

staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()Ljava/lang/String;");

和方法 id 一样, 最后一个参数是方法的描述, 也就是方法签名, 同样的是 java 虚拟机规范。

接下来就是根据方法 id 调用方法了,同样使用 Call<Type>Method,或者 CallStatic<Type>Method 去执行对应的非静态和静态方法。

6. 捕获异常

java 里面是有异常机制的,如果我本地执行 java 代码, java 代码里面抛出了异常, 本地方法这么处理呢? java JNIEnv 接口提供了一系列方法来处理异常, 现在来总结下:

 public class JavaClass{

     private void throwingMethod() throws NullPointerException{

           throw new NullPointerException("Null pointer");

      }

 

      private nativve void accessMethods();

}

如果我们在 accessMethods 的本地方法里面调用了 throwingMethod 方法, 那么我们本地代码里面就要精确的处理 throwingMethod 方法可能产生的异常。

首先我们肿么会想到, 本地代码里面肿么抛出异常呢, 比如我们定义了一个可以抛出异常的本地方法, 辣么我们实现本地方法的时候肿么抛出异常呢?

jthrowable ex;

                   ..

env->CallVoidMethod(instance, throwingMethodId);

ex = env->ExceptionOccurred();

 if (NULL != ex){

       env->ExceptionClear();

 }

JNI 提供了 ExceptionOccurred 方法去查询虚拟机是否有异常抛出, 本地异常处理需要精确使用  ExceptionClear 方法来清除异常

问题来啦, 我们肿么在本地代码里面抛出异常呢?

jclass clazz;

                  ...

clazz = env->FindClass("java/lang/NullPointerException"); //这里的参数是java类的内部名, 不要和签名弄混哦
if(NULL != clazz){

   env->ThrowNew(clazz, "Exception message.");

}

由于本地代码不归虚拟机控制, 所以啊, 抛出异常后, 我们的方法不应该继续有其他操作了,而是应该返回同时释放本地引用和资源。

后面的只是提一下了:

java 里面的关键字 Synchronized,肿么 在本地代码实现呢?

例:

if(JNI_OK == env->MonitorEnter(obj)){

//错误处理

}
//同步代码

if (JNI_OK == env->MonitorExit()){

//错误处理

}

、本地线程

本地代码产生的线程 java 虚拟机是不知道的, 所以 JNIEnv 是不能跨线程使用的, 如果要使用的话我们需要将本地线程贴到 java 虚拟机上,去重新获得 JNIEnv 指针。不过 java 虚拟机是是可以跨线程的, 所以 JavaVM 指针是可以全局共享的。

JavaVM* cachedJvm;

..

JNIEnv* env;

//Attach

 cachedJvm->AttachCurrentThread(cachedJvm, &env, NULL);

//现在线程可以通过JNIEnv和Java应用交互了

//Detach

cachedJvm->DetachCurrentThread();

话说 JavaVm 肿么获得呢? 

其实只有在本地代码中注册一个回调就可以了, 本地代码在加载的时候会自动执行这个方法。

JavaVM *cachedJvm;

jint JNI_OnLoad(JavaVM *vm, void *reserved){

  g_jvm = vm;

  if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4)){

         return JNI_ERR;

  }

      return JNI_VERSION_1_4;
}

五、JNI引用

               引用知识前面的博客总结过了,这里就不写了

 

posted @ 2016-09-13 11:09  章炎  阅读(1647)  评论(0编辑  收藏  举报