在进行Android开发的过程中,我们必定会遇到视频图像处理、高强度密集运算、特殊算法等场景,这时我们就不得不需要去接触一些C/C 代码,进行JNI开发。下面我将从Android.mk和CMake这两种方式教大家如何进行开发。文章结尾将给出演示的项目代码,如果你能耐心地仔细看完,相信你一定能掌握如何在Android下进行JNI开发。
使用Android.mk进行JNI开发
1.编写native接口和C/C 代码
定义native接口
代码语言:javascript复制package com.xuexiang.jnidemo;
public class JNIApi {
public native String stringFromJNI();
}
编写C/C 代码
代码语言:javascript复制extern "C" JNIEXPORT jstring
JNICALL
Java_com_xuexiang_jnidemo_JNIApi_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C ";
return env->NewStringUTF(hello.c_str());
}
2.编写Android.mk
模版如下:
代码语言:javascript复制LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native-lib
LOCAL_SRC_FILES := native-lib.cpp
## 导入logcat日志库
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY)
说明:
(1)LOCAL_PATH:=$(callmy-dir)
:指向当前目录的地址,包含该.mk
(2)include $(CLEAR_VARS)
:清理掉所有以LOCAL_开头的内容,这句话是必须的,因为如果所有的变量都是全局的,所有的可控的编译文件都需要在一个单独的GNU中被解析并执行。
(3)LOCAL_MODULE
:调用的库名,用来区分android.mk中的每一个模块。文件名必须是唯一的,不能有空格。注意,这里编译器会为你自动加上一些前缀lib和后缀.so,来保证文件是一致的。
(4)LOCAL_SRC_FILES
:变量必须包含一个C、C 或者java源文件的列表,这些会被编译并聚合到一个模块中,文件之间可以用空格或Tab键进行分割,换行请用""
(5)LOCAL_LDLIBS
:定义需要链接的库。一般用于链接那些存在于系统目录下本模块需要链接的库(比如这里的logcat库)。
(6)include $(BUILD_SHARED_LIBRARY)
:来生成一个动态库libnative-lib.so
3.编写Application.mk
代码语言:javascript复制# APP_ABI := armeabi armeabi-v7a arm64-v8a x86
APP_ABI := all
APP_OPTIM := release
## 引用静态库
APP_STL := stlport_static
#NDK_TOOLCHAIN_VERSION=4.8
#APP_PLATFORM := android-14
说明:
(1)APP_ABI
:定义编译so文件的CPU型号,all为所有类型。也可以指定特定类型的CPU型号,直接使用空格隔开。
(2)APP_OPTIM
:优化选项,非必填。其值可以为'release'或'debug'.此变量用来修改优先等级.默认情况下为release.在release模式下,将编译生成被优化了的二进制的机器码,而debug模块用来生成便于调试的未被优化的二进制机器码。
(3)APP_STL
:选择支持的C 标准库。在默认情况下,NDK通过Androoid自带的最小化的C 运行库(system/lib/libstdc .so)来提供标准C 头文件.然而,NDK提供了可供选择的C 实现,你可以通过此变量来选择使用哪个或链接到你的程序。
APP_STL := stlport_static --> static STLport library
APP_STL := stlport_shared --> shared STLport library
APP_STL := system --> default C runtime library
比如,这里我们使用到了 #include<string>
,就需要设置 stlport_static
4.设置项目根目录的local.properties文件
因为Android Studio 2.2以后推荐使用CMake进行JNI开发,因此需要修改一下参数进行兼容。
代码语言:javascript复制android.useDeprecatedNdk=true
5.编译C/C 代码生成so文件
cd 到jni(存放Android.mk的目录)下,执行 ndk-build
即可。
执行成功后,将会在jni的同级目录下生成 libs
和 obj
文件夹,存放的是编译好的so文件。
6.在模块的build.gradle中设置so文件路径
代码语言:javascript复制sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/libs']
}
}
至此完成了Android.mk的设置,下面我们就可以愉快地进行jni开发了!
上面介绍的Android.mk都可以在Eclispe和Android Studio下进行编译开发,可以说是一种比较传统的做法。下面我将介绍Android Studio着重推荐的CMake方式进行JNI开发。
使用CMake进行JNI开发
开发环境
JNI:Java Native Interface(Java 本地编程接口),一套编程规范,它提供了若干的 API 实现了 Java 和其他语言的通信(主要是 C/C )。Java 可以通过 JNI 调用本地的 C/C 代码,本地的 C/C 代码也可以调用 java 代码。Java 通过 C/C 使用本地的代码的一个关键性原因在于 C/C 代码的高效性。
在 Android Studio 下,进行JNI的开发,需要准备以下内容:
(1)Android Studio 2.2以上。
(2)NDK:这套工具集允许为 Android 使用 C 和 C 代码。
(3)CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果只计划使用 ndk-build,则不需要此组件。
(4LLDB:一种调试程序,Android Studio 使用它来调试原生代码。
创建支持C 的项目
新建支持C 的项目
在新建项目时,勾上 IncludeC support
就行了:
在向导的 Customize C Support 部分,有下列自定义项目可供选择:
- C Standard:使用下拉列表选择使用哪种 C 标准。选择 Toolchain Default 会使用默认的 CMake 设置。
- Exceptions Support:如果希望启用对 C 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle文件的 cppFlags中,Gradle 会将其传递到 CMake。
- Runtime Type Information Support:如果希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle文件的 cppFlags中,Gradle 会将其传递到 CMake。
支持C 的项目目录
(1)src/main/cpp
下存放的我们编写供JNI调用的C 源码。
(2)CMakeLists.txt
文件是CMake的配置文件,通常他包含的内容如下:
# TODO 设置构建本机库文件所需的 CMake的最小版本
cmake_minimum_required(VERSION 3.4.1)
# TODO 添加自己写的 C/C 源文件
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )
# TODO 依赖 NDK中的库
find_library( log-lib
log )
# TODO 将目标库与 NDK中的库进行连接
target_link_libraries( native-lib
${log-lib} )
build.gradle的配置
代码语言:javascript复制android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
// 默认是 “ cppFlags "" ”
// 如果要修改 Customize C Support 部分,可在这里加入
cppFlags "-frtti -fexceptions"
}
}
ndk {
// abiFiliter: ABI 过滤器(application binary interface,应用二进制接口)
// Android 支持的 CPU 架构
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'//, 'armeabi' 不支持了
}
}
buildTypes {
...
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
注意事项
- 1.在使用JNI前,需要加载so库
static {
System.loadLibrary("native-lib");
}
- 2.快速生成C 代码:先在java中定义native方法,然后使用
Alt Enter
快捷键自动生成C 方法体。
- 3.CPP 资源文件夹下面的文件和文件夹不能重名,不然 System.loadLibrary() 时找不到,会报错:java.lang.UnsatisfiedLinkError: Native method not found.
- 4.在定义库的名字时,不要加前缀 lib 和后缀 .so,不然会报错:java.lang.UnsatisfiedLinkError: Couldn’t load xxx : findLibrary【findLibrary returned null错误.
- 5.新建 C/C 源代码文件,要添加到 CMakeLists.txt 文件中。
# 增加c 源代码
add_library( # library的名称.
native-lib
# 标志库共享.
SHARED
# C 源码文件的相对路径.
src/main/cpp/native-lib.cpp )
# 将目标库与 NDK中的库进行连接
target_link_libraries( # 目标library的名称.
native-lib
${log-lib} )
- 6.引入第三方 .so文件,要添加到 CMakeLists.txt 文件中。
# TODO 添加第三方库
# TODO add_library(libavcodec-57
# TODO 原先生成的.so文件在编译后会自动添加上前缀lib和后缀.so,
# TODO 在定义库的名字时,不要加前缀lib和后缀 .so,
# TODO 不然会报错:java.lang.UnsatisfiedLinkError: Couldn't load xxx : findLibrary returned null
add_library(avcodec-57
# TODO STATIC表示静态的.a的库,SHARED表示.so的库
SHARED
IMPORTED)
set_target_properties(avcodec-57
PROPERTIES IMPORTED_LOCATION
# TODO ${CMAKE_SOURCE_DIR}:表示 CMakeLists.txt的当前文件夹路径
# TODO ${ANDROID_ABI}:编译时会自动根据 CPU架构去选择相应的库
# TODO ABI文件夹上面不要再分层,直接就 jniLibs/${ANDROID_ABI}/
# TODO ${CMAKE_SOURCE_DIR}/src/main/jniLibs/ffmpeg/${ANDROID_ABI}/libavcodec-57.so
${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavcodec-57.so)
- 7.引入第三方 .h 文件夹,也要添加到 CMakeLists.txt 文件中
# TODO include_directories( src/main/jniLibs/${ANDROID_ABI}/include )
# TODO 路径指向上面会编译出错(无法在jniLibs中引入),指向下面的路径就没问题
include_directories( src/main/cpp/ffmpeg/include )
- 8.C library编译生成的so文件,在
build/intermediates/cmake
下
至此完成了CMake的设置,下面我们就可以愉快地进行jni开发了!
讲完了两种进行JNI开发的姿势后,下面我们来简单讲讲JNI的基础语法。
JNI基础语法
基础类型
Java类型 | native类型 | 描述 |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
引用类型
JNI为不同的java对象提供了不同的引用类型,JNI引用类型如下:
在c里面,所有JNI引用类型其实都是jobject。
Native方法参数
- JNI接口指针是native方法的第一个参数,JNI接口指针的类型是JNIEnv。
- 第二个参数取决于native method是否静态方法,如果是非静态方法,那么第二个参数是对对象的引用,如果是静态方法,则第二个参数是对它的class类的引用
- 剩下的参数跟Java方法参数一一对应
extern "C" /* specify the C calling convention */
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
const char *str = env->GetStringUTFChars(s, 0);
...
env->ReleaseStringUTFChars(s, str);
return ...
}
点击查看JNI接口
签名描述
基础数据类型
Java类型 | 签名描述 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void |
引用数据类型
(以 L
开头,以 ;
结束,中间对应的是该类型的完整路径)
String : Ljava/lang/String;
Object : Ljava/lang/Object;
自定义类型 Area : Lcom/xuexiang/jnidemo/Area;
数组
(在类型前面添加 [
,几维数组就在前面添加几个 [
)
int [] :[I
Long[][] : [[J
Object[][][] : [[[Ljava/lang/Object
使用命令查看
代码语言:javascript复制javap -s <java类的class文件路径>
class文件存在于 build->intermediates->classes
下。
JNI常见用法
1、jni访问java非静态成员变量
1.使用 GetObjectClass
、 FindClass
获取调用对象的类
2.使用 GetFieldID
获取字段的ID。这里需要传入字段类型的签名描述。
3.使用 GetIntField
、 GetObjectField
等方法,获取字段的值。使用 SetIntField
、 SetObjectField
等方法,设置字段的值。
注意:即使字段是 private
也照样可以正常访问。
extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallNoStaticField(JNIEnv *env, jobject instance) {
//获取jclass
jclass j_class = env->GetObjectClass(instance);
//获取jfieldID
jfieldID j_fid = env->GetFieldID(j_class, "noStaticField", "I");
//获取java成员变量int值
jint j_int = env->GetIntField(instance, j_fid);
LOGI("noStaticField==%d", j_int);//noStaticField==0
//Set<Type>Field 修改noStaticKeyValue的值改为666
env->SetIntField(instance, j_fid, 666);
}
2、jni访问java静态成员变量
1.使用 GetObjectClass
、 FindClass
获取调用对象的类
2.使用 GetStaticFieldID
获取字段的ID。这里需要传入字段类型的签名描述。
3.使用 GetStaticIntField
、 GetStaticObjectField
等方法,获取字段的值。使用 SetStaticIntField
、 SetStaticObjectField
等方法,设置字段的值。
3、jni调用java非静态成员方法
1.使用 GetObjectClass
、 FindClass
获取调用对象的类
2.使用 GetMethodID
获取方法的ID。这里需要传入方法的签名描述。
3.使用 CallVoidMethod
执行无返回值的方法,使用 CallIntMethod
、 CallBooleanMethod
等执行有返回值的方法。
extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallParamMethod(JNIEnv *env, jobject instance) {
//回调JNIApi中的noParamMethod
jclass clazz = env->FindClass("com/xuexiang/jnidemo/JNIApi");
if (clazz == NULL) {
printf("find class Error");
return;
}
jmethodID id = env->GetMethodID(clazz, "paramMethod", "(I)V");
if (id == NULL) {
printf("find method Error");
return;
}
env->CallVoidMethod(instance, id, number);
}
4、jni调用java静态成员方法
1.使用 GetObjectClass
、 FindClass
获取调用对象的类
2.使用 GetStaticMethodID
获取方法的ID。这里需要传入方法的签名描述。
3.使用 CallStaticVoidMethod
执行无返回值的方法,使用 CallStaticIntMethod
、 CallStaticBooleanMethod
等执行有返回值的方法。
5、jni调用java构造方法
1.使用 FindClass
获取需要构造的类
2.使用 GetMethodID
获取构造方法的ID。方法名为 <init>
, 这里需要传入方法的签名描述。
3.使用 NewObject
执行创建对象。
extern "C"
JNIEXPORT jint JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallConstructorMethod(JNIEnv *env, jobject instance) {
//获取jclass
jclass j_class = env->FindClass("com/xuexiang/jnidemo/Area");
//找到构造方法jmethodID public Area(int width, int height)
jmethodID j_constructor_methoid = env->GetMethodID(j_class, "<init>", "(II)V");
//初始化java类构造方法 public Area(int width, int height)
jobject j_Area_obj = env->NewObject(j_class, j_constructor_methoid, 2, 10);
//找到getArea() jmethodID
jmethodID j_getArea_methoid = env->GetMethodID(j_class, "getArea", "()I");
//调用java中的 public int getArea() 获取面积
jint j_area = env->CallIntMethod(j_Area_obj, j_getArea_methoid);
LOGI("面积==%d", j_area);//面积==20
return j_area;
}
6、jni引用全局变量
1.使用 NewGlobalRef
创建全局引用,使用 NewLocalRef
创建局部引用。
2.局部引用,通过DeleteLocalRef手动释放对象;全局引用,通过DeleteGlobalRef手动释放对象。
3.引用不主动释放会导致内存泄漏。
7、jni异常处理
1.使用 ExceptionOccurred
进行异常的检测。注意,这里只能检测java异常。
2.使用 ExceptionClear
进行异常的清除。
3.使用 ThrowNew
来上抛异常。
注意, ExceptionOccurred
和 ExceptionClear
一般是成对出现的,类似于java的try-catch。
//上抛java异常
void throwException(JNIEnv *env, const char *message) {
jclass newExcCls = env->FindClass("java/lang/Exception");
env->ThrowNew(newExcCls, message);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_jniTryCatchException(JNIEnv *env, jobject instance) {
//获取jclass
jclass j_class = env->GetObjectClass(instance);
//获取jfieldID
jfieldID j_fid = env->GetFieldID(j_class, "method", "Ljava/lang/String666;");
//检测是否发生Java异常
jthrowable exception = env->ExceptionOccurred();
if (exception != NULL) {
LOGE("jni发生异常");
//jni清空异常信息
env->ExceptionClear(); //需要和ExceptionOccurred方法成对出现
throwException(env, "native出错!");
}
}
8、日志打印
代码语言:javascript复制#include <android/log.h> //引用android log
//定义日志打印的方法
#define TAG "CMake-JNI" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型
LOGE("jni发生异常"); //日志打印