一、理解JNI、NDK
1、JNI(Java Native Interface,JAVA原生接口)
使用JNI可以使Java代码和其他语言写的代码(如C/C++代码)进行交互。
JNI 是一个协议
这个协议用来沟通java代码和外部的本地代码(c/c++).
通过这个协议,java代码就可以调用外部的c/c++代码
外部的c/c++代码也可以调用java代码
问:为什么要进行交互?
|- 首先,Java语言提供的类库无法满足要求,且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。
|- 然后,Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。
|- 接着,使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。
物联网、智能家居、车载电脑;java反编译比c语言容易。
2、NDK全称:Native Development Kit 。
|- NDK是一系列工具的集合,它有很多作用。
|- 首先,NDK可以帮助开发者快速开发C(或C++)的动态库。
|- 其次,NDK集成了交叉编译器。使用NDK,我们可以将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
NDK工具必须在Linux下运行,它可以在linux环境下编译出可以在arm平台下运行的二进制库文件。
使用JNI技术,其实就是在Java程序中,调用C语言的函数库中提供的函数,来完成一些Java语言无法完成的任务。由于Java语言和C语言结构完全不相同,因此若想让它们二者交互,则需要制定一系列的规范。JNI就是这组规范,此时 Java只和JNI交互,而由JNI去和C语言交互。
JNI技术分为两部分:Java端和C语言端。且以Java端为主导。
|- 首先,Java程序员在Java端定义一些native方法,并将这些方法以C语言头文件的方式提供给C程序员。
|- 然后,C程序员使用C语言,来实现Java程序员提供的头文件中定义的函数。
|- 接着,C程序员将函数打包成一个库文件,并将库文件交给Java程序员。
|- 最后,Java程序员在Java程序中导入库文件,然后调用native方法。
在Java程序执行的时候,若 在某个类中调用了 native 方法,则虚拟机会通过JNI来 转调用库文件中的 C 语言代码。提示: C 代码最终是在 Linux 进程中执行的,而不是在虚拟机中。
二、相关C语言知识
库函数:
|- 为了代码重用,在C语言中提供了一些常用的、用于执行一些标准任务(如输入/出)的函数,这些函数事先被编译,并生成目标代码,然后将生成的目标代码打包成一个库文件,以供再次使用。库文件中的函数被称为库函数,库文件被称为函数库。
|- 在Windows中C语言库函数中的中间代码都是以.obj为后缀的,Linux中是以.o为后缀。
提示: 单个目标代码是无法直接执行的,目标代码在运行之前需要使用 连接程序将目标代码和 其他库函数连接在一起后生成可执行的文件。 Windows下.dll的文件 , linux下 .so .a的文件。
函数库:
|- 动态库:在编译用户程序时不会将用户程序内使用的库函数连接到用户程序的目标代码中,只有在运行时,且用户程序执行到相关函数时才会调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。
|- 静态库:在编译用户程序时会将其内使用的库函数连接到目标代码中,程序运行时不再需要静态库。使用静态库生成可执行文件比较大。
在Linux中:
|- 静态库命名一般为:lib+库名+.a 。
|- 如:libcxy.a 其中lib说明此文件是一个库文件,cxy是库的名称,.a说明是静态的。
|- 动态库命名一般为:lib+库名+.so 。.so说明是动态的。
头文件:xxx.h
|- 头文件中存放的是对某个库中所定义的函数、宏、类型、全局变量等进行声明,它类似于一份仓库清单。若用户程序中需要使用某个库中的函数,则只需要将该库所对应的头文件include到程序中即可。
|- 头文件中定义的是库中所有函数的函数原型。而函数的具体实现则是在库文件中。
|- 简单的说:头文件是给编译器用的,库文件是给连接器用的。
|- 在链接 器连接程序时,会依据 用户程序中导入的 头文件,将对应的 库函数导入到程序中。头文件以 .h为后缀名。交叉编译:
|- 将中间代码连接成当前计算机可执行的二进制程序时,连接程序会根据当前计算机的CPU、操作系统的类型来转换。
根据运行的设备的不同,可以将cpu分为:
|- arm结构 :主要在移动手持、嵌入式设备上。
|- x86结构 : 主要在台式机、笔记本上使用。如Intel和AMD的CPU 。
若想在使用了基于x86结构CPU的操作系统中编译出可以在基于arm结构CPU的操作系统上运行的代码,就必须使用交叉编译。
交叉编译:在 一个平台下编译出在 另一个平台中可以执行的 二进制代码。Google提供的NDK就可以完成 交叉编译的工作。C语言相关基础:
* byte
* short
* int
* long
* float
* double
* char
* boolean
#C基本数据类型
* short:2
* int:4
* long:4
* float:4
* double:8
* char:1
#符号位
* signed:分正负
* unsigned:不分正负
###int取值范围
* int数据:4个字节,32位* 能表示的数字是 2的32次方 个数字
* 取值范围 0 ~ 2的32次方-1
* 最高位取出来作为符号位,用于表示正负,不再表示数值,剩下31个数值位
* 少了一位,当前能表示的数字是 2的31次方 个
* 取值范围 0 ~ 2的31次方-1
* 符号位如果为0,那么31个数值位表示的数字是正数,符号位如果为1,31个数值位表示出来的是负数
* 结合符号位,int型的取值范围 -2的31次方 ~ 2的31次方-1
* 无符号就是最高位不表示符号位,依然作为数值位使用
###原反补
* 原码:二进制表示* 反码:除最高位,其他位依次取反
* 补码:反码+1
* 正数原反补是一样的
#内存地址
* 内存的门牌号* 内存中所有数据都是通过地址拿到的
* 没有地址的内存是无法访问
* 32位系统能表示的最大内存是4G
* 因为32位系统的内存总线长度是32位,也就是可以用于分配给内存作为地址的数字是 2的32次方 个
* 内存中每个字节都有一个地址
#内存修改器
* 原理:比方说修改血量,血量一定是保存在内存中的一个变量,找到这个变量的地址,修改地址上的值
#指针变量
* 保存的值是一个内存地址* 定义方式 int* p:指针指向的内存地址上存放的数据是一个int型
* *p:引用指针指向的内存地址上存放的数据
* int** q:定义二级指针,二级指针存放的是一个一级指针的地址
#常见错误
* 指针赋值之前,不要使用(给*p赋值)* 未赋值的指针称为野指针
* 指针的类型不要混用
#值传递和引用传递
* 值传递:传递一个普通的值* 引用传递:传递一个内存地址,其实所有传递都叫值传递,引用传递传递的值是一个地址
#数组
* 数组的内存空间是连续的* 数组变量保存的是首地址(第0个元素的地址)
#栈
* 内存空间是连续的* 自动分配
* 大小固定
* 系统自动释放
* 栈上分配的内存叫静态内存
#堆
* 程序员手动申请* java:new
* c:malloc
* 大小不固定,取决于系统虚拟内存
* 程序员手动释放
* java:自动
* c:free函数
* 空间不连续(随机)
* 堆上分配的内存叫动态内存
三、JNI的使用
# 交叉编译
* 在一个平台下编译出另一个平台可以执行的二进制程序* CPU平台:arm,x86,mips
* 系统平台:Windows、Linux、Mac OS
* 原理:模拟另一个平台的特性去编译代码
* 源代码->预编译->编译->链接->可执行程序
* 工具链:一个工具使用完毕自动使用下一个
# 常见工具
* NDK:native development kits* CDT:C/C++ development tools
* eclipse插件
* 高亮显示C关键字
* cygwin:Windows平台下的Linux命令行模拟器
#NDK目录结构
* docs:帮助文档* build/tools:Linux批处理文件
* platforms:存放开发jni用到的h头文件和so动态链接库
* prebuilt:预编译使用的工具
* sample:使用jni的案例
* source:NDK的部分源码
* toolchains:工具链
* ndk-build.cmd:编译打包C代码的指令
#JNI步骤(Java代码调用C代码中的方法(本地方法))
1. 定义并调用本地方法2. 创建jni文件夹
3. jni文件夹里创建c文件
4. c文件中实现本地方法,格式如下
//返回值与本地方法一致
//函数名:Java_包名_类名_本地方法名
//env:结构体二级指针,该结构体中封装了大量的函数指针,可以帮助程序员实现某些常用功能
//thiz:本地方法调用者的对象(MainActivity的对象)
jstring Java_com_itheima_helloworld1_MainActivity_helloFromC(JNIEnv* env, jobject thiz)
5. 创建Android.mk文件,指定要编译的c文件
6. 在jni目录下,执行ndk-build.cmd,编译打包出so动态链接库
7. 在java代码中加载动态链接库
8. 部署,运行
###常见错误
* 找不到类库* 没有添加对应平台的支持
* 类库名写错了
* 找不到本地方法
* 忘记加载类库
* c函数名写错了
###javah指令
* 自动生成jni样式的头文件,头文件中就包含了我们需要的函数名* 1.7:在src目录下使用:javah com.itheima.helloworld2.MainActivity
* 1.6:在bin/classes目录下使用
###添加本地支持(在eclipse中,项目属性中native support)
* 自动生成jni文件夹* 自动生成c文件和Android.mk文件
* 指定jni.h头文件的路径,相当于关联源码
* 不需要再去jni目录下使用ndk-build.cmd指令,项目部署时,会先打包编译so类库再去部署到手机上
###数组传递
* java的数组是对象,传递对象是传递对象的地址,c函数中修改了地址上的值,所以数组的值就改变了
###javap指令
* 打印指定类中所有方法的签名* 在bin/classes目录下使用:javap -s com.itheima.helloworld2.MainActivity
JNI步骤总结:
1.创建一个android工程
2.JAVA代码中写声明native 方法 public native String helloFromJNI();
3.用javah工具生成头文件
4. 创建jni目录,引入头文件,根据头文件实现c代码
5.编写Android.mk文件
6.Ndk编译生成动态库
7.Java代码load 动态库.调用native代码
###JNI中C代码通过反射调用Java代码中的方法(和Java的反射技术原理类似,但不同)
* C代码是执行在主线程的,会导致阻塞
#自定义竖型进度条
* 自定义控件,继承View
* 三个构造方法
* 布局文件中使用自定义控件要写包名
* onDraw方法用于在组件内绘制内容,图形或者文字都是通过这个方法绘制到界面上的
#C++实现JNI
* C++中的JNIEnv和C的JNIEnv不是同一个结构体* C++的 JNIEnv 是jni.h中定义的 _JNIEnv
* C的 JNIEnv 是jni.h中定义的 JNINativeInterface*
* _JNIEnv结构体中的函数其实就是调用了JNINativeInterface中的同名函数指针
* C++中函数要先声明才能使用
#分支C进程(C进程没有Java进程那么容易被销毁掉(即使Java进程被销毁,C进程如果没工作完将会继续工作),有时候需要这样的应用场合)
* fork函数分支一个C进程,返回子进程的pid* 子进程执行fork函数时不会再分支进程了,返回0
在c代码中使用logcat方法:
Android.mk文件增加LOCAL_LDLIBS += -llog
C代码中增加
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
LOGI("info\n");
LOGD("debug\n");