JNI是什么
java native interface (java本地接口)
JNI是一个协议,用来沟通Java代码和外部的本地代码(C/C++),通过这个协议,java代码就可以调用外部的c/c++代码;外部的c/c++也可以调用java代码完成两种语言之间的沟通和交流
为什么要使用JNI
- 市场需求
- 让java代码和底层代码之间互相调用
- java调用底层特殊硬件(调用c语言,车载电脑)
- 效率上c/c++语言效率更高(时间和内存要求严格的场景)
- 复用已经存在的c代码, c语言发展了几十年有很多优秀的代码库(ffmpeg,opencv,7zip)
- java反编译非常容易.c语言反编译不容易.关键业务逻辑需要用c实现.
- 历史遗留问题,复用原来pc端的c代码
- java调用底层特殊硬件(调用c语言,车载电脑)
如何使用JNI
- 1.熟悉java语言
- 2.熟悉c语言
- 3.JNI的规范、NDK
先来回顾一下C语言的知识点
数据类型
java的数据类型c语言的数据类型
- int 4个byte
- short 2个byte
- byte 1个byte
- long 8个byte
- double 8个byte
- float 4个byte
- char 2个byte
- boolean 1个byte
- char 的长度为1个byte
- int 的长度为4个byte
- float 的长度为4个byte
- double 的长度为8个byte
- long 的长度为4个byte
- short 的长度为2个byte
总结:
- c语言中的char可以用java的byte代替
- c语言中的int ,float , double ,short 和java完全一样,可以相互代替
- c语言的long ,用java的int代替
- java中的byte和boolean类型,c语言里面没有.
- java中的byte可以用c语言中的char表示
- java中的boolean类型,c语言用0 false和非0 true
- java中的long类型,c语言用 long long类型表示.
输入输出函数
输出:
- %d—-int
- %ld—long int
- %c—-char
- %f—-float
- %u—-无符号数
- %hd—短整形
- %lf—double
- %x—-十六进制输出int或long或short
- %o—-八进制输出
- %s—-字符串
从键盘输入
int len;
Scanf("%d",&len);
什么是指针
指针就是一个地址,地址就是一个指针
指针变量:用来存放一个地址的变量.用来存储某种数据在内存中的地址.
世面上书籍一般把指针和指针变量的概念混在一起了.
指针的重要性
- 可以直接访问硬件
- 快速传递数据(指针表示地址)
- 返回一个以上的值(返回一个数组或结构体的指针)
- 表示复杂的数据结构(结构体)
- 方便处理字符串
*号的三种含义
* 号的第一种含义表示的是乘法操作.
int i = 3;
int j = 5;
i * j相乘
* 号的第二种含义
如果一个星号是在一种数据类型的后面
代表的就是这种数据类型的指针变量,
用来存放这种数据类型在内存中的地址.
int i = 3;
int* p; 可以存放i的地址
p=&i;
* 号的第三种含义
如果星号在一个指针变量的前面
代表的就是把这个地址里面的数据取出来.
int* p;
*p; 把p地址里面的数据取出来
- 每种数据类型的地址,只能用当前数据类型的指针变量来表示
- C语言中编译是自上而下的,子函数需要写在main()函数的上面。
值传递和引用传递
准确的讲所有的语言,只有值传递.
如果传递的值是一个地址,通过这个地址可以找到地址对应的引用.
这个值传递可以理解为引用传递.
在java语言中对象实际上存放在某个内存地址里面,传递对象就相当于传递的是内存地址(引用传递)多级指针用来存指针变量的地址
数组
- 一块连续的内存空间
- 数组名其实就是数组的内存空间的首地址
- 每个元素占据多少个byte的内存空间,跟数组的数据类型有关
- 所有指针变量在内存中的长度都是一样的,不同的指针类型,为了方便指针做运算
- 指针的运算,只能运用到连续的内存空间(数组)
内存分配
动态内存:动态内存分配 需要申明头文件:malloc 全称:memory allocate申明头文件:#include<malloc.h>
int* p=malloc(sizeof(int)*10); //一个int4个byte,申请存放10个int类型的内存
free(p); //释放内存
静态内存
int i=3; //静态内存,栈空间申请一空区域 生命周期是函数体结束
int arr[20]; //静态的在栈空间申请一块区域,大小能存放20个int
动态内存和静态内存的比较:
静态内存是系统程序编译执行后系统自动分配,由系统自动释放,静态内存是栈分配的,动态内存是堆分配的。
结构体的使用
函数的指针
- 1、定义int (*pf)(int x,int y)
- 2、赋值pf=add;
- 3、引用pf(3,5)
#include<stdio.h>
#include<stdlib.h>
void (*pf)();//声明函数的指针
//结构体里面不可以定义方法,只能声明方法
struct student{
int age;
float score;
char sex;
void (*study)();
};
void study(){
printf("good good study,good good up!\n");
}
int main(void){
struct student st={20,77.98f,'F',study};
printf("学生的年龄:%d\n",st.age);
printf("结构体的长度:%d\n",sizeof(st));
st.study();
system("pause");
}
或者这样写
// st.study();
struct student* p=&st;
(*p).study();
或者
p->study();
联合体:联合体的长度大小永远与数据类型最大的一致
#include <stdio.h>
main()
{
union { short i; short k; char ii; long long l;} mix;
printf("mix:%d\n",sizeof(mix));
mix.l = 2132141241;
printf("mix.i=%hd\n",mix.i);
system("pause");
}
枚举的用法
#include <stdio.h>
enum WeekDay
{
Monday=1,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
}; //赋值为1,则从1到7
main(void)
{
enum WeekDay day = Friday;
printf("%d\n",day);
system("pause");
}
自定义数据类型
typedef int haha;
typedef long gaga;
main(){
haha i = 3;
gaga ga = 5;
printf("%d\n",i);
system("pause");
}
现在来介绍下jni技术:
常见概念
交叉编译
在一个平台上编译出来另外一个平台可以运行的二进制代码.
不同的操作系统(windows, mac os,linux),不同的处理器平台(x86,arm,MIPS)
交叉编译的原理
源代码—>编译—->链接—->可执行性程序模拟另外一种平台的cpu和操作系统的特性.
交叉编译的工具链
一大堆工具,放在一起,链式调用,工具链
常见工具
- ndk : native develop kits 本地开发工具集(交叉编译工具链)
- cdt : c/c++ develop tools c和c++开发工具 (eclipse的一个插件) 语法的高亮显示
- Cygwin : windows下一个linux模拟器(了解)
NDK的目录结构
- docs : 开发文档
- build: linux下编译的批处理命令
- platform : 某种平台下编译需要的头文件和函数库
- prebuild : 预编译的工具
- sample: 实例代码
- sources : 一些工具链的源码
- toolschains: 工具链
- ndk-build.cmd: ndk编译的命令脚本
jni开发步骤:
1、先在java代码中声明一个native本地函数public native int add(int x,int y);
//字符串类型的函数
public native String helloFromC();
2、在工程目录下建立一个文件夹名称必须叫jni 3、在jni文件夹下建立一个.c文件 4、现在可以在文件夹下书写C语言代码了
//先定义头文件
#include<stdio.h>
#include<stdlib.h>
#include<jni.h>
//必须严格按照要求书写函数的签名
//env是虚拟机的环境 obj调用者对象,如果有形参,就在后面加上形参;前面两个参数所有函数都要加上
jint Java_包名_类名_方法名(JNIEnv* env,jobject obj,jint x,jint y){
return x+y;
}
//env是虚拟机的环境 obj调用者对象,如果有形参,就在后面加上形参
jstring Java_包名_类名_方法名(JNIEnv* env,jobject obj){
//使用env环境指向的结构体,有自带的函数,利用jvm的函数进行操作
char* arr=("hello from c!");
return (*(*env)).NewStringUTF(env,arr);
//也可以使用下面的方法返回值
return (*env)->NewStringUTF(env,arr);
}
5、编译.c文件
首先在jni目录下配置c代码编译的脚本文件 Android.mk文件,里面的例子如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello //编译后的模块名
LOCAL_SRC_FILES := Hello.c //编译的源文件的名称
include $(BUILD_SHARED_LIBRARY)
6、调用ndk指令编译代码
首先要配置androoid-ndk-r9b的环境变量,到jni目录下调用ndk-build.cmd
7、 生成一个.so的文件 ( c代码编译出来的二进制可执行文件)
.so为动态函数库、.a为静态函数库,静态函数库体积大
存放在libs包下的armeabi文件下
8、在java代码里面写静态代码块,加载.so文件只写名字,前缀和后缀不写
static{
System.loadLibrary("hello");
}
9、 像使用一般java方法一样调用native的方法.
String result="3+5="+add(3+5);
String result1=helloFromC();
jni开发的常见错误
10-22 02:18:35.672: E/AndroidRuntime(1552): Caused by: java.lang.UnsatisfiedLinkError: Couldn’t load hello: findLibrary returned null
- 如果处理器平台不匹配,返回的lib就是空
在jni目录下新建Application.mk文件中编写
这个能在所有的平台运行APP_ABI := all
- 检查lib的名字是否拼写错误
10-22 02:24:50.418: E/AndroidRuntime(1696): Caused by: java.lang.UnsatisfiedLinkError: Native method not found: com.itheima.hello2.MainActivity.add:(II)I
- 检查c语言里面编写的方法名是否符合规范 Java_包名_类名_方法名(参数)
使用java中命令行的javah可以生成类的头文件,直接可以得到函数的签名。
到工程的bin目录下进入classes中有一个com包,同级下,按住Ctrl+shift右击鼠标打开命令行,执行下面命令
javah com.包名.类名 //自动生成一个头文件.h,复制到jni下,在里面可以找到函数签名,直接复制,加上参数和逻辑代码的编写就可以
- 如果上面的方法出现错误:无法访问android.app.MainActivity,那么就切换到工程的sre->com包下执行上述命令就可以生成所需的头文件了
现在有个集成的工具
不过不太稳定,配置ndk;可以自动生成jni环境的文件,在jni文件下的.c文件夹中,我们只要加上函数签名,书写c语言代码,在java中调用c语言掌握要点
- 把基本类型的数据传递给c语言.
- String 字符串 传递给c语言(工具方法 jstring2cstr)
- 传递java数组给c语言
*
//env虚拟机的环境
//jstr 要转换的java字符串
char* Jstring2cstr(JNIEnv* env,jstring jstr){
char* rtn=NULL;
jclass clsstring=(*env)->FindClass(env,"java/lang/String");
jstring strencode=(*env)->NewStringUTF(env,"GB2312");
jmethodID mid=(*env)->GetMethodID(env,clsstring,"getBytes","(Ljava/lang/String;)[B");
jbyteArray barr=(jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode);
jsize alen=(*env)->GetArrayLength(env,barr);
jbyte* ba=(*env)->GetByteArrayElements(env,barr,JNI_FALSE);
if(alen>0){
rtn=(char*)malloc(alen+1);
memcpy(rtn,ba,alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env,barr,ba,0);
return rtn;
}
在C语言中修改java中int型的数组如下:
JNIEXPORT void JNICALL Java_com_包名_类名_方法名(JNIEnv* env,jobject obj,jintArray jintarr){
//jsize (*GetArrayLength)(JNIEnv*,jarray);
//得到数组长度
int len=(*env)->GetArrayLength(env,jintarr);
//jint* (*GetIntArrayElements)(JNIEnv*,jintArray,boolean*);
//得到数组的每个元素
jint* cintarr=(*env)->GetIntArrayElements(env,jintarr,0);
int i;
for(i=0;i<len;i++){
*(cintarr+i)+=10;
}
}
修改其它类型的数组也可以:(*env)->Get….. 案例银行加密小案例、字符串加密解密案例、调用美图秀秀JNI的美化方法。
c代码回调java
- 复用已经存在的java代码
- c语言需要给java一些通知
- c代码不方便实现的逻辑
回顾一下反射
//1.把字节码装载进来
Class clazz = Demo.class.getClassLoader().loadClass("Dialog");
//2.查询对话框里面的方法
Method method = clazz.getDeclaredMethod("showDialog", String.class);
//3.调用方法
method.invoke(clazz.newInstance(), "哈哈嘎嘎嘎");
举个例子:c语言中调用java语言弹出一个对话框
先在java中实现对话框的方法/**
* 显示对话框
* @param msg 对话框的消息
*/
public void showDialog(final String msg){
runOnUiThread(new Runnable() {
@Override
public void run() {
if(dialog!=null){
dialog.dismiss();
dialog = null;
}
dialog = new ProgressDialog(MainActivity.this);
dialog.setTitle("提醒");
dialog.setMessage(msg);
dialog.show();
}
});
}
先在c语言中实现方法:三步骤
void showJavaDialog(JNIEnv* env,jobject obj,char* cstr){
//jclass (*FindClass)(JNIEnv*, const char*);
//1.查找字节码
jclass jclazz = (*env)->FindClass(env,"com/itheima/alipay/MainActivity");
//2.查找方法id
// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID methodid = (*env)->GetMethodID(env,jclazz,"showDialog","(Ljava/ lang/String;)V");
//3.调用方法
//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
(*env)->CallVoidMethod(env,obj,methodid,(*env)->NewStringUTF(env,cstr));
}
最后直接在C中调用方法即可
showJavaDialog(env,obj,"正在加密用户名");
sleep(2);
showJavaDialog(env,obj,"正在加密密码");
sleep(2);
showJavaDialog(env,obj,"检查安全支付的环境");
sleep(2);
showJavaDialog(env,obj,"正在连接淘宝支付服务器...");
sleep(2);
showJavaDialog(env,obj,"等待服务器反馈数据..");
sleep(2);
dismissJavaDialog(env,obj);
在命令行中执行
adb shell
ps //查看进程
kill 1334 //杀死进程号为1334的进程
实际开发的流程
- java程序员先去定义native的方法.
- jni程序员根据native的方法生成函数的签名
- 找个c工程师 给我添代码.
- 配置编译环境,编译调用
1、原来做pc端,window、phone。
2、移植一个Android版本。
3、c代码已经很久以前都已经写好的。
4、自已去包装一个c代码,通过jni调用(只关心接收的值、函数名、返回值)