以前折腾视频编码的时候总是感叹,为什么这帮人拿着这么高的工资写出来的标准却这么乱七八糟的,直到现在才知道:任何一个领域,当你深入到他最底层的部分时,他一定是杂乱无章而充斥着垃圾的。上层的干净整洁相貌堂堂,都是靠这这些七扭八歪的柱子撑起来的。
0x02 实践是检验真理的唯一标准
不知道我的读者中有多少是写Java的,也不知道有多少是写过JNI的,无论如何,一个简单的介绍大概算不上什么坏事。
JNI,全称Java Native Interface,即Java本地接口。它处于JVM并列的位置上,为Java程序与本地程序互相协作(也就是windows上的dll/linux上的so/mac上的dylib文件,学名动态链接库)提供双向的接口——在c++函数里面调用Java方法,或者在Java类方法里面调用dll提供的函数。
当然JNI的主要目的是用其他语言来服务Java程序,Java处于主导地位,原因有二:
- 本地程序只能动态加载,程序入口在Java程序中。只提供了在Java程序中加载一个库的方法,没有提供在c++程序中启动一个JVM的方法。(待查证)
- 双方之间传递数据必须使用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