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

访问属性:

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

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

时间: 2024-12-28 11:54:31

NDK开发总结的相关文章

Android NDK开发环境搭建

目录[-] 一.下载NDK开发包,并解压. 二.下载Cygwin,安装所需库. 三.将NDK添加到Cygwin的build配置中. 四.安装CDT. ? 1 2 3 4 5 /* http://my.oschina.net/lifj/blog/176916 */ 有人说,网上的环境配置一大堆,你还写做什么?一来,是我再次复习的材料.二来,网上有些地方说的不是很详细,我也是参考了好多资料,弄了半天才弄好环境.写下来,帮助后面的人省掉不必要的麻烦.下面进入正题. 作为一个2年的android攻城狮,

NDK开发,如何配置 debug环境

刚开始做NDK 开发的时候,Android Studio 还没提供了 native C/C++ 设置断点 调试,我们都是通过输出 日志来调试,这样费时耗力.Android Studio 应该是在 2.2 版本才提供的设置断点 debug 功能,同时在该版本也提供了 cmake 编译.     我目前在做 NDK 开发的时候,还是习惯用 NDK-Build(也就是设置 Android.mk) 来开发,我先简单说一下怎么用输出日志来调试: 1.首先在 Android.mk 设置MODULE 添加日志

Ubuntu 12.04 32位 eclipse android SDK NDK开发环境搭建

文章写作时间: 2017/04/05 一.软件包如下: 1.jdk 1.8(JAVA 开发及运行坏境) 2.eclipse(IDE工具) 3.ADT23.0(eclipse 开发安卓插件) 4.SDK24 (Android 开发环境) 5.NDK R10 (NDK开发环境) 下载链接 http://pan.baidu.com/s/1nvjYfnr 二.安装步骤 1.JDK安装 a.在/home/"用户名" 下新建文件夹"Java_JDK"(mkdir /home/&

android studio NDK 开发初探

android studio NDK 开发初探 环境配置 1)下载ndk 2)在android studio中配置ndk 路径 3)配置gradle 在gradle.properties中加入 android.useDeprecatedNdk=true配置 开启ndk 在开发过程中我们有时还会用到c++中到stl库这时我们就需要在build.gradle 中加入 defaultConfig { ndk { moduleName "jnitest" stl "stlport_s

Android NDK开发指南---Application.mk文件和android.mk文件

https://android.googlesource.com/platform/development/+/donut-release/ndk/docs/OVERVIEW.TXT https://android.googlesource.com/platform/ndk/+/4e159d95ebf23b5f72bb707b0cb1518ef96b3d03/docs/ANDROID-MK.TXT https://android.googlesource.com/platform/ndk/+/4

如何使用Eclipse的NDK开发动态库,静态库

============问题描述============ 如何使用Eclipse的NDK开发动态库,静态库? Eclipse中已经安装了NDK,CDT和Esequoyah并配置(是在网上查的). 我是做C++的,公司业务需要自学Android,对Java也是知道一点皮毛. 在网上查了一些资料,我都没有成功做出最简单的动态库或静态库,郁闷啊! 有没有详细指导? ============解决方案1============ 你去搜一下Android的jni开发,就知道了,一点都不难,不过一般开发ndk

Android NDK开发篇(五):Java与原生代码通信(数据操作)

尽管说使用NDK能够提高Android程序的运行效率,可是调用起来还是略微有点麻烦.NDK能够直接使用Java的原生数据类型,而引用类型,由于Java的引用类型的实如今NDK被屏蔽了,所以在NDK使用Java的引用类型则要做对应的处理. 一.对引用数据类型的操作 尽管Java的引用类型的实如今NDK被屏蔽了,JNI还是提供了一组API,通过JNIEnv接口指针提供原生方法改动和使用Java的引用类型. 1.字符串操作 JNI把Java的字符串当作引用来处理,在NDK中使用Java的字符串,须要相

Android NDK开发之从环境搭建到Demo级十步流

写在正文之前: 几个月没有更新博客,感觉有点生疏了,所以说不能断,一断人就懒. 其实这几个月也并不是什么事也没有做,俺可是时刻想着今年的任务呢,10本书,30篇博文-,这几个月间断性的也是在学习中,学H5,学设计模式,以及NDK JNI开发等等. 学习JNI主要是因为公司有一些COCOS游戏需要添加计费点,而又没有真正的游戏开发人员,这个重任就落到我身上了,然后就是各种虐,一虐到底,苦不堪言,这种虐并不是学习技术的虐,而是一款游戏用于N种计费点,不停的改改改,那个需求这个需要的,然后你就等着被玩

Android NDK 开发(三)--常见错误锦集合Log的使用【转】

转载请注明出处:http://blog.csdn.net/allen315410/article/details/41826511  Android NDK开发经常因某些因素会出现一些意想不到的错误,很多时候调试这些错误的时候,显得比调试Java代码要复杂,一方面是导致错误的原因很多很杂,另一方面NDK开发涉及到C/C++代码的编写,很多程序员对此不熟悉.那么这篇博客就总结一下,在NDK开发中经常出现的一些问题,并且尝试提供一些正确的解决方案,方便在开发时能够快速定位到错误,更改错误,当然了,错

5.7 NDK开发

JNI开发流程主要分为以下6步: 编写Java源代码 将Java源代码编译成class字节码文件 用javah -jni命令生成.h头文件(-jni参数表示将class中用native声明的函数生成jni规则的函数) 用本地代码实现.h头文件中的函数 将本地代码编译成动态库 (windows: *.dll ,linux/unix: *.so ,mac os x: *.jnilib ) 拷贝动态库至  java.library.path 本地库搜索目录下,并运行Java程序 JNI是Java众多开