前言
当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。注册分为 静态注册 和 动态注册 两种。默认的实现方式即静态注册。
一、Eclipse安装与配置
1、Eclipse安装
双击运行exe文件
选择项目保存的目录
首先Eclipse创建一个工程
项目名称
一直默认即可
选择图标
![im
创建成功
2、Eelipse配置
配置java编译环境:
配置当前文件的编码
添加LogCat,可以看到安装的一些输出的log信息
配置java代码提示:(配置之后输入java关键字前几个字母后,会提示java关键字后面部分)
abcdefghijklmnopqrstuvwxyz
二、Native 方法的静态注册和动态注册
1、前言
当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。
注册分为 静态注册 和 动态注册两种。默认的实现方式即静态注册。
2、Native 方法的静态注册
NDK 开发中通过 javah -jni 命令生成的包含 JNI 的头文件,接口的命名方式一般是 Java_
,程序执行时系统会根据这种命名规则来调用对应的 Native 方法,这种注册方式称之为静态注册。
静态注册方式的优点:方便简单,IDE (高版本的 AndroidStudio)就可以自动帮你完成;
静态注册方式的缺点:JNI (Native 方法)名字过长,可读性差,由于命名规则的限制,不能灵活改变。
3、Native 方法的动态注册
由于静态注册存在命名局限性,生产环境中一般不采用静态注册的方式。动态注册的优点是可以自由命名 Native 方法,缺点是如果 Native 方法过多,操作比较麻烦。
动态注册的时机是在加载函数库(.a 或 .so)的时候进行注册,即在JNI_OnLoad
方法里进行注册。
三、静态注册-案例一
目标:通过C层返回java层一个字符串并在真机的app中显示
1、java层代码
来到MainActivity,java文件,
删除onCreateOptionsMenu方法,因为用不上
在定义一个方法
public native string Getsting();
用native关键字修饰方法,Native方法一般用于两种情况(这里是与C语言联动):
1)在方法中调用一些不是由java语言写的代码。
2)在方法中用java语言直接操纵计算机硬件。
这里string类型报错,先等等,我们之后在修改
这里已经定义了一个被native修饰的方法,那么修饰这个方法就要做一些操作,在java层用Toast展示出来:
Toast 是一个 View 视图,快速的为用户显示少量的信息。
Toast 在应用程序上浮动显示信息给用户,它永远不会获得焦点,不影响用户的输入等操作,主要用于 一些帮助 / 提示。
Toast 最常见的创建方式是使用静态方法 Toast.makeText。
Toast.makeText()方法的参数接收解释
context:上下文
text:弹出的内容字符
duration:显示的时长
修改Toast.makeText()方法的参数
context改为this上下文,
text改为我们的方法Getsing(),
duration改为Toast.LENGTH_SHORT 或者修改为1显示时长
添加.show():输入结果
我们发现在Getsing()部分和string部分都报错了
去除报错
点击处置修改错误后,定义的Getsting()静态方法前面的数据类型由string修改为CharSequence
CharSequence类是java.lang包下的一个接口,此接口对多种不同的对char访问的统一接口,像String、StringBuffer、StringBuilder类都是CharSequence的子接口;
CharSequence类和String类都可以定义字符串,但是String定义的字符串只能读,CharSequence定义的字符串是可读可写的;
对于抽象类或者接口来说不可以直接使用new的方式创建对象,但是可以直接给它赋值;
这时候java层的简单操作就写好了。
2、编译.h文件
我们需要获取src目录位置
复制该目录,cmd进入
E:\data\yunjiananquan\src
使用javah执行命令
javah 生成实现本地方法所需的 C 头文件和源文件。C 程序用生成的头文件和源文件在本地源代码中引用某一对象的实例变量。.h 文件含有一个 struct 定义,该定义的布局与相应类的布局平行。该 struct 中的域对应于类中的实例变量。
-jni 生成JNI样式的标头文件(默认值)
-jni需要一个参数:完整路径
包名加类名,就是完整路径
com.example.yunjiananquan.MainActivity #完整路径
生成.h文件
javah -jni com.example.yunjiananquan.MainActivity
注意:编译出现中文提示编码GBK的不可映射字符,加上-enconding UTF-8即可
javah -encoding UTF-8 -jni com.example.yunjiananquan.MainActivity
但是在Eclipse无显示,这里需要刷新一下安卓项目,或者按F5
这时候我们看到在src(source)同目录下生成了一个.h的文件
3、分析.h文件
开始分析com_example_yunjiananquan_MainActivity.h文件
首先第一行代码
#include <jni.h>
#include是preprocessor的一条指令, 负责把 #include 的文件中的内容全部copy到当前文件的#include所在的位置,也就是用该文件中的内容取代#include 这条指令。这里的意思是引入jni.h文件
接下来,在c语言中,头文件中的加入 #ifndef #define #endif目的防止该头文件被重复引用
“被重复引用”是指一个头文件在同一个c文件中被include了多次,这种错误是由于include嵌套造成的。比如在a.h文件#include "c.h" 而此时b.cpp文件包含#include "a.h" 和#include "c.h"此时就会造成c.h重复引用
可以理解防止重名,防止重复导入,就是提前做一个预判,如果有异常会进行一些处理
extern关键字:在C语言中全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
下面就是重点JNI接口方法
详细可以参考我之前写过的文章
https://forum.butian.net/share/483
这是更加java层的Getsting()方法生成的JNI接口的方法
JNIEXPORT jobject JNICALL Java_com_example_yunjiananquan_MainActivity_Getsting
(JNIEnv *, jobject);
jobject:返回值
Java_com_example_yunjiananquan_MainActivity_Getsting:方法名
(JNIEnv *, jobject):参数
JNI文件生成分析好了,我们接下来要开始通过NDK编译SO文件了
4、NDK编译SO文件准备
**NDK编译方式:**需要放在$PROJECT/jni/目录下, 其中$PROJECT表示你的工程目录,这样就可以被ndk-build脚本文件找到.(注:在这种方式下,进入jni目录,即$PROJECT/jni/,然后执行ndk-build,就可以直接编译jni生成.so文件了)。
在NDK编译中,最少需要三个文件,这里需要四个,需要把.h文件放在jni目录下:
test.c
Application.mk
Android.mk
首先创建一个jni目录
创建好jni文件后,把.h文件修改一下名称,修改为yunjian.h(名字随意,后缀名不变即可)
将Yunjian.h拖到jni文件夹中
5、编写.C文件
开始编写c文件,创建Yunjian.c
开头引入Yunjian.h
#include <Yunjian.h>
然后开始写函数名,函数是从Yunjian.h中Java_com_example_yunjiananquan_MainActivity_Getsting函数,复制并放在C文件中
将后面的;删除改为{},这里代表方法体
JNIEXPORT jobject JNICALL Java_com_example_yunjiananquan_MainActivity_Getsting
(JNIEnv *, jobject);
这里我们说一下函数的三要素是什么呢?
返回值、参数、方法体
那些现在参数都有了吗?目前只有两个类型,但是只有参数类型,需要给参数取名称
第一个参数类型是:JNIEnv 我们随便取名,就叫yunj(*要保留,放在参数前面)
第二个参数类型是:jobject 我们叫obj(参数名可随意修改,传统写法是env obj)
JNIEXPORT jobject JNICALL Java_com_example_yunjiananquan_MainActivity_Getsting
(JNIEnv *yunj,jobject obj){}
参数补全之后还差什么呢,因为方法名是get开头,所以最终我们要给jobject返回一个回显内容,目的是从c层返回一个字符串给java层
如果在return后面加上yunjiananquan,这时候就可以让这个字符串返回到java层了吗?
显然是不能的,这里需要我们借助一个JNI接口:NewStringUTF方法
jstring (*NewStringUTF)(JNIEnv*, const char*);
直接放进来,需要使用的话还需要定义参数,不然yunjiananquan无法从c层返回到java层!
首先yunjiananquan是个字符串,那么可以使用str接收,使用JNIEnv指针指出:
将NewStringUTF方法外面的括号和*删除,并补充参数
第一个参数为JNIEnv*,与上面的参数一样,所以这里修改为:yunj
第二个参数为const char*,修改为传入值“yunjiananquan”,不然她无法返回java层。
const char*修改为传入值yunjiananquan,不然她无法返回java层:“yunjiananquan”,并把return返回的内容修改为str变量
回顾一下我们前面的操作
通过java生成一个.h文件,在.c文件首先引入.h文件,然后复制过来.h文件中的方法,定义两个参数名,将最后的分号改为{}方法体加载jnu接口中NewStringUTF方法,然后用到了NewStringUTF要求把yunjiananquan传入java层,为了实现传出,NewStringUTF是在头库中找到的,然后改了两个参数类型,改了两个参数类型后,然后提出str参数去接收yunjiananquan,那么最后直接return把yunjiananquan传过去。
6、Android.mk编写
这时候还不能传过去,因为还缺少两个mk文件
Android.mk
LOCAL_PATH := $(call my-dir) #获取jni文件路径
include $(CLEAR_VARS)
LOCAL_MODULE := Yunjian #模块名称
LOCAL_SRC_FILES := Yunjian.c #源文件 .c或者.cpp
LOCAL_ARM_MODE := arm #编译后的指令集ARM指令
LOCAL_LDLIBS += -llog #依赖库
include $(BUILD_SHARED_LIBRARY) #指定编译文件的类型.so
7、Android.mk编写
Application.mk
APP_ABI := armeabi-v7a
注意:如果发现注释#后面的中文乱码无法显示的问题可以配置
8、NDK编译so文件并在真机运行
1、生成so文件
下面来到jni目录下,通过ndk-build命令生成so文件
刷新F5,就可以在libs看到so文件了
2、修改java层代码
这时候如果想让java代码联动C文件,需要装载库文件,System.loadLibrary("Yunjian");就是载入一个JNI库文件
static{ } : 静态代码块,类加载的时候,先执行里面的命令
static{
System.loadLibrary("Yunjian");
}
3、Eclipse连接真机测试
连接真机(root过后的),执行
我们看到成功在真机上弹出窗口
但是显示时间只有2秒钟,怎么加显示时间呢,在MainAcivity.java中修改,在重新生成.h文件,在ndk-build生成.so文件
四、静态注册-案例二
目标:在java层设置普通变量和静态变量,C层通过不同的方法分别调用java层的变量,并返回给java,通过app展示
1、java层代码
继续深入学习,来定义两个变量
在MainAcivity类中定义了两个变量,一个是普通的、一个是静态的
public String yunjian1 = "我是yunjian1";
public static String yunjian2 = "我是静态yunjian2";
在定义native修饰的两个方法
//获取普通的变量yunjian1
public native CharSequence GetStingyunjian1();
//获取普通的静态变量yunjian2
public native CharSequence Getstingyunjian2();
继续用Toast来调用方法:
//调用GetStingyunjian1()方法
Toast.makeText(this, GetStingyunjian1(), 1).show();
//调用GetStingyunjian2()方法
Toast.makeText(this, Getstingyunjian2(), 1).show();
使用javah -jni生成.h
javah -jni com.example.yunjiananquan.MainActivity
F5刷新,看到了.h文件,在文件中看到了我们刚刚定义的两个方法名称
接下来将.h文件改名改Yunjian.h,并将原jni目录下的Yunjian.h删除,将刚刚生成的文件复制进去
2、修改.c文件
接下来继续将.h文件中的两个方法复制到在.c文件
修改方法的参数名,方法体等
1、配置调用普通方法的JNI接口
开始GetStingyunjian1方法体的配置
有了参数,开始写方法体内容,获取普通变量的方法需要用到jni接口中的:GetObjetField()方法
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
定义指出,返回给str变量
然后修改GetObjectField里面的参数
第一个参数JNIEnv*,默认的,填写yunj
第二个参数jobject,默认的,填写obj
第三个参数jfieldID,需要调用GetFidldID()方法获取返回值
GetFidldID()方法
GetFidldID()方法
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
修改GetFidlID()方法指出等信息
补充GetFidlID的参数
JNIEnv* 默认参数,为:yunj
jclass:需要调用FindClass,获取FindClass返回值
const char*在上面章节中将const char*修改为字符串“yunjiananquan",那么这里修改为”yunjian1"变量名
const char*:”yunjian1"变量的类型,unjian1变量定义的为string类型,那么这里返回值填写string的全名
第二个参数jclass,FindClass方法
将FindClass()方法,继续放到.c文件中
jclass (*FindClass)(JNIEnv*, const char*);
修改参数,定义变量
JNIEnv*,默认参数:yunj
const char*,这里填写java类的路径:com.example.yunjiananquan.MainActivity
这里java的类名放到C中,需要将.换为/
"com/example/yunjiananquan/MainActivity"
完成FindClass方法编写
继续GetFieldID,目前第二个参数jclass以及确定了,下面配置两个const char*
第三个参数修改为java层”yunjian1"变量名:
第三个参数就是java层的变量名称
第四个参数即使返回值类型,yunjian1变量定义的为string类型,那么这里返回值填写string的全名
java.lang.String
那么在C里面的的写法不一样,这种编码叫做JNI字段描述符,L开头的描述符,就是类描述符,它后紧跟着类的字符串,然后分号“;”结束。
比如"Ljava/lang/String;"就是表示类型String;
"Ljava/lang/String"
到这里GetObjectField()中参数全部定义好,加入return返回str,并修改指出的返回值名,不能和类型一样
到这里获取java层普通变量的方法在c层就写好了,刚开始我们定义了GetObjectField()方法,回来发现缺少变量,就往上套娃。我们仔细观察发现,在上面的三个方法中,FindClass方法定义了java层的类名,GetFieldID方法定义了yunjian1变量的类型和名字,GetObjectField实现调用。
2、配置调用静态方法的JNI接口
那么接下来开始修改java层的获取静态变量方法
构建Java_com_example_yunjiananquan_MainActivity_Getstingyunjian2的方法体,修改参数,变量,return
这里使用GetStaticObjectField()方法获取java层的静态变量
jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
将GetStaticObjectField()方法加入.c文件,并定义变量,指出,参数等
JNIEnv* 默认参数,为:yunj
jclass 需要调用FindClass,获取FindClass返回值
jfieldID 需要调用GetStaticFieldID方法获取返回值
发现第二参数jclass与上面获取普通字段使用过的方法一样,复制下来即可
//为下面的jclass参数获取内容,GetStaticObjectField方法的第二个参数
jclass jclass=(*yunj)->FindClass(yunj, "com/example/yunjiananquan/MainActivity");
第三个参数jfieldID,怎么获取呢,在上面获取普通变量是使用的GetFieldID,那么获取静态变量,使用:GetStaticFieldID
jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char*,const char*);
继续补全GetStaticFieldID方法的参数指向等
JNIEnv* 默认类型,填写obj
jclass 需要调用FindClass,获取FindClass返回值
const char*参数填写要获取的java层的静态变量名 yunjian2
const char*参数设置静态变量的类型:"Ljava/lang/String"
最后补充return stt。
注意:这时候排错一下,这里方法返回值的类型和返回值的名称不可以一样,需要修改:
3、生成so文件,通过真机测试apk
接下来生成so文件
cd E:\data\Yunjiananquan\src
ndk-build
这似乎我们继续回到java层,写一个静态方法
连接真机,执行查看结果
未完待续。。。。。。
五、总结
1. jni接口:java native interface 2.作用:用于javahe/ c++代码的交互3.使用方法:jni静态注册和jni动态方法 (1) jni静态注册流程 a.在java代码中定义nativef修饰的方法; b.来到指定路径(src路径)执行javah -jni,根据java中native修饰的方法生成.h头文件; c.编写C、C++代码,导入头文件,同时实现我们.h头文件中的方法; d.编写两个mk文件: android.mk文件, application.mk文件(把四个文件放到jni目录); e.来到指定路径(jni文件夹所在路径)ndk-build生成so文件
(完整总结见下篇)