在进行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 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
模版如下:
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)
说明:
LOCAL_PATH := $(call my-dir)
:指向当前目录的地址,包含该.mk
include $(CLEAR_VARS)
:清理掉所有以LOCAL_开头的内容,这句话是必须的,因为如果所有的变量都是全局的,所有的可控的编译文件都需要在一个单独的GNU中被解析并执行。
LOCAL_MODULE
:调用的库名,用来区分android.mk中的每一个模块。文件名必须是唯一的,不能有空格。注意,这里编译器会为你自动加上一些前缀lib和后缀.so,来保证文件是一致的。
LOCAL_SRC_FILES
:变量必须包含一个C、C++或者java源文件的列表,这些会被编译并聚合到一个模块中,文件之间可以用空格或Tab键进行分割,换行请用"\"
LOCAL_LDLIBS
:定义需要链接的库。一般用于链接那些存在于系统目录下本模块需要链接的库(比如这里的logcat库)。
include $(BUILD_SHARED_LIBRARY)
:来生成一个动态库libnative-lib.so
3.编写Application.mk
# 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
说明:
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 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开发,因此需要修改一下参数进行兼容。
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 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 ... }
签名描述
基础数据类型
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
使用命令查看
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引用全局变量
- 使用
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 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、日志打印
#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发生异常"); //日志打印