资料
正文
这个功能是点击按钮,然后在native 触发一个线程并回调界面中的方法实现UI刷新。那么涉及到的知识点就包含了:
- 创建一个非静态的native 函数。
- 通过jobject 反射activity中的某个方法。
- 创建一个用完就被销毁的native 线程。
- 可以在正确的时机关闭线程。
配置
scss
cmake_minimum_required(VERSION 3.4.1)
add_library(hello-jnicallback SHARED
hello-jnicallback.c)
target_link_libraries(hello-jnicallback
android
log
)
可以看到,这个项目使用的是C,同时so 文件叫 hello-jnicallback。同时还是需要cmake 进行打包编译。
源码
定义了一个不可变量:
arduino
static const char *kTAG = "hello-jniCallback";
- static 表示这个只在文件内可用
- const 表示这个值是不可变量,声明了就不可更改了。
- char* 表示字符串指针,指向了一个字符数组。
宏定义,定义函数:
scss
#define LOGI(...) \
((void)__android_log_print(ANDROID_LOG_INFO, kTAG, __VA_ARGS__))
#define LOGW(...) \
((void)__android_log_print(ANDROID_LOG_WARN, kTAG, __VA_ARGS__))
#define LOGE(...) \
((void)__android_log_print(ANDROID_LOG_ERROR, kTAG, __VA_ARGS__))
- #define 定义宏
- logi(...) 表示宏的名称,... 表示这个宏可以接受任意数量的参数。
- (void) 这个将确保不会产生任何返回值,因为__android_log_print 实际上返回了一个整数值,使用void 可以避免产生警告或错误。
定义结构体并定义别名:
ini
typedef struct tick_context {
JavaVM *javaVM;
jclass jniHelperClz;
jobject jniHelperObj;
jclass mainActivityClz;
jobject mainActivityObj;
pthread_mutex_t lock;
int done;
} TickContext;
定义文件内变量:
ini
TickContext g_ctx;
获取ABI类型
arduino
JNIEXPORT jstring JNICALL
Java_com_example_hellojnicallback_MainActivity_stringFromJNI(JNIEnv *env,
jobject thiz) {
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
#if defined(__ARM_NEON__)
#if defined(__ARM_PCS_VFP)
#define ABI "armeabi-v7a/NEON (hard-float)"
#else
#define ABI "armeabi-v7a/NEON"
#endif
#else
#if defined(__ARM_PCS_VFP)
#define ABI "armeabi-v7a (hard-float)"
#else
#define ABI "armeabi-v7a"
#endif
#endif
#else
#define ABI "armeabi"
#endif
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__x86_64__)
#define ABI "x86_64"
#elif defined(__mips64) /* mips64el-* toolchain defines __mips__ too */
#define ABI "mips64"
#elif defined(__mips__)
#define ABI "mips"
#elif defined(__aarch64__)
#define ABI "arm64-v8a"
#else
#define ABI "unknown"
#endif
return (*env)->NewStringUTF(env,
"Hello from JNI ! Compiled with ABI " ABI ".");
}
这段代码是一个用于确定目标平台ABI(应用程序二进制接口)的宏定义。它根据目标平台的特性定义了不同的ABI字符串。以下是对这段代码的详细解释:
#if defined(__arm__)
:检查是否定义了__arm__
宏,这通常表示目标平台是ARM架构。#if defined(__ARM_ARCH_7A__)
:在ARM架构下,进一步检查是否定义了__ARM_ARCH_7A__
宏,这通常表示目标平台是ARMv7-A架构。#if defined(__ARM_NEON__)
:在ARMv7-A架构下,进一步检查是否定义了__ARM_NEON__
宏,这表示目标平台支持NEON指令集。#if defined(__ARM_PCS_VFP)
:在ARMv7-A架构且支持NEON指令集的情况下,进一步检查是否定义了__ARM_PCS_VFP
宏,这表示目标平台支持浮点运算。
根据以上条件,定义了不同的ABI字符串:
- 如果目标平台是ARMv7-A架构,支持NEON指令集,且支持浮点运算,则ABI定义为
armeabi-v7a/NEON (hard-float)
。 - 如果目标平台是ARMv7-A架构,支持NEON指令集,但不支持浮点运算,则ABI定义为
armeabi-v7a/NEON
。 - 如果目标平台是ARMv7-A架构,不支持NEON指令集,但支持浮点运算,则ABI定义为
armeabi-v7a (hard-float)
。 - 如果目标平台是ARMv7-A架构,既不支持NEON指令集,也不支持浮点运算,则ABI定义为
armeabi-v7a
。
- 如果目标平台不是ARMv7-A架构,则根据其他条件定义其他的ABI字符串:
- 如果目标平台是x86架构,则ABI定义为
x86
。 - 如果目标平台是x86_64架构,则ABI定义为
x86_64
。 - 如果目标平台是MIPS64架构,则ABI定义为
mips64
。 - 如果目标平台是MIPS架构,则ABI定义为
mips
。 - 如果目标平台是ARM64-v8a架构,则ABI定义为
arm64-v8a
。
- 如果目标平台是x86架构,则ABI定义为
- 如果以上所有条件都不满足,则定义ABI为
unknown
。
这段代码的主要目的是为了确定目标平台的ABI,以便进行相应的编译和链接。
通过JNI_OnLoad进行初始化
JNI_OnLoad 的调用时机是当Java 调用这个玩意的时候触发。所以这个触发时机是非常早的,可以进行一些数据的初始化。
ini
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
memset(&g_ctx, 0, sizeof(g_ctx));
g_ctx.javaVM = vm;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // JNI version not supported.
}
jclass clz =
(*env)->FindClass(env, "com/example/hellojnicallback/JniHandler");
g_ctx.jniHelperClz = (*env)->NewGlobalRef(env, clz);
jmethodID jniHelperCtor =
(*env)->GetMethodID(env, g_ctx.jniHelperClz, "<init>", "()V");
jobject handler = (*env)->NewObject(env, g_ctx.jniHelperClz, jniHelperCtor);
g_ctx.jniHelperObj = (*env)->NewGlobalRef(env, handler);
queryRuntimeInfo(env, g_ctx.jniHelperObj);
g_ctx.done = 0;
g_ctx.mainActivityObj = NULL;
return JNI_VERSION_1_6;
}
- memset(&g_ctx, 0, sizeof(g_ctx)); 通过memset重置内存区域设置为特定的值,说白了就是申请内存。Java 对象通过创建对象去申请内存。
- 然后通过 (*vm)->GetEnv(vm,(void **)&env,JNI_VERSION_1_6) 获取到env 对象。如果获取不到,就返回错误。
- (*env)->ReleaseStringUTFChars(env, buildVersion, version); 释放本地字符串。
- (*env)->DeleteLocalRef(env, buildVersion) 用于释放本地为 Java 对象创建的引用。
创建线程并开始任务
需要使用pthread:
scss
#include <pthread.h>
JNIEXPORT void JNICALL
Java_com_example_hellojnicallback_MainActivity_startTicks(JNIEnv *env,
jobject instance) {
pthread_t threadInfo_;
pthread_attr_t threadAttr_;
pthread_attr_init(&threadAttr_);
pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);
pthread_mutex_init(&g_ctx.lock, NULL);
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;
}
-
pthread_attr_init 初始化线程属性对象。
-
pthread_attr_setdetachstate 设置线程属性未 分离状态,当创建线程后立即分离,他不会等待其他线程完成,而是执行完成后立即终止。
-
pthread_mutex_init 设置未互斥锁,防止多个线程同时访问和修改这些资源。
-
pthread_create 创建一个新的线程。
-
thread 一个指向线程标识符的指针,这个标识符由这个函数返回。
-
attr 线程的属性指针。
-
start_routine 一个函数指针,接受一个void* 产生,通常用作传递给线程的参数,并返回一个void. 而这里就是 UpdateTicks。
-
arg 传递给start_routine 的参数。
-
-
pthread_attr_destroy 销毁attr 对象。
-
assert 断言,当为false 的时候,就终止报错了。
线程调用的函数
scss
void *UpdateTicks(void *context) {
TickContext *pctx = (TickContext *) context;
JavaVM *javaVM = pctx->javaVM;
JNIEnv *env;
LOGE("添加到JVM中");
// 尝试查询,
jint res = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_6);
if (res != JNI_OK) {
// 把当前线程附加到JVM上。
res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
if (JNI_OK != res) {
LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res);
return NULL;
}
}
jmethodID statusId = (*env)->GetMethodID(
env, pctx->jniHelperClz, "updateStatus", "(Ljava/lang/String;)V");
sendJavaMsg(env, pctx->jniHelperObj, statusId,
"TickerThread status: initializing...");
// get mainActivity updateTimer function
jmethodID timerId =
(*env)->GetMethodID(env, pctx->mainActivityClz, "updateTimer", "()V");
struct timeval beginTime, curTime, usedTime, leftTime;
const struct timeval kOneSecond = {(__kernel_time_t) 1,
(__kernel_suseconds_t) 0};
// 发送一次消息
sendJavaMsg(env, pctx->jniHelperObj, statusId,
"TickerThread status: start ticking ...");
// 开始死循环
while (1) {
LOGE("---");
gettimeofday(&beginTime, NULL);
// 锁定
pthread_mutex_lock(&pctx->lock);
int done = pctx->done;
if (pctx->done) {
pctx->done = 0;
}
// 解锁
pthread_mutex_unlock(&pctx->lock);
if (done) {
// 跳出循环
break;
}
(*env)->CallVoidMethod(env, pctx->mainActivityObj, timerId);
gettimeofday(&curTime, NULL);
timersub(&curTime, &beginTime, &usedTime);
timersub(&kOneSecond, &usedTime, &leftTime);
struct timespec sleepTime;
sleepTime.tv_sec = leftTime.tv_sec;
sleepTime.tv_nsec = leftTime.tv_usec * 1000;
if (sleepTime.tv_sec <= 1) {
nanosleep(&sleepTime, NULL);
} else {
sendJavaMsg(env, pctx->jniHelperObj, statusId,
"TickerThread error: processing too long!");
}
}
sendJavaMsg(env, pctx->jniHelperObj, statusId,
"TickerThread status: ticking stopped");
// 将线程 从Jvm中移除
(*javaVM)->DetachCurrentThread(javaVM);
LOGE("将线程 从Jvm中移除 ");
return context;
}
- TickContext *pctx = (TickContext *) context; 结合上面的pthread_create,最后一个参数是给这个函数的入参,所以这个context 其实是这个对象。
- (*javaVM)->AttachCurrentThread(javaVM, &env, NULL) 将当前线程附加到JVM上。
- struct timeval beginTime, curTime, usedTime, leftTime; 声明timeval的结构体。
- gettimeofday 获取当前时间。
- pthread_mutex_lock 锁定当前线程
- pthread_mutex_unlock 解锁当前线程
- timersub 计算两个值,并赋予差值,最后一个参数就是结果。
- (*javaVM)->DetachCurrentThread(javaVM) 将数据从JVM中移除。
发送消息
scss
void sendJavaMsg(JNIEnv *env, jobject instance, jmethodID func,
const char *msg) {
jstring javaMsg = (*env)->NewStringUTF(env, msg);
(*env)->CallVoidMethod(env, instance, func, javaMsg);
(*env)->DeleteLocalRef(env, javaMsg);
}
- (*env)->NewStringUTF(env, msg) 创建一个Java 对象
- (*env)->CallVoidMethod(env, instance, func, javaMsg) 调用Java 函数
- (*env)->DeleteLocalRef(env, javaMsg) 删除创建的对象
发送消息到Java 层,然后GC。
终止线程
scss
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);
// waiting for ticking thread to flip the done flag
struct timespec sleepTime;
memset(&sleepTime, 0, sizeof(sleepTime));
sleepTime.tv_nsec = 100000000;
while (g_ctx.done) {
nanosleep(&sleepTime, NULL);
}
// release object we allocated from StartTicks() function
(*env)->DeleteGlobalRef(env, g_ctx.mainActivityClz);
(*env)->DeleteGlobalRef(env, g_ctx.mainActivityObj);
g_ctx.mainActivityObj = NULL;
g_ctx.mainActivityClz = NULL;
// 释放互斥锁
pthread_mutex_destroy(&g_ctx.lock);
}
- pthread_mutex_lock 加锁
- pthread_mutex_unlock 解锁
- memset 重置申请内存
总结
思路是创建一个线程,然后通过nanosleep 对线程进行暂停,同时创建线程的时候,在里面开启一个死循环,当需要关闭的时候,通过不停的暂停,然后去关闭死循环。这里就和Java 有很多不一样的了,C是直接面向内存,不存在Java直接创建的对象的说法,Java 创建对象后申请内存,并重置内存,但是C需要调用memset才行,如果不调用,就可能出现没有被赋值的对象出现数据错乱的问题。感觉C才是一切皆文件的最好诠释,Java 不面向内存开发,感觉不出来这种感觉。这里面的代码也包含了申请了的内存的手动回收,就很符合我对C的刻板印象。