在进行Android开发的过程中,我们必定会遇到视频图像处理、高强度密集运算、特殊算法等场景,这时我们就不得不需要去接触一些C/C++代码,进行JNI开发。下面我将从Android.mk和CMake这两种方式教大家如何进行开发。文章结尾将给出演示的项目代码,如果你能耐心地仔细看完,相信你一定能掌握如何在Android下进行JNI开发。
使用Android.mk进行JNI开发
1.编写native接口和C/C++代码
定义native接口
package com.xuexiang.jnidemo;public class JNIApi { public native String stringFromJNI();}复制代码
编写C/C++代码
extern "C" JNIEXPORT jstringJNICALLJava_com_xuexiang_jnidemo_JNIApi_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str());}复制代码
模版如下:
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := native-libLOCAL_SRC_FILES := native-lib.cpp## 导入logcat日志库LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -lloginclude $(BUILD_SHARED_LIBRARY)复制代码
说明:
-
LOCAL_PATH := $(call my-dir)
:指向当前目录的地址, -
include $(CLEAR_VARS)
:清理掉所有以LOCAL_开头的内容,这句话是必须的,因为如果所有的变量都是全局的,所有的可控的编译文件都需要在一个单独的GNU中被解析并执行。 -
LOCAL_MODULE
:调用的库名,用来区分android.mk中的每一个模块。文件名必须是唯一的,不能有空格。注意,,来保证文件是一致的。 -
LOCAL_SRC_FILES
:变量必须包含一个C、C++或者java源文件的列表,这些会被编译并聚合到一个模块中,文件之间可以用空格或Tab键进行分割,换行请用"\" -
LOCAL_LDLIBS
:定义需要链接的库。一般用于链接那些存在于系统目录下本模块需要链接的库(比如这里的logcat库)。 -
include $(BUILD_SHARED_LIBRARY)
:来生成一个动态库libnative-lib.so
# APP_ABI := armeabi armeabi-v7a arm64-v8a x86APP_ABI := allAPP_OPTIM := release## 引用静态库APP_STL := stlport_static#NDK_TOOLCHAIN_VERSION=4.8#APP_PLATFORM := android-14复制代码
说明:
-
APP_ABI
:定义编译so文件的CPU型号,all为所有类型。也可以指定特定类型的CPU型号,直接使用空格隔开。 -
APP_OPTIM
:优化选项,非必填。其值可以为'release'或'debug'.此变量用来修改优先等级.默认情况下为release.在release模式下,将编译生成被优化了的二进制的机器码,而debug模块用来生成便于调试的未被优化的二进制机器码。 -
APP_STL
:选择支持的C++标准库。在默认情况下,NDK通过Androoid自带的最小化的C++运行库(system/lib/libstdc++.so)来提供标准C++头文件.然而,NDK提供了可供选择的C++实现,你可以通过此变量来选择使用哪个或链接到你的程序。
APP_STL := stlport_static --> static STLport libraryAPP_STL := stlport_shared --> shared STLport libraryAPP_STL := system --> default C++ runtime library复制代码
比如,这里我们使用到了#include <string>
,就需要设置stlport_static
4.设置项目根目录的local.properties文件
因为Android Studio 2.2以后推荐使用CMake进行JNI开发,因此需要修改一下参数进行兼容。
android.useDeprecatedNdk=true复制代码
5.编译C/C++代码生成so文件
cd 到jni(存放Android.mk的目录)下,执行ndk-build
即可。
执行成功后,将会在jni的同级目录下生成libs
和obj
文件夹,存放的是编译好的so文件。
6.在模块的build.gradle中设置so文件路径
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的开发,需要准备以下内容:
-
Android Studio 2.2以上。
-
NDK:这套工具集允许为 Android 使用 C 和 C++ 代码。
-
CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果只计划使用 ndk-build,则不需要此组件。
-
LLDB:一种调试程序,Android Studio 使用它来调试原生代码。
创建支持C++的项目
新建支持C++的项目
在新建项目时,勾上Include C++ 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++的项目目录
-
src/main/cpp
下存放的我们编写供JNI调用的C++源码。 -
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的配置
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 nulladd_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 ...}复制代码
签名描述
基础数据类型
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 [] :[ILong[][] : [[JObject[][][] : [[[Ljava/lang/Object复制代码
使用命令查看
javap -s复制代码
class文件存在于 build->intermediates->classes
下。
JNI常见用法
1、jni访问java非静态成员变量
-
1.使用
GetObjectClass
、FindClass
获取调用对象的类 -
2.使用
GetFieldID
获取字段的ID。这里需要传入字段类型的签名描述。 -
3.使用
GetIntField
、GetObjectField
等方法,获取字段的值。使用SetIntField
、SetObjectField
等方法,设置字段的值。
注意:即使字段是private
也照样可以正常访问。
extern "C"JNIEXPORT void JNICALLJava_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 //SetField 修改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 JNICALLJava_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 JNICALLJava_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, "", "(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引用全局变量
-
使用
NewGlobalRef
创建全局引用,使用NewLocalRef
创建局部引用。 -
局部引用,通过DeleteLocalRef手动释放对象;全局引用,通过DeleteGlobalRef手动释放对象。
-
引用不主动释放会导致内存泄漏。
7、jni异常处理
-
使用
ExceptionOccurred
进行异常的检测。注意,这里只能检测java异常。 -
使用
ExceptionClear
进行异常的清除。 -
使用
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 JNICALLJava_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、日志打印
#include//引用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发生异常"); //日志打印复制代码