内容介绍:JNI(一) - Android Studio简单开发流程
版权声明:本文为原创文章,未经允许不得转载
联系方式:[email protected]
博客地址:[http://blog.csdn.net/kevindgk(http://blog.csdn.net/kevindgk)
开发文档:https://kevindgk.github.io/
Demo地址:https://github.com/KevinDGK/JNITest
JNI:Java Native Interface(Java 本地接口),它是为了方便Java调用C、C++等本地代码所封装的一层接口。
NDK:Native Development Kit(本地开发工具包),通过NDK可以在Android中更加方便的通过JNI来访问本地代码。
打开AS的SDK Manager,安装NDK插件:
整个NDK比较大,解压缩完2个G,自动安装到配置的sdk目录下:
安装完毕后,点开structure,配置NDK的路径:
配置NDK的环境变量:
验证是否配置成功:
在命令行输入ndk-build,如果显示以上内容,表示成功。
项目名称:JNITest
包名:com.dgk.jnitest
实现功能:界面有两个按钮,点击Get从本地方法中获取一个字符串,并toast出来;点击Set向本地方法传递一个字符串,打印到Logcat。
2.1 写Android界面和基本逻辑,并声明两个本地方法。
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private static final String tag = "【MainActivity】";
private Button btn_get;
private Button btn_set;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_get = (Button) findViewById(R.id.btn_get);
btn_set = (Button) findViewById(R.id.btn_set);
btn_get.setOnClickListener(this);
btn_set.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_get:
Log.i(tag, "点击Get按钮");
Toast.makeText(this, getStringFromJNI(), Toast.LENGTH_SHORT).show();
break;
case R.id.btn_set:
Log.i(tag, "点击Set按钮");
setStringToJNI("Hello C! 我是一只来自Java世界的Cat,喵~~~");
break;
}
}
/**
* 声明get方法
* - 作用是从本地方法返回一个String
* @return 返回一个字符串
*/
public native String getStringFromJNI();
/**
* 声明set方法
* - 作用是向本地方法传递一个String
*/
public native void setStringToJNI(String str);
/**
* 加载本地代码库
* - 在应用启动的时候加载名为"libjni-test.so"的代码库,该库在安装Apk的时候就已经
* 被包管理器拆包放到了/data/data/包名/lib/目录下了。
*/
static {
System.loadLibrary("jni-test");
}
}
activity_main.xml
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
2.2 写C代码
选中main右键选择New->Folder->JNI Folder,会在main路径下创建一个jni的文件夹,用于存放本地源代码,src\main\jni这个是AS默认的jni源代码存放路径,也可以自己手动创建。
创建Hello.c
#include
#include
#include
#include
#define LOG_TAG "【C_LOG】"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
JNIEXPORT jstring JNICALL Java_com_dgk_jnitest_MainActivity_getStringFromJNI (JNIEnv *env, jobject thiz)
{
LOGI("调用 C getStringFromJNI() 方法\n");
char* str = "Hello Java! 我是一只来自C世界的Dog,汪!!!";
return (*env)->NewStringUTF(env, str);
}
JNIEXPORT void JNICALL Java_com_dgk_jnitest_MainActivity_setStringToJNI (JNIEnv* env, jobject thiz, jstring str){
LOGI("调用 C setStringFromJNI() 方法\n");
char* string = (char*)(*env)->GetStringUTFChars(env, str, NULL);
LOGI("%s\n", string);
(*env)->ReleaseStringUTFChars(env, str, string);
}
C代码的我的理解,可以先大致看一下:
#include
#include
#include
#include
/*
在C语言中标准输出的方法是printf,但是打印出来的内容在logcat看不到,需要使用
__android_log_print()方法打印log,才能在logcat看到,由于该方法名比较长,我们在
这里需要定义宏,使得在C语言中能够向Android一样打印log。
注意:该方法还需要在gradle中声明ldLibs "log",详见build.gradle
*/
#define LOG_TAG "【C_LOG】"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
/*
返回类型 Java_包名_类名_方法名
- 返回类型:jstring,即java格式的String
- 参数:
可以在jni.h头文件中查找到各个自定义变量的原类型。
必带的两个参数:
- JNIEnv:是结构体JNINativeInterface的一级指针,即JNINativeInterface*,
结构体JNINativeInterface:接口函数指针表,该表就是用来Java和C语言之间进行交互的,
包含着Java变量和C变量之间的对应关系,可以用于变量之间的转换。
JNIEnv* env:是JNIEnv的一级指针,
是结构体JNINativeInterface的二级指针,即JNINativeInterface**
- jobject thiz:谁调用了这个本地函数,那么这个thiz就是指的哪个对象,本项目中是MainActicity
*/
JNIEXPORT jstring JNICALL Java_com_dgk_jnitest_MainActivity_getStringFromJNI (JNIEnv *env, jobject thiz)
{
LOGI("调用 C getStringFromJNI() 方法\n");
/*
char* 相当与C语言中的字符串
char* 指的是字符串str的指针,指向的是该str的内存区域,
但是在C语言中操作字符串可以直接使用一级指针,来获取字符串的各个元素。
*/
char* str = "Hello Java! 我是一只来自C世界的Dog,汪!!!";
/*
通过在jni.h的结构体JNINativeInterface中查找jstring,可以找到将C语言的字符串转换成
Java字符串的代码:
const struct JNINativeInterface* functions;
jstring NewStringUTF(const char* bytes){
return functions->NewStringUTF(this, bytes);
}
即:JNINativeInterface*->NewStringUTF(*env, str)
即:(env*)->NewStringUTF(*env, str)
将str转换并保存在结构体中,然后使用间接引用运算符->来获得这个jstring成员。
在这里,结构体是JNINativeInterface,他的一级指针是JNIEnv,即*env(因为env又是JNIEnv的一级指针)。
所以(*env)->NewStringUTF(env, str)就相当于JNINativeInterface.jstring,表示该结构体内的
jstring格式的变量。
*/
return (*env)->NewStringUTF(env, str);
}
JNIEXPORT void JNICALL Java_com_dgk_jnitest_MainActivity_setStringToJNI (JNIEnv* env, jobject thiz, jstring str){
LOGI("调用 C setStringFromJNI() 方法\n");
// 将收到的jstring转换成UTF-8格式的C字符串
char* string = (char*)(*env)->GetStringUTFChars(env, str, NULL);
LOGI("%s\n", string);
// 显示释放转换成UTf-8的string空间,如果不显示调用,JVM会一直保存该对象,不回收,容易导致内存溢出
(*env)->ReleaseStringUTFChars(env, str, string);
}
2.3 配置gradle
配置完gradle后如果直接同步,会提示:
如果点击第一行,会转到google开发网站,显示一个实验性的gradle,可以用来集成NDK,在本篇不予介绍。点击第二行,或者直接在gradle.properties中贴上代码,表示使用当前过时的插件:
android.useDeprecatedNdk=true
直接Build->Make Project,会自动生成.so库,保存路径为\app\build\intermediates\ndk\debug\lib
将生成的lib包下的需要的.so库copy到main/jniLibs目录中即可:
该文件夹为AS默认的jni库的目录,可以在gradle中修改。
运行手机:魅族pro5 Android 5.1
点击GET:
点击SET:
到现在为止,已经完成了最简单的JNI开发流程。
在这里我们的C代码的方法名和参数是自己写的,而实际上完全可以让jdk来帮我们生成,流程是:
编译java代码生成.class文件—>使用javah命令生成C语言的头文件—>将.h文件中的JNI方法声明复制到C文件中,并完善方法的方法体。这样的话,可以加速我们的开发流程,同时避免写错方法名,但是在这个过程中由于对自己的开发环境还有命令了解不够,经常会出问题,所以小编在简洁流程中并未写出。
如果对C语言不了解,就需要自动生成头文件了,在这里给出小编自己的流程,一行代码搞定:
在Make Project之后,在Terminal中输入命令(当前目录为应用的根目录)
javah -d . -cp android.jar的地址;编译生成的.class目录 全类名
格式:
-d . 表示生成文件存放在当前目录
-cp class文件的加载根路径,在这里需要加载两个类,一个是android的基础类库,要不然jdk本身识别不了android的东西,比如Activity\Log等;第二个是MainActivity编译生成的路径 + 全类名,注意中间有个空格,而且不能写全地址,不识别。
运行完该命令后会默认在当前目录(即程序的根目录)下生成一个文件:com_dgk_jnitest_MainActivity.h,该文件就是JNI的头文件,里面定义好了JNI的本地方法声明,可以直接复制来用。
对于这一步,网上的命令七花八门,感觉好多复制过来直接用然后就发表了,很多坑,令人很头疼,搞了一天,烦躁!其实主要的问题就是,Android Studio对JNI的兼容性刚开始不是很好,后来又更新的版本比较多,再加上如果对gradle不熟悉的话,就会造成写起来很难受的感觉。
还好,当真正的把整个流程给串起来以后就会感觉还是很清晰的,不过还是希望成熟的插件能够将整个流程串起来~