JNI常用知识点总结

JNI是什么

java native interface (java本地接口)
JNI是一个协议,用来沟通Java代码和外部的本地代码(C/C++),通过这个协议,java代码就可以调用外部的c/c++代码;

外部的c/c++也可以调用java代码完成两种语言之间的沟通和交流

为什么要使用JNI

  1. 市场需求
  2. 让java代码和底层代码之间互相调用
  • java调用底层特殊硬件(调用c语言,车载电脑)
  • 效率上c/c++语言效率更高(时间和内存要求严格的场景)
  • 复用已经存在的c代码, c语言发展了几十年有很多优秀的代码库(ffmpeg,opencv,7zip)
  • java反编译非常容易.c语言反编译不容易.关键业务逻辑需要用c实现.
  • 历史遗留问题,复用原来pc端的c代码

如何使用JNI

  • 1.熟悉java语言
  • 2.熟悉c语言
  • 3.JNI的规范、NDK

先来回顾一下C语言的知识点

数据类型

java的数据类型

  • int 4个byte
  • short 2个byte
  • byte 1个byte
  • long 8个byte
  • double 8个byte
  • float 4个byte
  • char 2个byte
  • boolean 1个byte

c语言的数据类型

  • 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
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
#include
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  
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 

    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
#include
#include
//必须严格按照要求书写函数的签名
//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语言

把java中的String转换成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

修改其它类型的数组也可以:(*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的进程

实际开发的流程

  1. java程序员先去定义native的方法.
  2. jni程序员根据native的方法生成函数的签名
  3. 找个c工程师 给我添代码.
  4. 配置编译环境,编译调用

1、原来做pc端,window、phone。

2、移植一个Android版本。

3、c代码已经很久以前都已经写好的。

4、自已去包装一个c代码,通过jni调用(只关心接收的值、函数名、返回值)

你可能感兴趣的:(JNI常用知识点总结)