首页 > 代码库 > NDK开发总结

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类型对比

JavaTypeJNITypeC/C++TypeSize
Booleanjbooleanunsigned charunsigned 8 bits
Bytejbytecharsingned 8 bits
Charjcharunsigned shortunsigned 16 bits
Shortjshortshortsigned 16 bits
intjintint signed 32 bits
Longjlonglong longsigned 64 bits
Floatjfloatfloat32 bits
Doublejdoubledouble64 bits

 

            2、java引用类型

 

java typeNative Type
java.lang.Classjclass
java.lang.Throwablejthrowable
java.lang.Stringjstring
other objectjobject
java.lang.Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
char[]jcharArray
short[]jshortArray
int[]jintArray
float[]jfloatArray
double[]jdoubleArray
other arraysjarray
  

 

       原始类型在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 ModeAction
0Copy 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_ABORTfree 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字符指针。

 

 

        访问属性:

                     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层的类的属性来获取参数。

 

         调用方法:

                       跟获取属性一样, 也要先获取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去执行对应的非静态和静态方法。

 

 

          捕获异常:

                       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引用:

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

 

NDK开发总结