侧边栏壁纸
博主头像
慢行的骑兵博主等级

贪多嚼不烂,欲速则不达

  • 累计撰写 37 篇文章
  • 累计创建 27 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Android NDK探索之旅(一)

慢行的骑兵
2025-06-30 / 0 评论 / 0 点赞 / 13 阅读 / 3,989 字

一.概念

1.JNI

  • JNI是属于Java生态的技术。
  • 在 Android 开发中,JNI(Java Native Interface) 的核心目标是实现 Java/Kotlin(后续统称Java层)与本地代码(如 C/C++)的交互。

2.NDK

  • NDK是Android的工具开发包。
  • NDK使我们能够在 Android 应用中使用 C 和 C++ 代码,扩展 Android 的能力边界。

3.NDK 与 JNI 的关系

  • JNI 是 NDK 的通信桥梁,负责 Java层 与 Native 代码的交互。

4.Native层方法参数

  • 通过Android studio新建一个最简单的NDK项目,我们查看.cpp文件,内部有两个参JNIEnv* env和jobject thiz,先对这两个参数类型进行讲解。

4.1.JNIEnv类型

  • JNIEnv类型实际上代表了Java环境,通过JNIEnv* 指针就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等。
  • JNIEnv线程相关性:每个线程中都有一个 JNIEnv 指针。JNIEnv只在其所在线程有效, 它不能在线程之间进行传递。
  • 在C++创建的子线程中获取JNIEnv,要通过调用JavaVM的AttachCurrentThread函数获得。在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。把这里当作规定理解就行了。
  JNIEnv类中有很多函数可以用,如下所示:
  ● NewObject:创建Java类中的对象。
  ● NewString:创建Java类中的String对象。
  ● New<Type>Array:创建类型为Type的数组对象。
  ● Get<Type>Field:获取类型为Type的字段。
  ● Set<Type>Field:设置类型为Type的字段的值。
  ● GetStatic<Type>Field:获取类型为Type的static的字段。
  ● SetStatic<Type>Field:设置类型为Type的static的字段的值。
  ● Call<Type>Method:调用返回类型为Type的方法。
  ● CallStatic<Type>Method:调用返回值类型为Type的static方法。
  更多的函数使用可以查看jni.h文件中的函数名称。

4.2.jobject类型

  • jobject:表示 Java 层调用该 Native 方法的 对象实例(相当于 Java 中的 this)。如果 Native 方法是 静态的,则 jobject 代表类的 Class 对象(jclass)。

5.JavaVM

  • JavaVM 是虚拟机在 JNI 层的代表。
  • 一个进程只有一个 JavaVM。
  • 所有的线程共用一个 JavaVM。

6.全局引用、弱全局引用、局部引用

  • 全局引用
    • 通过 NewGlobalRef 和 DeleteGlobalRef 方法创建和释放一个全局引用。
    • 全局引用能在多个线程中被使用,且不会被 GC 回收,只能手动释放。
  • 弱全局引用
    • 通过 NewWeakGlobalRef 和 DeleteWeakGlobalRef 创建和释放一个弱全局引用。
    • 弱全局引用类似于全局引用,唯一的区别是它不会阻止被 GC 回收。
  • 局部引用
    • 通过 NewLocalRef 和 DeleteLocalRef 方法创建和释放一个局部引用。
    • 局部引用只在创建它的 native 方法中有效,包括其调用的其它函数中有效。因此我们不能寄望于将一个局部引用直接保存在全局变量中下次使用(请使用全局引用实现该需求)。我们可以不用删除局部引用,它们会在 native 方法返回时全部自动释放,但是建议对于不再使用的局部引用手动释放,避免内存过度使用。

二.Java层跟Native之间的互相调用

  • 这里将借助google官方的ndk-sample中的hello-jniCallback从三个角度来分析Java层跟Native之间的互相调用。
    • Java层调用Native层
    • Native开辟线程
    • Native层调用Java层

2.1.Java层调用Native层

  • 在Java层定义Native方法,并在Java层进行调用
    Java层定义Native层方法并调用
  • Native方法的实现
    Native方法的实现
  • Native层代码的实现以及Java层调用Native层代码比较直观,这里就不做过多解释了。

