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的刻板印象。

相关推荐
雨白10 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk10 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING11 小时前
RN容器启动优化实践
android·react native
恋猫de小郭13 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker19 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴19 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos