NDK Samples目录:GoogleSamples - NDK Samples
项目地址:https://github.com/googlesamples/android-ndk/tree/master/hello-jniCallback
说明文档:https://github.com/googlesamples/android-ndk/blob/master/hello-jniCallback/README.md
该项目演示如何从C代码调用Java层方法。
最低要求:
- Android Studio 版本大于 2.2
该项目的演示的方法主要是:
JniHandler :: getBuildVersion()
演示C代码如何回调Java层的静态方法。JniHandler :: getRuntimeMemorySize()
演示C代码如何回调Java层的实例方法。JniHandler :: updateStatus(String msg)
演示C代码如何回调Java层带参方法。
代码主要分析 hello-jnicallback.c ,java层代码比较简单直接省略:
声明了结构体 TickContext,主要负责了:
1.缓存JavaVM、jniHelper/MainActiviy的类和实例
2.互斥锁lock
3.主要逻辑中的循环的执行状态 done。
对应的结构体变量为 g_ctx。
/*
声明结构体 tick_context
*/
typedef struct tick_context {
JavaVM *javaVM; // javaVM
jclass jniHelperClz; // JniHandler类,全局引用
jobject jniHelperObj; // JniHandler实例,全局引用
jclass mainActivityClz; // MainActivity类,全局引用
jobject mainActivityObj; // MainActivity实例,全局引用
pthread_mutex_t lock; // 一个互斥锁
int done; // 循环的执行状态,1表示已完成,退出循环
} TickContext;
/*
定义结构体变量 g_ctx
*/
TickContext g_ctx;
JNI_OnLoad在System.loadLibrary后被调用,相当于动态库的初始化方法:
1:缓存JavaVM
2:缓存JniHelper类的缓存,创建了一个JniHelper实例并缓存
3:调用queryRuntimeInfo方法
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {
// JniEnv
JNIEnv* env;
// 初始化结构体 g_ctx
memset(&g_ctx, 0, sizeof(g_ctx));
// 缓存 JavaVM
g_ctx.javaVM = vm;
// 检查是否支持 JNI_VERSION_1_6 版本, 不支持时返回JNI_ERR
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 查找JniHandler类,并缓存
jclass clz = (*env)->FindClass(env, "com/example/hellojnicallback/JniHandler");
g_ctx.jniHelperClz = (*env)->NewGlobalRef(env, clz);
// 创建一个JniHandler实例,并缓存
jmethodID jniHelperCtor = (*env)->GetMethodID(env, g_ctx.jniHelperClz,
"", "()V");
jobject handler = (*env)->NewObject(env, g_ctx.jniHelperClz, jniHelperCtor);
g_ctx.jniHelperObj = (*env)->NewGlobalRef(env, handler);
// 获取引用的运行信息
// 主要是 展示如何从 c代码调用java层 JniHelper 的方法。
queryRuntimeInfo(env, g_ctx.jniHelperObj);
// 初始化运行状态
g_ctx.done = 0;
g_ctx.mainActivityObj = NULL;
// 返回该动态库的JNI版本
return JNI_VERSION_1_6;
}
queryRuntimeInfo方法主要演示了如何从C层调用Java层方法,包括静态方法和实例方法。
注意:还演示如何处理资源的手动释放,以避免内存泄漏
以下代码分析忽略非空检测,移除非空检测代码块:
void queryRuntimeInfo(JNIEnv *env, jobject instance) {
// 查找 JniHelper 静态方法 getBuildVersion 的 ID
jmethodID versionFunc = (*env)->GetStaticMethodID(
env, g_ctx.jniHelperClz,
"getBuildVersion", "()Ljava/lang/String;");
// 调用静态方法 getBuildVersion,获取返回值
// !Java对象需要在使用结束后释放引用
jstring buildVersion = (*env)->CallStaticObjectMethod(
env, g_ctx.jniHelperClz, versionFunc);
// 将 jstring 转换成一个 UTF-8 的 C字符串
// !在不关注拷贝结果的情况下,请勿对 GetStringXXX 结果进行修改
// !GetXXX 返回值在使用后,需要调用对应的 ReleaseXXX,以释放对Java层对象的引用
// !参考:https://www.cnblogs.com/codc-5117/archive/2012/09/06/2672833.html
const char *version = (*env)->GetStringUTFChars(env, buildVersion, NULL);
// 打印信息
LOGI("Android Version - %s", version);
// 释放 GetStringUTFChars 产生的引用
(*env)->ReleaseStringUTFChars(env, buildVersion, version);
// 释放 CallStaticObjectMethod 返回的 jstring 的引用
(*env)->DeleteLocalRef(env, buildVersion);
// 查找 JniHelper 实例方法 getRuntimeMemorySize 的 ID
jmethodID memFunc = (*env)->GetMethodID(env, g_ctx.jniHelperClz,
"getRuntimeMemorySize", "()J");
// 调用实例方法 getRuntimeMemorySize,获取返回值
// !jlong等基本数据类型不需要手动释放
// !参考:https://blog.csdn.net/zhangguixian5/article/details/8490114
jlong result = (*env)->CallLongMethod(env, instance, memFunc);
// 打印信息
LOGI("Runtime free memory size: %" PRId64, result);
// 屏蔽编译器警告
(void)result;
}
在MainActivity的声明周期onResume中,会调用本地方法startTick:
1.缓存MainActivity类和实例
2.创建一个新线程,新线程中调用UpdateTicks完成实际逻辑
注意:还演示了C层如何创建一个新的线程:
JNIEXPORT void JNICALL
Java_com_example_hellojnicallback_MainActivity_startTicks(
JNIEnv *env, jobject instance) {
// 线程ID
pthread_t threadInfo_;
// 线程属性
pthread_attr_t threadAttr_;
// 初始化线程属性
pthread_attr_init(&threadAttr_);
// 设置为分离线程
// !参考:https://blog.csdn.net/inuyashaw/article/details/78904231
pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);
// 初始化互斥锁
pthread_mutex_init(&g_ctx.lock, NULL);
// 缓存MainActivity类和实例
jclass clz = (*env)->GetObjectClass(env, instance);
g_ctx.mainActivityClz = (*env)->NewGlobalRef(env, clz);
g_ctx.mainActivityObj = (*env)->NewGlobalRef(env, instance);
// 创建线程
int result = pthread_create( &threadInfo_, &threadAttr_, UpdateTicks, &g_ctx);
assert(result == 0);
// 销毁线程属相,释放内存
pthread_attr_destroy(&threadAttr_);
(void)result;
}
UpdateTicks 是一个死循环,主要逻辑为定时调用Java层 MainActivity::updateTimer 方法:
注意:还展示了C层时间函数的简单使用
以下代码分析忽略sendJavaMsg(主要为Java层日志),移除sendJavaMsg及相关的 statusId:
void* UpdateTicks(void* context) {
// TickContext 结构体变量
TickContext *pctx = (TickContext*) context;
// javaVM
JavaVM *javaVM = pctx->javaVM;
// 调用 GetEnv 获取当前线程 JniEnv
// 当前当前线程未附加到 VM 时,调用 AttachCurrentThread 将当前线程附加到 VM
// !文档:
// https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html#GetEnv
// https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html#attach_current_thread
JNIEnv *env;
jint res = (*javaVM)->GetEnv(javaVM, (void**)&env, JNI_VERSION_1_6);
if (res != JNI_OK) {
res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
if (JNI_OK != res) {
LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res);
return NULL;
}
}
// 获取 MainActivity 实例方法 updateTimer 的 ID
// updateTimer 的 Java层逻辑 十分简单:每调用一次,时间显示 +1s
jmethodID timerId = (*env)->GetMethodID(env, pctx->mainActivityClz, "updateTimer", "()V");
// 定义时间结构体 timeval 变量 beginTime, curTime, usedTime, leftTime
// !timeval 有两个成员,一个是秒,一个是微秒
struct timeval beginTime, curTime, usedTime, leftTime;
// 定义时间结构体 timeval 常量 kOneSecond
const struct timeval kOneSecond = {
(__kernel_time_t)1,
(__kernel_suseconds_t) 0
};
// 主要逻辑的死循环
while(1) {
// 记录开始时间
gettimeofday(&beginTime, NULL);
// 互斥锁加锁
pthread_mutex_lock(&pctx->lock);
// 获取当前的调用状态
int done = pctx->done;
// 还原结构体的调用状态为0
if (pctx->done) {
pctx->done = 0;
}
// 互斥锁解锁
pthread_mutex_unlock(&pctx->lock);
// 当前状态为1时,表示完成逻辑,退出主循环
if (done) {
break;
}
// 主逻辑开始
// 调用 MainActivity 静态方法 updateTimer (Java层的时间显示 +1s)
(*env)->CallVoidMethod(env, pctx->mainActivityObj, timerId);
// 记录结束时间
gettimeofday(&curTime, NULL);
// 计算调用 updateTimer 的耗时(结束时间 - 开始时间)
timersub(&curTime, &beginTime, &usedTime);
// 计算剩余需要等待的时间 (1s - 耗时)
timersub(&kOneSecond, &usedTime, &leftTime);
// 定义结构体 timespec 变量 sleepTime
// !timespec 有两个成员,一个是秒,一个是纳秒
struct timespec sleepTime;
// 把计算的剩余时间赋值到 sleepTime
sleepTime.tv_sec = leftTime.tv_sec;
sleepTime.tv_nsec = leftTime.tv_usec * 1000; // 微杪转纳秒 * 1000
// 等待时间的秒数等于1秒或少于1秒,调用 nanosleep 进行纳米级睡眠
// 等待时间大于1秒???啥情况
if (sleepTime.tv_sec <= 1) {
nanosleep(&sleepTime, NULL);
} else { /* Log */ }
}
// 把当前线程从 VM 中分离
// 不进行分离操作会导致线程无法退出引起资源泄漏
(*javaVM)->DetachCurrentThread(javaVM);
return context;
}
在MainActivity的声明周期onPause中,会调用本地方法StopTicks:
1.修改执行状态为 1 (表示已完成)
2.销毁startTicks中产生的全局引用和变量
(这里的StopTicks方法首字母是大写,可能是一个小坑吧,不影响)
JNIEXPORT void JNICALL
Java_com_example_hellojnicallback_MainActivity_StopTicks(JNIEnv *env, jobject instance) {
// 互斥锁加锁,确保执行状态的线程安全
pthread_mutex_lock(&g_ctx.lock);
// 执行状态设置为已完成,会导致逻辑线程退出循环
g_ctx.done = 1;
// 互斥锁解锁
pthread_mutex_unlock(&g_ctx.lock);
// 定义结构体 timespec 变量 sleepTime
struct timespec sleepTime;
// 初始化sleepTime,置为0
memset(&sleepTime, 0, sizeof(sleepTime));
// 设置sleepTime,时长等于0.1秒
sleepTime.tv_nsec = 100000000;
// 等待逻辑线程的循环结束
// 当逻辑线程执行时,会将上面的 状态1 设成 状态0,这里就是等待逻辑线程的执行
// 每次等待的时间为0.1秒
while (g_ctx.done) {
nanosleep(&sleepTime, NULL);
}
// 此处开始,逻辑线程已退出主循环
// 删除在 startTicks 中缓存的 MainActivity类和对象 的全局引用,避免内存泄漏
(*env)->DeleteGlobalRef(env, g_ctx.mainActivityClz);
(*env)->DeleteGlobalRef(env, g_ctx.mainActivityObj);
// 置空
g_ctx.mainActivityObj = NULL;
g_ctx.mainActivityClz = NULL;
// 销毁在 startTicks 中创建的互斥锁
pthread_mutex_destroy(&g_ctx.lock);
}
此次完成项目的主要部分的源码分析,共演示了:
- C层中调用Java层的静态/实例方法
- C层中创建线程并附加到VM
- 部分函数,如互斥锁,时间函数的简单使用
- 基本的资源手动释放处理,以避免内存泄漏