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

相关推荐
隐-梵3 小时前
Android studio学习之路(六)--真机的调试以及多媒体照相的使用
android·学习·android studio
stevenzqzq3 小时前
Android Studio Logcat V2 使用指南(适配 2024 年版本)
android·ide·android studio
bytebeats3 小时前
改进 Jetpack Compose 中的 ModalBottomSheet API
android
bytebeats3 小时前
使用Dagger SPI 查找非必要组件依赖项
android·gradle·dagger
bytebeats3 小时前
在Kotlin中编写依赖于时间的可测试协程代码
android·kotlin·测试
_一条咸鱼_3 小时前
AI 大模型之 Transformer 架构深入剖析
android
QING6183 小时前
Kotlin 中 reified 配合 inline 不再被类型擦除蒙蔽双眼
android·kotlin·app
Yang-Never4 小时前
OpenGL ES -> SurfaceView + EGL实现立方体纹理贴图+透视效果
android·kotlin·android studio·贴图
QING6184 小时前
Android应用启动与退出监听方案——新手入门指南
android·架构·app
叫我龙翔4 小时前
【项目日记】高并发服务器项目总结
android·运维·服务器