现有项目添加C/C++源码
如果现有项目还不支持C/C++,则可以通过如下步骤来加入C/C++
然后按照步骤操作即可。添加完成后打开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 ) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
|
通过JNIEXPORT
和JNICALL
两个宏定义,虚拟机在加载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()); }
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 };
|
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); if (env == NULL) { ALOGD("Jni", "[rjy] env is null"); return NULL; } jclass mapClz = env->FindClass("java/util/Map"); ALOGD("Jni", "[rjy] mapClz %s\n", mapClz == NULL? "is null" : "not null"); jclass appClz = env->FindClass("me/rjy/android/demo/MainActivity"); 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