概述
本文介绍如何在 Android Studio 环境下进行 JNI 开发。
相关文档:
Android Developer:向您的项目添加 C 和 C++ 代码
Android Developer:ndk-build
Android Developer:CMake
Android Developer:JNI 提示
准备工作
首先要在 AS 的 Project Structure 中配置 Android NDK Location。
开发
Java 类
首先创建一个声明 native 方法的 Java 类:
1 | public class JniUtils { |
rebuild 一下工程,然后就可以在 JniUtils
类所在的 Module 的 build/intermediates/classes/debug/<包名>
下面找到 JniUtils.class 文件。
生成头文件
进入 build/intermediates/classes/debug
目录,执行:
1 | javah -jni com.example.heqiang.testsomething.util.JniUtils |
就会在当前目录生成 com_example_heqiang_testsomething_util_JniUtils.h 头文件。当然这个头文件的文件名你也可以自定义成其他。
另外,这个头文件也不是必须的,可以生成,也可以省去。
JNI 开发
在 src/main 路径下新建一个名为 jni 的文件夹,再将前面生成的头文件放到该目录下面。
然后可以在目前下面创建 native 文件,名字可以随意定。
1 | #include "com_example_heqiang_testsomething_util_JniUtils.h" |
这介绍一个自动生成 jni 方法的窍门,光标定位到 Java 的native 方法上面,然后按快捷键,会有 create funtion XXXX
选项,选择后会在 c 文件里面自动生成对应的 jni 方法。
编译
AS 自动编译
支持两种方式:
- ndk-build + Android.mk + Application.mk
- CMake + CMakeLists.txt
使用 ndk-build
首先配置 build.gradle :
1 | android { |
这种方法会把 cpp 文件添加到当前 Android Studio 中,方便我们阅读jni代码。
触发编译,这个时候可以会报错:
1 | Error:Execution failed for task ':app:compileDebugNdk'. |
在 gradle.properties 文件中配置:
1 | android.useDeprecatedNdk=true |
使用 cmake
1 | // CMakeLists.txt |
配置gradle:
1 | android { |
在 app/build/intermediates/cmake/ 目录下生成so。
手动编译
ndk-build
上面的方法容易受到 AS 版本的影响,下面来介绍创建 Android.mk 文件的方法来生成so。
其实在上面的方法中在 build/intermediates/ndk/debug 路径下也有生成 Android.mk 文件。
首先我们在 jni 目录下面创建 Application.mk 文件:
1 | APP_ABI := armeabi,armeabi-v7a,arm64-v8a |
创建 Android.mk:
1 | LOCAL_PATH := $(call my-dir) |
在 jni 目录下面运行:
1 | ndk-build |
这样会在 src/main/libs
下面生成 so 文件。
然后在 build.gradle 中配置:
1 | sourceSets.main { |
cmake
加载so
在 JniUtils 类中添加下面代码记载so:
1 | public class JniUtils { |
然后就可以通过 JniUtils.getString()
c 和 c++ 开发的异同
使用 c++ 开发 jni 和使用 c 语言还是有些差别的。
1.文件后缀改成 cpp
2.在 jni.h 文件中有两套代码。一套是支持c的, 一套是支持c++的。
比如,c++ 中 getString 方法要写成:
1 | JNIEXPORT jstring JNICALL Java_com_example_heqiang_testsomething_util_JniUtils_getString(JNIEnv *env, jobject obj) { |
3.在方法上面加上 extern “C”
1 | extern "C" |
native 调用 Java
前面我们讲了 Java 调用 c/c++,接下来讲一下如果用 c/c++ 调用 Java 方法。
值得注意的是,在 native 的主线程和子线程调用 Java 的方法是不一样的。
主线程调用
要想通过 native 调用 Java,就需要有个 Java 的对象,获取这个对象也有两个方法:
- Java 层调用 native 方法时,会有个对象 jobject 参数,这个就是 Java 的当前对象。
- 通过C/C++创建java对象
先来看第一种方法:
1 | // JniUtils.java |
1 | //Constants.c |
再来看第二种方法:
创建一个 Java 类:
1 | package com.example.heqiang.testsomething.util; |
1 | //Constants.c |
子线程调用
JavaVM是属于Java进程的,每个进程只有一个JavaVM,而这个JavaVM可以被多线程共享,但是JNIEnv和jobject是属于线程私有的,不能共享。
要在子线程函数里使用AttachCurrentThread()和DetachCurrentThread()这两个函数,在这两个函数之间加入回调java方法所需要的代码。AttachCurrentThread方法用来获取到当前线程中的JNIEnv指针。
1 | JavaVM* javaVM = NULL; |
使用 RegisterNatives 注册本地方法
前面在jni中写本地方法时,我们使用了 Java_classpath_className_nativeMethodName 的形式,比如 Java_com_example_heqiang_testsomething_jni_JniUtils_getString,函数名很长,而且当类名变了的时候,函数名必须一个一个的改,挺麻烦的。
实际上 jvm 也同时提供了直接RegisterNative方法手动的注册native方法,下面我们就把上面的 getString 方法的实现方式修改一下。
1 | JNIEXPORT jstring JNICALL getString(JNIEnv *env, jobject obj) { |
小技巧
获取签名参数
进入 class 文件所在的目录,然后运行: javap -s -p JniUtils.class
1 | Compiled from "JniUtils.java" |
可以得到每个方法的签名参数。
签名对照表:
Java 类型 | 符号 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | L |
float | F |
double | D |
void | V |
object 对象 | LClassName;L类名 |
Arrays | [array-type[数组类型 |
methods方法 | (argument-types)return-type(参数类型)返回类型 |
类型转换
jstring -> char* :
1 | jstring r2 = static_cast<jstring>(env->CallObjectMethod(pObj, get2)); |
其他
jni 里面没有 CallStringMethod 方法,我们可以调用 CallObjectMethod 方法,然后对返回值进行强制转换为 jstring 即可。