手把手教你如何在Android下进行JNI开发(入门)

5,394 阅读7分钟

在进行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的同级目录下生成libsobj文件夹,存放的是编译好的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类型描述
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/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类型签名描述
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
void

引用数据类型

(以L开头,以;结束,中间对应的是该类型的完整路径)

StringLjava/lang/String;
ObjectLjava/lang/Object;
自定义类型 AreaLcom/xuexiang/jnidemo/Area;

数组

(在类型前面添加[,几维数组就在前面添加几个[)

int [][ILong[][][[JObject[][][][[[Ljava/lang/Object

使用命令查看

javap -s <java类的class文件路径>

class文件存在于 build->intermediates->classes下。

JNI常见用法

1、jni访问java非静态成员变量

  • 1.使用GetObjectClassFindClass获取调用对象的类

  • 2.使用GetFieldID获取字段的ID。这里需要传入字段类型的签名描述。

  • 3.使用GetIntFieldGetObjectField等方法,获取字段的值。使用SetIntFieldSetObjectField等方法,设置字段的值。

注意:即使字段是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.使用GetObjectClassFindClass获取调用对象的类

  • 2.使用GetStaticFieldID获取字段的ID。这里需要传入字段类型的签名描述。

  • 3.使用GetStaticIntFieldGetStaticObjectField等方法,获取字段的值。使用SetStaticIntFieldSetStaticObjectField等方法,设置字段的值。

3、jni调用java非静态成员方法

  • 1.使用GetObjectClassFindClass获取调用对象的类

  • 2.使用GetMethodID获取方法的ID。这里需要传入方法的签名描述。

  • 3.使用CallVoidMethod执行无返回值的方法,使用CallIntMethodCallBooleanMethod等执行有返回值的方法。

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.使用GetObjectClassFindClass获取调用对象的类

  • 2.使用GetStaticMethodID获取方法的ID。这里需要传入方法的签名描述。

  • 3.使用CallStaticVoidMethod执行无返回值的方法,使用CallStaticIntMethodCallStaticBooleanMethod等执行有返回值的方法。

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来上抛异常。

注意,ExceptionOccurredExceptionClear一般是成对出现的,类似于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发生异常"); //日志打印

相关链接

微信公众号

更多资讯内容,欢迎微信搜索公众号:【我的Android开源之旅】