2.2.Native开辟线程

  • 先了解一下大体流程,然后再顺着代码进行熟悉,就会比较清晰。
    • 初始化:保存 JavaVM,缓存必要的 jclass/jmethodID。
    • 创建线程:使用 pthread。
    • 绑定 JVM:在线程中调用 AttachCurrentThread()。
    • 跨线程通信:通过全局引用调用 Java 方法。
    • 资源释放:解绑线程、删除全局引用。

2.2.1.JNI_OnLoad函数

//动态库加载时自动调用的初始化函数。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {//JavaVM:Java 虚拟机指针,全局唯一,需保存供后续使用。
    JNIEnv *env;
    memset(&g_ctx, 0, sizeof(g_ctx));//作用:将全局结构体 g_ctx 内存清零,避免残留数据。

    g_ctx.javaVM = vm;//保存 JavaVM 引用;//注释1
    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
      return JNI_ERR;  // JNI version not supported.
    }

    //jclass FindClass(const char* clsName):通过类的名称(类的全名,这时候包名不是用点号而是用/来区分的)来获取jclass。
    //如:jclass str = env->FindClass("java/lang/String");获取Java中的String对象的class对象。
    jclass clz =
        (*env)->FindClass(env, "com/example/hellojnicallback/JniHandler");
    g_ctx.jniHandlerClz = (*env)->NewGlobalRef(env, clz);

    jmethodID jniHandlerCtor =
        (*env)->GetMethodID(env, g_ctx.jniHandlerClz, "<init>", "()V");
    //NewObject:创建java类的对象
    jobject handler = (*env)->NewObject(env, g_ctx.jniHandlerClz, jniHandlerCtor);
    g_ctx.jniHandlerObj = (*env)->NewGlobalRef(env, handler);
    queryRuntimeInfo(env, g_ctx.jniHandlerObj);

    g_ctx.done = 0;
    g_ctx.mainActivityObj = NULL;
    return JNI_VERSION_1_6;//返回 JNI 版本  若返回的版本低于 Java 端预期,会导致库加载失败。
}
  • JNI_OnLoad函数中用到了结构体,信息如下:
typedef struct tick_context {
    JavaVM *javaVM;
    jclass jniHandlerClz;
    jobject jniHandlerObj;
    jclass mainActivityClz;
    jobject mainActivityObj;
    pthread_mutex_t lock;
    int done;
} TickContext;
  • 我们暂时只需要关注注释1那里即可,保存 JavaVM 引用到g_ctx.javaVM中;
    • 保存 JavaVM(Java 虚拟机指针)是 Native 层多线程与 Java 交互的基础。后续要通过保存的 JavaVM 绑定当前线程,然后通过JavaVM获取 JNIEnv,从而调用 Java 方法。

2.2.2.创建线程:使用 pthread

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_CREATE_DETACHED(分离线程)。// 分离线程:线程结束后自动释放资源,无需调用 pthread_join 等待。
    pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);

    pthread_mutex_init(&g_ctx.lock, NULL);//初始化互斥锁 g_ctx.lock 第二个参数 NULL 表示使用默认锁属性。         用于保护共享数据(如 g_ctx.done 标志位)

    jclass clz = (*env)->GetObjectClass(env, instance);//jclass GetObjectClass(jobject instance):通过对象实例来获取jclass,相当于Java中的getClass方法.获取 instance(MainActivity)的类对象。
    //将局部引用 (clz 和 instance) 提升为全局引用。
      // 必要性:局部引用在函数返回后失效,全局引用可跨线程和跨函数使用。
      // 需手动释放:在 StopTicks 中调用 DeleteGlobalRef。
    g_ctx.mainActivityClz = (*env)->NewGlobalRef(env, clz);//创建全局引用
    g_ctx.mainActivityObj = (*env)->NewGlobalRef(env, instance);//创建全局引用

    //创建了一个原生线程,该线程会执行 UpdateTicks 函数,线程属性设置为 PTHREAD_CREATE_DETACHED(分离线程,无需手动join)
     // 参数1:输出线程标识符 (threadInfo_)。
     // 参数2:线程属性(已设为分离线程)。
     // 参数3:线程入口函数 UpdateTicks。
     // 参数4:传递给线程函数的参数(&g_ctx,即上下文结构体指针)。
    int result = pthread_create(&threadInfo_, &threadAttr_, UpdateTicks, &g_ctx);//注释2
    assert(result == 0);//成功返回 0,失败返回错误码。

    pthread_attr_destroy(&threadAttr_);//销毁线程属性对象,释放资源。注意:此操作不影响已创建的线程。
    //(void)result; 的机制
    // 类型转换:将 result 强制转换为 void 类型(表示"无值")。
      // 副作用:该操作本身不产生任何实际效果,但向编译器表明:
      //"开发者明确知道这个变量未被使用,且这是有意为之。"
    (void)result;
}
  • 重点关注注释2:pthread_create函数,一共需要4个参数,参数1和参数2以及初始化互斥锁,销毁线程属性对象按照上面的代码进行套用,结构体的赋值操作根据实际需求来改就行。其中参数3是需要定义的函数。函数信息如下:
