NDK(02)从hello-jniCallback Demo中学到了什么?

资料

正文

这个功能是点击按钮,然后在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字符串。以下是对这段代码的详细解释:

  1. #if defined(__arm__):检查是否定义了__arm__宏,这通常表示目标平台是ARM架构。
  2. #if defined(__ARM_ARCH_7A__):在ARM架构下,进一步检查是否定义了__ARM_ARCH_7A__宏,这通常表示目标平台是ARMv7-A架构。
  3. #if defined(__ARM_NEON__):在ARMv7-A架构下,进一步检查是否定义了__ARM_NEON__宏,这表示目标平台支持NEON指令集。
  4. #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
  1. 如果目标平台不是ARMv7-A架构,则根据其他条件定义其他的ABI字符串:
    • 如果目标平台是x86架构,则ABI定义为x86
    • 如果目标平台是x86_64架构,则ABI定义为x86_64
    • 如果目标平台是MIPS64架构,则ABI定义为mips64
    • 如果目标平台是MIPS架构,则ABI定义为mips
    • 如果目标平台是ARM64-v8a架构,则ABI定义为arm64-v8a
  2. 如果以上所有条件都不满足,则定义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的刻板印象。

相关推荐
小白也想学C5 分钟前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程12 分钟前
初级数据结构——树
android·java·数据结构
闲暇部落2 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX4 小时前
Android 分区相关介绍
android
大白要努力!5 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee5 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood6 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-9 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen11 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年18 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin