本篇总结Android JNI如何通过两种不同的构建方式与C/C++进行交互,话不多少先上两张效果图,注意:本项目是基于Android Studio3.0开发的。
以上就是与C/C++语言交互的内容部分分,涵盖了开发基本上的需求,让我进入正题
两种不同的构建方式
以下介绍一种基于CMake的方式来构建,一种是基于比较传统的mk文件构建方式,虽然基于CMake的方式更加的只能,但是也是基于传统的方式来的,这种方式只能适用于Android Studio 2.2以上的版本。无论基于什么构建,首先,你必须要下载Android NDK开发包。点我下载
基于CMake方式
基于CMake方式构建的项目,在写C++和C代码的时候可以与写Java代码一样的有提示,而且还可以直接的debug。
打开一个项目,从菜单栏中选择 Tools > Android > SDK Manager。
点击 SDK Tools 选项卡。
勾选 LLDB,CMake 和 NDK。如图:
- The Android Native Development Kit (NDK): 让你能在 Android 上面使用 C 和 C++ 代码的工具集。
- CMake: 外部构建工具。如果你准备只使用 ndk-build 的话,可以不使用它。
- LLDB: Android Studio 上面调试本地代码的工具。
创建支持C/C++的项目
点击新建项目,勾选以下选项以支持C/C++
- C++ Standard:点击下拉框,可以选择标准 C++,或者选择默认 CMake 设置的 Toolchain Default 选项。
- Exceptions Support:如果你想使用有关 C++ 异常处理的支持,就勾选它。勾选之后,Android Studio 会在 module 层的 build.gradle 文件中的 cppFlags 中添加 -fexcetions 标志。
-
Runtime Type Information Support:如果你想支持 RTTI,那么就勾选它。勾选之后,Android Studio 会在 module 层的 build.gradle 文件中的 cppFlags 中添加 -frtti 标志。
最后点击Finish完成工程的创建。
与之前普通项目就多了这三个文件。 - .externalNativeBuild:这个文件夹与下面的CMakeLists.txt文件是对应的,他主要由CMakeLists.txt文件来构建so文件库的文件夹。
- cpp:这个是存放C/C++源代码的文件夹。
- CMakeLists.txt:这个文件主要是构建整个C++/C的脚本文件。里面需要编写构建的代码,有点类似Android.mk文件。
到这里,基于CMake的构建就完成了,点击运行可以直接看到来自C++的字符。
在这里,主要讲将CMakeLists.txt的用法,如何用了构建:
先看如下代码:
cmake_minimum_required(VERSION 3.4.1)
include_directories(src/main/cpp/include/)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
为了让 CMake 将源代码(native source code)编译成 native library。需要在编译文件中添加 cmake_minimum_required() 和 add_library() 命令。
看下图:
include_directories(src/main/cpp/include/)
这行代码的意思就是当你使用 add_library(),将一个源文件(source file)或库添加到你的 CMake 构建脚本,同步你的项目,然后你会发现 Android studio 将关联的头文件也显示了处理。然而,为了让 CMake 在编译时期能定位到你的头文件,你需要在 CMake 构建脚本中添加 include_directories() 命令,并指定头文件路径。
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
这段代码就是如果你的native-lib代码中有引用别的头文件,在这里就需要把头文件链接到这里,这个与add_library的顺序是一致的。
最后在Java代码中load的so库是与上面native-lib是一致的。
static {
System.loadLibrary("native-lib");
}
至此,一个完整的jni项目就集成完毕,至于如何引用第三方C/C++的库到CMake中,后期会介绍将librtmp,FFmpeg集成到Android中来讲解。
传统的构建JNI项目的方式
这个方式比较古老了,相信从eclipse转过来的童鞋都知道,这里我就不详细讲述这种方式的集成,这里推荐大家看这位大神的一篇博客:看不到我,看不到我
我这里就详细总结一下Android.mk与Application.mk文件的编写。
Android.mk
Android.mk是Android提供的一种makefile文件,用来指定诸如编译生成so库名、引用的头文件目录、需要编译的.c/.cpp文件和.a静态库文件
最基本的格式应该是这样:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_xxx := xxx
LOCAL_MODULE := NdkTools
LOCAL_SRC_FILES := NdkTools.cpp
LOCAL_xxx := xxx
include $(BUILD_SHARED_LIBRARY)
- LOCAL_PATH:变量制定了该.mk的路径,$(call my-dir)调用NDK内部的函数获得当前.mk文件的路径,每个Android.mk文件必须以定义LOCAL_PATH为开始。它用于在开发tree中查找源文件。宏my-dir 则由Build System提供。返回包含Android.mk的目录路径。
- include $(CLEAR_VARS):清空了除了LOCAL_PATH之外的所有LOCAL_xxx变量的值,CLEAR_VARS 变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx.
例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等。但不清理LOCAL_PATH.
这个清理动作是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能避免相互影响。 - 中间就是对于模块参数的设置,主要包括:模块名字、模块源文件、模块类型、编译好的模块存放位置、以及编译的平台等
- include $(BUILD_xxx_xxx): 执行NDK的默认脚本,它会收集include $(CLEAR_VARS)脚本后所有定义的LOCAL_xxx变量,然后根据它们来生成模块。BUILD_SHARED_LIBRARY:是Build System提供的一个变量,指向一个GNU Makefile Script。
它负责收集自从上次调用 include $(CLEAR_VARS) 后的所有LOCAL_XXX信息。并决定编译为什么。
BUILD_STATIC_LIBRARY :编译为静态库。
BUILD_SHARED_LIBRARY :编译为动态库
BUILD_EXECUTABLE :编译为Native C可执行程序
BUILD_PREBUILT :该模块已经预先编译
下面详细说说中间比较常见的东西吧:
- LOCAL_MODULE := NdkTools:LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。Build System会自动添加适当的前缀和后缀。例如,NdkTools,要产生动态库,则生成libNdkTools.so. 但请注意:如果模块名被定为:libNdkTools,则生成libfoo.so. 不再加前缀
- LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 指定最后生成的模块的目标地址,TARGET_ROOT_OUT:根文件系统,路径为out/target/product/generic/root
TARGET_OUT:system文件系统,路径为out/target/product/generic/system
TARGET_OUT_DATA:data文件系统,路径为out/target/product/generic/data
除了上面的这些,NDK还提供了很多其他的TARGET_XXX_XXX变量,用于将生成的模块拷贝到输出目录的不同路径,默认是TARGET_OUT - LOCAL_SRC_FILES := NdkTools.cpp:LOCAL_SRC_FILES变量必须包含将要打包如模块的C/C++ 源码。不必列出头文件,build System 会自动帮我们找出依赖文件。缺省的C++源码的扩展名为.cpp. 也可以修改,通过LOCAL_CPP_EXTENSION
可能以后还会经常用到一些以“LOCAL_”开头的编译变量,这些变量是啥意思,可以参考这篇文章:看不到我,看不到我
使用C代码通过JNI与Java代码沟通
好了,前面铺垫完了,接下才是编写代码中经常需要用到的地方
- 传递Boolean类型
JNIEXPORT jboolean
JNICALL Java_com_dramascript_ndktest_NdkTools_getBoolean
(JNIEnv *evn, jobject obj, jboolean b) {
unsigned char bo = b;
bo = 1;
return (jboolean)bo;
}
- 传递byte类型
JNIEXPORT jbyte
JNICALL Java_com_dramascript_ndktest_NdkTools_getByte
(JNIEnv *evn, jobject obj, jbyte b) {
int i = b;
i = 15;
return (jbyte)
i;
}
- 传递char类型
JNIEXPORT jchar
JNICALL Java_com_dramascript_ndktest_NdkTools_getChar
(JNIEnv *evn, jobject obj, jchar c) {
char ch = c;
ch = 'S';
return (jchar)
ch;
}
- 传递short类型
JNIEXPORT jshort
JNICALL Java_com_dramascript_ndktest_NdkTools_getShort
(JNIEnv *evn, jobject obj, jshort s) {
short st = s;
st = 100;
return (jshort)
st;
}
- 传递int类型
JNIEXPORT jint
JNICALL Java_com_dramascript_ndktest_NdkTools_getInt
(JNIEnv *evn, jobject obj, jint i) {
int a = i;
a = 1024;
return (jint)
a;
}
- 传递long类型
JNIEXPORT jlong
JNICALL Java_com_dramascript_ndktest_NdkTools_getLong
(JNIEnv *evn, jobject obj, jlong l) {
long lg = l;
lg = 1024L;
return (jlong)
lg;
}
- 传递float类型
JNIEXPORT jfloat
JNICALL Java_com_dramascript_ndktest_NdkTools_getFloat
(JNIEnv *evn, jobject obj, jfloat f) {
float ft = f;
ft = 23;
return (jfloat)
ft;
}
- 传递double类型
JNIEXPORT jdouble
JNICALL Java_com_dramascript_ndktest_NdkTools_getDouble
(JNIEnv *evn, jobject obj, jdouble d) {
double dl = d;
dl = 2048.12;
return (jdouble)
dl;
}
- 传递string类型
JNIEXPORT jstring
JNICALL Java_com_dramascript_ndktest_NdkTools_getString
(JNIEnv *env, jobject obj, jstring s) {
char *st = (char *) env->GetStringUTFChars(s, 0);
char *str = "我很好!";
jstring rtn;
rtn = env->NewStringUTF(str);
return rtn;
}
- 传递数组类型
JNIEXPORT jbyteArray
JNICALL Java_com_dramascript_ndktest_NdkTools_getByteArray
(JNIEnv *evn, jobject obj, jbyteArray ba) {
//获得byte数组
jbyte * bytes = evn->GetByteArrayElements(ba, 0);
int chars_len = evn->GetArrayLength(ba);
//返回新的byte数组
jbyteArray arr = evn->NewByteArray(6);
jbyte * by = evn->GetByteArrayElements(arr, 0);
char ch[10] = "abcd";
for (int i = 0; i < 4; ++i) {
by[i] = ch[i];
}
evn->SetByteArrayRegion(arr, 0, 6, by);
return arr;
}
- 传递对象类型
JNIEXPORT jobject
JNICALL Java_com_dramascript_ndktest_NdkTools_getObject
(JNIEnv *env, jobject obj, jobject paramIn) {
//获取
jclass paramInClass = env->GetObjectClass(paramIn);
if (paramInClass == NULL) {
return NULL;
}
if (env->IsInstanceOf(obj, paramInClass))//判断jobject是否是某个jclass类型。
{
jboolean iscopy;
jfieldID intId = env->GetFieldID(paramInClass, "age", "I");
jint num = (int) env->GetIntField(paramIn, intId);
jfieldID strId = env->GetFieldID(paramInClass, "name", "Ljava/lang/String;");
jstring str = (jstring)(env)->GetObjectField(paramIn, strId);
const char *locstr = env->GetStringUTFChars(str, &iscopy);
env->ReleaseStringUTFChars(str, locstr);
}
//返回
jclass cls = env->FindClass("com/dramascript/ndktest/User");
jmethodID id = env->GetMethodID(cls, "", "()V");
jobject paramOut = env->NewObjectA(cls, id, 0);
jfieldID intId = env->GetFieldID(cls, "age", "I");
env->SetIntField(paramOut, intId, 23);
jfieldID strId = env->GetFieldID(cls, "name", "Ljava/lang/String;");
env->SetObjectField(paramOut, strId, (jstring)(env)->NewStringUTF("林俊杰"));
return paramOut;
}
- 传递数组对象类型
JNIEXPORT jobjectArray
JNICALL Java_com_dramascript_ndktest_NdkTools_getObjectArray
(JNIEnv *env, jobject _obj, jobjectArray objarr) {
//获取
//JNI提供了两个函数来访问对象数组,GetObjectArrayElement返回数组中指定位置的元素,
// SetObjectArrayElement修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据
// 中的所有对象元素或者一次复制多个对象元素到缓冲区。
jobject objects = env->GetObjectArrayElement(objarr, 0);//返回第0个对象
jsize length = (env)->GetArrayLength(objarr);
//返回
//申明一个object数组
jobjectArray args = 0;
//获取object所属类,一般为ava/lang/Object就可以了
jclass objClass = (env)->FindClass("java/lang/Object");
//新建object数组
args = (env)->NewObjectArray(5, objClass, 0);
//给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中
for (int i = 0; i < 5; i++) {
jclass cls = env->FindClass("com/dramascript/ndktest/User");
jmethodID id = env->GetMethodID(cls, "", "()V");
jobject paramOut = env->NewObjectA(cls, id, 0);
jfieldID intId = env->GetFieldID(cls, "age", "I");
env->SetIntField(paramOut, intId, 23);
jfieldID strId = env->GetFieldID(cls, "name", "Ljava/lang/String;");
env->SetObjectField(paramOut, strId, (jstring)(env)->NewStringUTF("书戏"));
//添加到objcet数组中
(env)->SetObjectArrayElement(args, i, paramOut);
}
return args;
}
JNIEXPORT jobject
JNICALL Java_com_dramascript_ndktest_NdkTools_getObjectList
(JNIEnv *env, jobject obj) {
jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用
jmethodID list_costruct = env->GetMethodID(list_cls, "", "()V"); //获得得构造函数Id
jobject list_obj = env->NewObject(list_cls, list_costruct); //创建一个Arraylist集合对象
//或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;
jmethodID list_add = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");
jclass cls = env->FindClass("com/dramascript/ndktest/User");
jmethodID id = env->GetMethodID(cls, "", "()V");
for (int i = 0; i < 3; i++) {
jobject paramOut = env->NewObject(cls, id);
env->CallBooleanMethod(list_obj, list_add, paramOut); //执行Arraylist类实例的add方法,添加一个stu对象
}
return list_obj;
}
- C层代码调用Java代码的某个方法
JNIEXPORT void JNICALL Java_com_dramascript_ndktest_NdkTools_callSetData
(JNIEnv *env, jobject obj){
jclass clz = env->FindClass("com/dramascript/ndktest/NdkTools");
jmethodID mid = env->GetMethodID(clz, "setData", "(Ljava/lang/String;I)V");
env->CallVoidMethod(obj, mid, env->NewStringUTF("haha in C ."),20);
}
注意:在C++中调用JNI头文件的方法和在C代码中调用的方法传的参数是不一样的,如下:
//在C++可以少env这个参数
jfieldID intId = env->GetFieldID(paramInClass, "age", "I");
//在C里面是不能缺少的
jfieldID intId = (*env)->GetFieldID(*env,paramInClass, "age", "I");
至此,结束,代码在:Demo代码,如果觉得不错,给点打赏呗。