void *UpdateTicks(void *context) {
    TickContext *pctx = (TickContext *)context;
    JavaVM *javaVM = pctx->javaVM;

    // 每个线程必须调用 AttachCurrentThread 后才能使用 JNI 函数。
    JNIEnv *env;
    jint res = (*javaVM)->GetEnv(javaVM, (void **)&env, JNI_VERSION_1_6);//注释4 //检查当前线程是否已附加到 JVM,并获取 JNIEnv
    if (res != JNI_OK) {
      res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);//注释3 // 若未附加,则将当前线程附加到 JVM(获取 JNIEnv)。
      if (JNI_OK != res) {
        LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res);
        return NULL;
      }
    }

    //GetMethodID:获取 Java 方法的 ID(需类对象、方法名和签名)。
    jmethodID statusId = (*env)->GetMethodID(
        env, pctx->jniHandlerClz, "updateStatus", "(Ljava/lang/String;)V");//注释5
    sendJavaMsg(env, pctx->jniHandlerObj, 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->jniHandlerObj, statusId,
                "TickerThread status: start ticking ...");//注释6

    //2. 每秒调用Java的updateTimer()方法
    while (1) {
      gettimeofday(&beginTime, NULL);// 记录开始时间
      pthread_mutex_lock(&pctx->lock);//pthread_mutex_lock/unlock	保护共享变量 pctx->done(线程安全)。
      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->jniHandlerObj, statusId,
                    "TickerThread error: processing too long!");
      }
    }

    sendJavaMsg(env, pctx->jniHandlerObj, statusId,
                "TickerThread status: ticking stopped");
    // 4. 线程结束时分离JVM
    (*javaVM)->DetachCurrentThread(javaVM);//注释8
    return context;
}

2.2.3.绑定 JVM:在线程中调用 AttachCurrentThread()

  • 在UpdateTicks函数的注释3和注释4位置,将当前线程附加到 JavaVM并获取JNIEnv。
  • 主线程的 JNIEnv 是自动绑定的,但 Native 创建的线程必须手动绑定

2.2.4.跨线程通信:通过全局引用调用 Java 方法

  • 注释5位置,获取 Java 方法的 ID(需类对象、方法名和签名),对应方法为updateStatus。注释6位置,调用sendJavaMsg函数,该函数的第二个参数是在JNI_OnLoad函数中通过NewGlobalRef创建的一个全局引用。最终在sendJavaMsg函数中通过该全局引用来调用Java方法(对应注释7)
void sendJavaMsg(JNIEnv *env, jobject instance, jmethodID func,
                 const char *msg) {
    //NewStringUTF:将 C 风格的 UTF-8 字符串 (const char*) 转换为 Java 的 jstring 对象
    jstring javaMsg = (*env)->NewStringUTF(env, msg);
    // CallVoidMethod()	调用 Java 对象的实例方法,无返回值(对应 Java 中的 void 方法)
      // instance: 方法所属的 Java 对象。
      // func: 方法 ID(签名需匹配,此处应为 (Ljava/lang/String;)V)。
      // javaMsg: 参数(Java 字符串)
    (*env)->CallVoidMethod(env, instance, func, javaMsg);//注释7
    // 显式释放局部引用,避免局部引用表溢出(尤其在循环中重要)
    (*env)->DeleteLocalRef(env, javaMsg);
}

2.2.5.资源释放:解绑线程、删除全局引用

  • 解绑线程对应2.2.2中的UpdateTicks函数的注释8位置。删除全局引用需要使用DeleteGlobalRef函数,由于项目的业务逻辑因素,将删除全局引用定义到了StopTicks函数中(对应注释9和注释10),代码如下:
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);//注释9
  (*env)->DeleteGlobalRef(env, g_ctx.mainActivityObj);//注释10
  g_ctx.mainActivityObj = NULL;
  g_ctx.mainActivityClz = NULL;

  pthread_mutex_destroy(&g_ctx.lock);
}

2.3.Native层调用Java层

  • 在2.2中已经体现了Native层对Java层函数的调用,2.2.4中的注释7,通过JNIEnv调用函数CallVoidMethod。
void  (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    //参数说明
    //env: JNI 环境指针,提供访问 JNI 函数的接口
    //obj: 要调用方法的 Java 对象实例
    //methodID: 要调用的方法的 ID,通过 GetMethodID() 函数获得
    //...: 可变参数,是传递给 Java 方法的参数

三.静态注册与动态注册

3.1.静态注册

  • 在Java层定义的Native方法(如:stringFromJNI函数),直接通过Android Studio自动生成即可;
    • JNI函数名格式(需将” . ”改为” _ ”):Java_ + 包名(com.example.hellojnicallback)+ 类名(MainActivity) + 函数名(stringFromJNI)

3.2.动态注册

  • 利用结构体 JNINativeMethod 数组记录 java 方法与 JNI 函数的对应关系。
    • 结构体格式如下:
    typedef struct {
      const char* name;
      const char* signature;
      void* fnPtr;
    } JNINativeMethod;
    
    • 结构体的第一个参数 name 是java 方法名;第二个参数 signature 用于描述方法的参数与返回值;第三个参数 fnPtr 是函数指针,指向 jni 函数;
  • 调用 RegisterNatives 方法,传入 java 对象,以及 JNINativeMethod 数组,以及注册数目完成注册。
  • 当Java层通过System.loadLibrary加载JNI库时,会在库中查JNI_OnLoad函数。可将JNI_OnLoad视为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他一些初始化工作。
jstring stringFromJNI(JNIEnv *env, jobject thiz){
  std::string hello = "Hello from C++";
  return env->NewStringUTF(hello.c_str());
}

static const JNINativeMethod gMethods[] = {
	{"stringFromJNI", "()Ljava/lang/String;", (jstring*)stringFromJNI}
};

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = NULL;
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
    	return -1;
        
    jclass clazz = env->FindClass("com/example/hellojnicallback/MainActivity");
    
    if (!clazz){
    	return -1;
    }
    
    //RegisterNatives函数参数:
        //参数1:clazz	jclass	要注册本地方法的 Java 类
        //参数2:methods	JNINativeMethod*	包含方法名、签名和函数指针的结构体数组
        //参数3:nMethods	jint	要注册的方法数量
    if(env->RegisterNatives(clazz, gMethods,sizeof(gMethods)/sizeof(gMethods[0])))
    {
    	return -1;
    }
    return JNI_VERSION_1_4;
}
  • 优点:1. 流程更加清晰可控;2. 效率更高;

四.总结

  • 本文从整理的角度来讲解Android NDK。包含必要的概念了解,并将“典型的JNI多线程实现”案例进行剖析,展示了Java层调用Native层以及如何安全地从Native线程回调Java层。
  • 在细节方面:如Native方法中JNIEnv的相关api的使用以及CMakeLists.txt的语法等并未进行展开,这块的内容要在实际开发中逐步积累。
  • 对Android NDK有了全局的认识,细节方面的累计只是时间问题,当然Native的代码学习少不了C/C++的基础。
0

评论区