【NDK】Java和Native相互调用的“线程切换”

本文探讨了Java调用Native和Native调用Java时线程切换的细节,重点在于 AttachCurrentThread和DetachCurrentThread操作对线程的影响。作者提供了实验验证和场景分析,以及在项目中优化反射和通信的方法,包括队列和简单版Looper的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目的

在优化CPU占用的时候发现的问题,本文分析Native调用Java和Java调用Native方法的线程切换情况,为后续有开发NDK的人指路,便于规划边界和设计程序。

Java => Native

JNI方法是不会切换线程的,可以通过打印线程id验证(不是Thread.currentThread().id)得出结论:
【Java和Native打印线程ID】

Java中打印:android.os.Process.myTid()
C++中打印:syscall(SYS_gettid)

android.os.Process.myTid()是获取在操作系统级别的线程id,Thread.currentThread().id是Java级别的id。

结果如下:
jni_thread

Native => Java

实现:Native调用Java需要通过JNIEnv,然后调用CallxxxMethod。

AttachCurrentThread和DetachCurrentThread注意点:

1. native线程里需要调用JNIEnv里的方法,需要先attachCurrentThread
2. 在退出native线程前,需要先DetachCurrentThread,不然会导致崩溃
3. DetachCurrentThread不能重复调用,需要先判断当前不为JNI_EDETACHED的

先说结论吧,native线程里,AttachCurrentThread和DetachCurrentThread,是不会切线程的(如果能,那不是支持协程了,C++没那么高级)。而在AttachCurrentThread后,会给当前线程attach过来一个新的JNIEnv,这个线程是新的,所以调用CallxxxMethod之后,发现在Java里的打印线程是不一样的。
cpp2java
C++部分打印的是操作系统的线程id,对应在Java部分也是一致的,说明操作系统级别是没有切线程的,但打印java层线程id,发现是有变化的,说明JNIEnv绑定了一个JVM级别的线程。

总结:attach/detachThread会导致JVM线程切换,但操作系统级别的线程并没有

发散:熟悉kotlin协程的可能知道,kt协程的suspend/resume本质也并没有切换线程,越高级的语言控制粒度越细(越卷)。

场景分析

因为投屏项目里,比如告警、监控、鼠标流等有很多消息需要从Native层发送到Java层,所以这一层反射是绕不过的。如何优化?

  1. 存储class,存储methodId,发现没什么效果(毕竟高频反射JIT也会缓存优化,调用成本和普通方法所差无几)
  2. 单线程发起反射(后来发现优化程度也有限,但好歹少了8%,JVM线程新建也总是有损耗的)
    –> 和表现也比较相符,不然一致new操作系统的线程,像鼠标左边的消息如此高频,早就挂掉了。

优化历程

首先是设计了一个生产者-消费者队列,在开启的时候AttachCurrentThread,退出的时候DetachCurrentThread,如此便可以在同一个Java线程发送数据。但这个设计有个缺点(其实是写的不是很完美),有个30ms延时,一般的告警信息问题还不大,像鼠标之类的实时数据就不能这样处理了。

后来基于C++的信号量机制,实现了一个简单版的Android的Looper,暂时没发现问题,其基本实现如下图所示:
信号量Looper
考虑到我们现有的需求,并不需要跨进程通信的能力,故信号量机制会更轻量,无需使用epoll_wait和epoll_ctl来实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值