Android Jni开发指南

现有项目添加C/C++源码

如果现有项目还不支持C/C++,则可以通过如下步骤来加入C/C++

Add C++ to Module

然后按照步骤操作即可。添加完成后打开app\src\main\cpp\CMakeLists.txt文件,修改项目名称

Java/Kotlin代码声明native函数

我们在me.rjy.android.demo.MainActivity中定义一个native函数,并加载native库。

Java代码:

1
2
3
4
5
6
7
public class MainActivity extends AppCompatActivity {
public native String stringFromJNI();

static {
System.loadLibrary("demo");
}
}

Kotlin代码:

1
2
3
4
5
6
7
8
class MainActivity : ComponentActivity() {
external fun stringFromJNI(): String

companion object {
init {
System.loadLibrary("demo")
}
}

定义jni函数

当Java代码中执行native函数时,虚拟机需要找到so库中相对应的函数符号。因此,我们在定义jni函数时,需要将函数注册到虚拟机中。

静态注册

静态注册会严格限制jni函数的命名。jni函数命名规则:Java_包名_类名_方法名,其中包名中的.要替换为下滑杠_。如果函数名称不符合规范,在运行时就会抛出java.lang.UnsatisfiedLinkError: No implementation found for ...的异常。

因为stringFromJNI函数在me.rjy.android.demo.MainActivity进行声明,所以定义的native函数如下:

1
2
3
4
5
6
7
8
9
10
#include <jni.h>
#include <string>

extern "C" JNIEXPORT
jstring JNICALL Java_me_rjy_android_demo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

通过JNIEXPORTJNICALL两个宏定义,虚拟机在加载so库时,就会把该函数与java代码的native函数进行绑定。

静态注册的优点时代码量很少,缺点是要严格遵守命名规范,而且函数命名很长。

动态注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <jni.h>
#include <string>

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

static jstring stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

//可以同时在数组中注册多个jni函数
static const JNINativeMethod sMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
};

static int registerNativeMethods(JNIEnv *env, const char *className,
const JNINativeMethod *gMethods, int numMethods) {
jclass clazz = env->FindClass(className);

if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}

jint JNI_OnLoad(JavaVM *jvm, void *) {
JNIEnv *env = NULL;
if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6)) {
return JNI_ERR;
}

if (registerNativeMethods(env, "me/rjy/android/demo/MainActivity",
sMethods, NELEM(sMethods)) == JNI_FALSE) {
return JNI_ERR;
}

return JNI_VERSION_1_6;
}

动态注册的代码要比静态注册多很多,这是动态注册的缺点。优点是灵活,比如根据条件注册不同的实现。动态注册的一个关键点是函数的签名,这个非常容易出错,有一个直观的方法可以直接拿到函数签名:通过APK 反编译介绍的方法,把apk中的类反编译为smali,然后找到对应的MainActivity.smali,smali文件中就可以找到对应的函数定义:

1
2
.method public final native stringFromJNI()Ljava/lang/String;
.end method

其中()Ljava/lang/String;就是函数签名,()表示无参函数,Ljava/lang/String;表示返回一个String类型。也可以在AndroidStudio的 Build > Analyze APK 功能打开apk,然后找到native函数定义的java类,右击选择“Show Bytecode”就可以看到类的字节码文件,在字节码文件中就能找到native函数的定义和签名。

JavaVM

JavaVM是虚拟机对外提供能力的入口。一个JVM只有一个JavaVM对象,这个对象是线程共享的。理论上,每个进程可以有多个 JavaVM,但 Android 只允许有一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _JavaVM {
const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

JNIEnv

JNIEnv 提供了大部分 JNI 函数。您的原生函数都会收到 JNIEnv 作为第一个参数。JNIEnv是线程独享的。如果一段代码无法通过其他方法获取自己的 JNIEnv,您应该共享相应 JavaVM,然后使用 GetEnv 发现线程的 JNIEnv,如下:

1
2
JNIEnv *env = NULL;
gJavaVM->GetEnv((void **) &env, JNI_VERSION_1_6);

但是,如果是通过native的pthread_create()或者std::thread来启动线程,线程中的代码是不附带JNIEnv的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void* say_hello(void* args)
{
JNIEnv *env = NULL;
gJavaVM->GetEnv((void **) &env, JNI_VERSION_1_6);
if (env == NULL) {
logD("Jni", "env is null"); //会走到这里
return NULL;
}
return 0;
}

static void testNativeThread() {
pthread_t thread;
int ret = pthread_create(&thread, NULL, say_hello, NULL);
pthread_join(thread, NULL);
}

上述代码获取的env是空,所以需要通过AttachCurrentThread()来进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void* say_hello(void* args)
{
JNIEnv *env = NULL;
gJavaVM->AttachCurrentThread(&env, NULL); //获取JNIEnv
if (env == NULL) {
ALOGD("Jni", "[rjy] env is null");
return NULL;
}
jclass mapClz = env->FindClass("java/util/Map"); //可以获取到jdk中的class
ALOGD("Jni", "[rjy] mapClz %s\n", mapClz == NULL? "is null" : "not null");
jclass appClz = env->FindClass("me/rjy/android/demo/MainActivity"); //获取不到app中定义的class
ALOGD("Jni", "[rjy] activity class %s", appClz == NULL? "is null" : "not null");
return 0;
}

static void testNativeThread() {
pthread_t thread;
int ret = pthread_create(&thread, NULL, say_hello, NULL);
pthread_join(thread, NULL);
}

上述代码可以获取到Map类,是因为native创建的线程对应的classLoader是bootclassloader。

通过 JNI 附加的线程在退出之前必须调用 DetachCurrentThread()。如果直接对此进行编码会很棘手,在 Android 2.0 (Eclair) 及更高版本中,您可以先使用 pthread_key_create() 定义将在线程退出之前调用的析构函数,之后再调用 DetachCurrentThread()。(将该键与 pthread_setspecific() 搭配使用,将 JNIEnv 存储在线程本地存储中;这样一来,该键将作为参数传递到您的析构函数中。)(参考:线程

Android JNI打印logcat日志

在c/c++文件中引入头文件:

1
#include <android/log.h>

然后在需要打印日志的地方通过__android_log_print来打印日志即可,函数定义如下:

1
int __android_log_print(int prio, const char* tag, const char* fmt, ...)

prio表示日志级别,常用的是这四个,其他的日志级别可以在头文件中找到

  • ANDROID_LOG_DEBUG
  • ANDROID_LOG_INFO
  • ANDROID_LOG_WARN
  • ANDROID_LOG_ERROR

示例:

1
__android_log_print(ANDROID_LOG_DEBUG, "tag", "value=%d", value);

可以对__android_log_print进行封装,在使用的时候会更加便捷:

1
2
3
#define ALOGD(tag, ...) __android_log_print(ANDROID_LOG_DEBUG, tag, __VA_ARGS__)
#define ALOGI(tag, ...) __android_log_print(ANDROID_LOG_INFO, tag, __VA_ARGS__)
#define ALOGE(tag, ...) __android_log_print(ANDROID_LOG_ERROR, tag, __VA_ARGS__)

使用方法:

1
ALOGD("JniTag", "%s: Hello World!", name);

代码分析

Jni相关类接口在libnativehelper\include_jni\jni.h头文件中定义。

参考文章

Android JNI 提示
Java Native Interface Specification Contents
Guide to JNI (Java Native Interface)
深入理解JNI