【TopDesk】3.1.2. 利用JNI在Java中检测耳机插拔

以前折腾视频编码的时候总是感叹,为什么这帮人拿着这么高的工资写出来的标准却这么乱七八糟的,直到现在才知道:任何一个领域,当你深入到他最底层的部分时,他一定是杂乱无章而充斥着垃圾的。上层的干净整洁相貌堂堂,都是靠这这些七扭八歪的柱子撑起来的。

0x02 实践是检验真理的唯一标准

不知道我的读者中有多少是写Java的,也不知道有多少是写过JNI的,无论如何,一个简单的介绍大概算不上什么坏事。

JNI,全称Java Native Interface,即Java本地接口。它处于JVM并列的位置上,为Java程序与本地程序互相协作(也就是windows上的dll/linux上的so/mac上的dylib文件,学名动态链接库)提供双向的接口——在c++函数里面调用Java方法,或者在Java类方法里面调用dll提供的函数。

当然JNI的主要目的是用其他语言来服务Java程序,Java处于主导地位,原因有二:

  1. 本地程序只能动态加载,程序入口在Java程序中。只提供了在Java程序中加载一个库的方法,没有提供在c++程序中启动一个JVM的方法。(待查证)
  2. 双方之间传递数据必须使用JNI提供的Java类型,传统的指针/字面量必须转换为Java类型才能传递,这一点在JNI提供的各种函数的签名中也有体现。

这就导致了,在Java中声明要调用一个外部的函数非常简单——在方法签名中加上native关键字,并且在程序入口处加载一下动态链接库即可。此后Java文件也可直接通过编译,甚至打包成为JAR文件都与正常流程完全相同,只有在运行时执行到了本地方法却没有加载相应的库时会报错。

与之相反地,JNI的本地库一侧的编程则相对麻烦得多——首先要编译Java代码到字节码的.class文件,然后利用JDK中的javah命令生成对应改Java类的c/c++头文件,在编写完成c/c++实现之后需要编译生成动态链接库文件(dll/so/dyilb),最后在启动JVM的命令行中把动态链接库的幕加入java.library.path系统变量中才算真正完成。

其中光是编写c/c++实现这一点就极为复杂:比如一个本地方法

public native String foo(String str);

实现把一个字符串倒序的功能(当然大脑正常的人多半都会选择直接用Java实现),除了正常的操作char *以外,还得加上多出来的两步——jstring转换到char *再转回来。
要是程序中用到了win32 API则更加麻烦,众所周知,windows中为了兼容unicode所使用的字符串格式不是单字节的char *而是双字节的LPWSTR,或者wchar_t *

当然这篇文章并不是为了介绍JNI的,按照标题重点应该在“利用JNI”上,也自然不打算系统性地讲解JNI编程,有兴趣的道友可以自行百度。

之所以一上来就扯了一千来字,是为了抒发在实现这个功能的过程中内心积蓄的苦闷,再结合开篇的那段话——上层的干净整洁相貌堂堂,都是靠这这些七扭八歪的柱子撑起来的——而我们现在就要钻进这堆烂柱子中一探究竟。

0x03 建设有中国特色的社会主义

前篇的最后我们写出了一个能够监听到耳机插拔事件的c++ Demo,下一步任务自然便是把他变成Java可以使用的代码。

注:本节按照当初的开发步骤,弯路走了不少,懒得看的可以直接拉到最后成品。

前面提到利用IMMNotificationClient中OnPropertyValueChanged回调函数可以监听到耳机插拔的事件,然而只能通过终端设备的名称以“Internal”还是“External”开头来判断,还得过滤掉名称为空的事件,这显然不能直接拿来用。
所幸不管是Internal还是External的事件都是一连串地来的,因此我们可以这样设计逻辑:程序中维护一个当前插拔状态的变量,当新的事件到来时更新这个变量,Internal开头的赋false(已拔出),External开头的赋true(已插入),都不是的不改变,然后只有当这个值真正改变(修改前后不同)时才通知拔出/插入。

由于JNI程序调试起来很不方便,这个逻辑也打算放在Java里面来实现,直接上代码:

package io.github.std4453.topdesk.headphone;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 *
 */
public class HeadphonePeer {
   
    private List<HeadphoneEventListener> listeners ;
    private boolean inserted ;
    private ExecutorService executor;

    public HeadphonePeer() throws Exception {
        this.inserted = false;

        this.listeners = new ArrayList<>();
        this.executor = Executors.newSingleThreadExecutor();

        this.startListening();
    }

    public void addListener(HeadphoneEventListener listener) {
        this.listeners.add(listener);
    }

    public void onEvent(String name) {
        System.out.println(name);
        if (name.startsWith("External")) this.onInsert();
        else if (name.startsWith("Internal")) this.onRemove();
    }

    public synchronized void onInsert() {
        if (!this.inserted) {
            this.inserted = true;
            this.executor.submit(() -> this.listeners.forEach
                    (HeadphoneEventListener::onHeadphoneInserted));
        }
    }

    private synchronized void onRemove() {
        if (this.inserted) {
            this.inserted = false;
            this.executor.submit(() -> this.listeners.forEach
                    (HeadphoneEventListener::onHeadphoneRemoved));
        }
    }

    private void startListening() throws Exception {
        String msg = nStartListening();
        if (msg != null) throw new Exception(msg);
    }

    void stopListening() throws Exception {
        this.nStopListening();
    }

    private native String nStartListening();

    private native void nStopListening();
}

注:onEvent()方法用于c++部分回调,传递的参数就是前面提到的终端设备名称。stopListening()方法是为了显式析构监听器,防止内存泄漏,因为Java的finalize()函数并不保证一定调用。nStartListening()返回null以表示成功,否则返回错误信息。

package io.github.std4453.topdesk.headphone;

/**
 *
 */
public interface HeadphoneEventListener {
   
    void onHeadphoneInserted();
    void onHeadphoneRemoved();
}
package io.github.std4453.topdesk.headphone;

/**
 *
 */
public class HeadphoneTest {
   
    public static void main(String[] args) throws Exception{
        System.loadLibrary("topdesk");

        HeadphonePeer peer = new HeadphonePeer();
        peer.a
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值