码迷,mamicode.com
首页 > 其他好文 > 详细

NDK开发总结

时间:2016-09-13 11:44:10      阅读:148      评论:0      收藏:0      [点我收藏+]

标签:

  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字符指针。

 

 

        访问属性:

                     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开发总结

标签:

原文地址:http://www.cnblogs.com/zhangyan-2015/p/5865536.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!