前面我们已经讲解过,如何在Java中使用JNI和DLL,这和在Android中使用JNI和SO原理是完全一致的,流程也相似,只是编译的平台和工具不同 如果我们前面都学透了,现在就会很轻松了。下面我们开始讲解,如何在Android中使用JNI
安装NDK开发环境
Tools - SDK Manager - 安装CMake,NDK,LLDB等组件 编写Java调用JNI的接口
package com.easing.android;
public class JniHello {
static {
System.loadLibrary("hello");
}
public native void hello();
public native void printMessage(String message);
public native int sum(int a, int b);
}
根据Java接口生成JNI头文件
在java目录下打开Terminal面板,输入javah com.easing.android.JniHello指令,根据Java接口自动生成对应的JNI头文件
#include
#ifndef _Included_com_easing_android_JniHello
#define _Included_com_easing_android_JniHello
extern "C" {
JNIEXPORT void JNICALL Java_com_easing_android_JniHello_hello(JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_com_easing_android_JniHello_printMessage(JNIEnv *, jobject, jstring);
JNIEXPORT jint JNICALL Java_com_easing_android_JniHello_sum(JNIEnv *, jobject, jint, jint);
}
#endif
编写CPP实现JNI头文件
在src下新建一个jni目录,专门用来编写c++代码 将刚才生成的头文件剪切到jni目录下,创建一个cpp文件,实现头文件中的接口
#include
#include
#include
#include
#define LOG_TAG "JniHello"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL Java_com_easing_android_JniHello_hello(JNIEnv *env, jobject obj) {
LOGD("JniHello");
}
extern "C" JNIEXPORT void JNICALL Java_com_easing_android_JniHello_printMessage(JNIEnv *env, jobject obj, jstring message) {
const char *str = env->GetStringUTFChars(message, NULL);
LOGI(str);
}
extern "C" JNIEXPORT jint JNICALL Java_com_easing_android_JniHello_sum(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}
NDK出于安全性的考虑,默认限制了jstring到char*的转换,要经过适当配置才能使用,这个我们下篇博客我们再细讲
编写Android.mk和Application.mk文件
c++代码想编译成so库,必须通过Android.mk和Application.mk来进行配置 Android.mk指定so库名称,要编译的文件 Application.mk则指定要适配的CPU架构,最低的安卓系统版本 关于这两者更详细的配置,我们后面再细讲,现在重要的学会调用JNI完整流程
#Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libhello
LOCAL_LDLIBS := -lm -llog
LOCAL_SRC_FILES := com_easing_android_JniHello.cpp
include $(BUILD_SHARED_LIBRARY)
#Application.mk
APP_ABI := all
APP_PLATFORM := android-23
设置支持的CPU架构
不同的CPU架构需要不同的so文件,如果我们要支持所有CPU架构的话,就需要编译并打包多个so文件,这样会大幅增加安装包体积 所以一般我们只会支持用户量最大(arm64-v8a)或兼容性最好(armeabi-v7a)的CPU架构 现在的新机子一般都是arm64-v8a架构的,使用arm64-v8a库运行效率最高 但是很多旧机子或旧的so库都是armeabi-v7a版本的,为了兼容一般都会添加armeabi-v7a支持 arm64-v8a机型也是可以使用armeabi-v7a的库的,如果不在意性能损失,可以只添加armeabi-v7a的so库,这样体积比较小,so包较少管理也方便
支持哪些CPU架构可以通过Gradle中的abiFilters选项来配置
android {
defaultConfig {
//过滤CPU架构,只使用armv7的库
ndk {
abiFilters "armeabi-v7a"
}
}
}
开启Gradle中的JNI编译选项
c++代码和mk文件编写完成后,我们有两种方式来使用jni代码 一种是让gradle自动将jni代码编译成为so库并引用 一种是通过ndk指令手动将其编译为so库,手动添加引用 第一种方式由于可以对c++代码自动编译自动引用,很适合经常修改c++源码的情景 第二种方式需要手动编译,麻烦一点,如果需要将so库分享给别人使用,则必须这么做
如果我们想Gradle帮我们自动编译C++代码,需要开启externalNativeBuild选项,并指定Android.mk位置 编译成功后的so库存放在build/intermediates/ndkBuild目录下,可以直接拷贝出来供其它用途
android {
defaultConfig {
}
//编译jni目录,开启这个选项后,会自动编译C++代码生成so文件,并自动引用
//开启此选项后,就不需要通过sourceSets选项来指定so库位置了,否则会发生so库冲突
externalNativeBuild {
ndkBuild {
path 'src/jni/Android.mk'
}
}
}
在Gradle中指定so库加载目录
如果想直接使用编译好的so库,可以通过Gradle中的sourceSets选项来指定so库的加载位置 但是要注意,同样的so文件,不能既通过externalNativeBuild来编译,又通过sourceSets来引用 这样会有两份同名的so库,造成冲突,从而最终无法编译通过
android {
defaultConfig {
}
//加载指定位置的so库
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
使用NDK编译SO库
我们可以使用externalNativeBuild指令来编译出so文件,但这样生成的so文件会被直接使用 我们也可以手动执行ndk指令来编译so文件,这样可以自己决定如何使用
在jni目录下打开Terminal面板,输入ndk-build指令,就会在jni同级目录生成一个名为libs的so库文件夹 在Java代码中使用JNI接口
这个就简单了,new出一个JniHello对象,直接调用就行了,主要是验证下结果