文章测试案例提交到Github:learnNdk
前言
一般的我们在开发Android
应用程序时都是用Java
语言开发, 但是随着业务要求的提高,纯Java
平台可能会达到瓶颈,不同的功能模块如果考虑用其他语言实现可能会有更好的表现,甚至可以做到Java
语言达不到的效果.
本篇我们最开始部分来探讨Android
平台下如何使用C/C++
语言来开发Android
应用。本篇的一系列文章都是自己在开发,学习过程中一点一滴的积累,也希望和广大技术爱好者一起交流,共同促进技术的提升,如有不正确的地方,或者有欠缺的地方欢迎大家不吝赐教。
总体分两部分:
- Android NDK 基础编程
- Android 常用库的
NDK
实现
什么是JNI?
JNI
是Java
编程语言中的一个很强大有用的特性,它的存在使得Java
类中的某个方法能够用native code
实现,同时也能像普通的Java
方法一样在Java
的类中被调用。Native
中的函数也能像Java
中一样使用Java
中的对象,同时也能创建一个新的Java
对象,并且使用这些对象去调用
,修改
,查询
。
使用JNI开发的流程
- 在Java类中声明一个native方法
- 运行
javah
命令获取包含该方法的C声明的头文件.- 用
C/C++
实现native方法.- 将C/C++代码编译成共享库
- 在Java程序中加载该类库.
- java里面调用这个native方法
下面我们写一个案例:我们计算两个数的和,我们想使用C
语言来实现这个函数的功能。
当然这个功能我们可以使用Java
代码实现。为了学习JNI
我们首先应该从最简单的案例入手。
1.1声明本地方法
/**
* 将参数传从Java层传递给JNI层,在jni层处理完之后,在返回给Java层
* @param a,b
* @return 处理之后的结果
*/
public native int add(int a, int b);
1.2获取头文件
使用JDK
中的javah
命令,注意Javah
的目标是字节码文件,因此我们要提前将Java
文件编译成Class
文件,然后进入到字节码文件的工程目录使用命令生成头文件.
// 直接在Android studio 命令窗口下进行如下操作
cd \app\build\intermediates\classes\debug.
// Javah 类的完整类名
javah com.sivin.ndkdemo.NormalJni
如果没有什么错误就可以发现在debug
目录下生成了一个NormalJni.h
的头文件,下面我可以就可以使用该头文件了。
那么为什么要这个头文件呢?事实上也可以不要这个头文件,我们知道在C/C++
中,头文件就是声明函数用的,其实,如果想让Java
可以调用native
层代码,函数需要特定的命名才行(Java_包名_类名_方法名
)和一些特定的参数
而已,这些如果让开发这手写,未免有点繁琐,同时也容易写错,因此Jdk
直接提供了javah
的命令帮助我们生成了这个头文件,这样就不用我们自己写了。
如果你使用的是Android studio 2.2
以上版本,可以新建项目的时候勾选上support c++
,这样就可以在书写native
方法的时候,直接自动生成对应的对应的函数规范,不需要在做上面的工作了,这也是工具的方便。但是原理我觉得还是有必要搞清楚。
1.3编写Native代码
通过上一步我们得到了Java
层native
方法对应的C
层的方法,下面我们就来具体实现以下这个方法
JNIEXPORT jint JNICALL
Java_com_sivin_ndkdemo_NormalJni_add(JNIEnv *env, jobject instance, jint a ,jint b) {
return a +b;
}
下面我们具体的任务就是实现native
里面的代码,实现部分就是C/C++
里面的知识了,这里我并不想过多的讲解如何使用C/C++
来去处整数的功能。如果你对C/C++
不是太了解,建议你先去学习一下。
1.4 Java代码中加载共享库
static{
System.loadLibrary("共享库名称");
}
为什么使用静态代码块加载?
当类加载的时候就应该去加载共享库.
1.5 Java代码中使用这个native 方法
经过上面的几部,我们已经将C/C++
代码打包成动态库,并且已经加载进虚拟机,此时我们可以向使用普通的java方法一样使用这个native
函数。
2.知识点梳理
上面的基本过程,其实就是很基本的JNI
的基本知识,相信了解过JNI
的人都清楚。相信大家实践一下,就可以实现一个demo了。下面我们来讲解一下这个案例所设计的基本知识点。
几个很重要的参数JNIEnv *env
,jobject jobject
,jclass jcls
Java
代码如何与C/C++
代码进行通信,这正是我们的主角--->JNI
要解决的问题,我们发现每一个对应Java
层方法的C 函数
第一个参数都是JNIEnv *env
这个参数,那么这个参数到底是什么呢?打开jni.h
这个头文件,有人想问,这个头文件在哪?这个问题。。。好吧刚接触JNI
的小白确实不知道,首先Java
想要与C/C++
通信,肯定是Java
的工作,难不成还是C/C++
的工作吗?知道了这个,我们很自然的想到JDK
开发包,我们打开我们的JDK
安装目录,里面有一个include
目录,这个目录下就可以找到。
我们打开可以看到这样的代码:
struct JNINativeInterface_;
struct JNIEnv_;
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
文件首先声明了两个结构体类型:然后使用宏定义来重新定义JNIEnv
,我们可以看到C
和C++
里JNIEnv
是不同的。有什么不同呢?这里我们先不讨论,我们目前是以C
语法学习,后面我们会讲解和C++
不同。
我们看到JNIEnv
在C
里面定义的原型是一个JNINativeInterface_ *
是一个结构体指针,下面我们来简单的看看这个结构体到底是什么。
struct JNINativeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
void *reserved3;
jint (JNICALL *GetVersion)(JNIEnv *env);
jclass (JNICALL *DefineClass)
(JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
jsize len);
...
}
这个结构体定义的东西很多,对C
不熟悉的人,咋一看感觉,有点复杂啊,其实,仔细想想,C
的结构体里能翻什么大浪,无非就是定义一些变量嘛。前三个是指针变量,后面的呢?认真一瞅,唉。。。也是指针变量,是函数指针变量
。
这么一来我们就似乎懂了,通过这个结构体,我们可以得到里面的函数指针,通过这个函数指针我们可以调用相关的函数。那么到底是什么函数呢?我就不卖关子了,这些函数正是我们Java
和C
相互调用的桥梁。是开发java
虚拟机这帮大牛们帮我们实现的。
我们发现,这里面提供很很多方法接口,它给我们提供了很多从C
到Java
或者从java
到C
的转换,通俗的来讲这就是一个纽带
这样我们就搞定了第一个参数:
`JNIEnv *env`:通过这个对象我们可以获取到很多从`C`到`Java`或者从`java`到`C`的转换
第二个参数:jobject
或jclass
: 我们注意到这两个参数一般并不是同时出现的,那么这两个参数代表什么呢?我们仔细观察发现,当我们的java native
方法是对象方法时,我们生成的对应的native code
第二个参数就是jobject
如果我们的方法是static
时我们生成的native code
就是jclass
,想必大家其实已经猜到了,jobject
就是我们调用这个方法时所属的对象。jclass
其实就是这个静态方法所属的Class
类。
这两个参数会十分有用,我们在后面会更加具体的讨论这个两个参数的作用和意义。
本篇内容就到此为止了。接下来我们会具体的讨论,JNIEnv *
里具体有哪些转换函数,以及JNI
和native code
,java code
之间数据传递是如何转换的,学习这些的前提是先把本篇内容写一个demo
出来,弄懂这demo
所表达出来的知识内容。建议初学者使用android studio 2.2
以上的实现这个demo,至于如何使用Cmake
我们会有后续文章讲解。
网友评论