UsbRequest.queue崩溃Android 3.1应用程序
我正在开发一个Android 3.1应用程序,它使用USB主机模式通过USB通过MIDI与我的键盘(Korg M3)进行通信。 这是在安装了Android 4.0.3的Xoom上运行的。 我能够通过USB接收MIDI信息而没有任何问题,但将笔记数据发送回键盘取得了成功,并在半秒延迟后频繁崩溃。
这是我在操作栏上点击按钮发送注释时遇到的错误:
E / dalvikvm(6422):JNI ERROR(app bug):访问过时的全局引用0x1da0020a(大小为130的表中的索引130)
我检查/试图追查原因:
- 由于代码是multithreading的,我有Java
synchronized
块,包括对输出请求池,输入请求池(根据Android文档中的ADB示例 )的访问,以及当前输出请求和相关ByteBuffer
对象引用的自定义锁对象。 我已经构造了执行这些锁的代码,以最大限度地减少发生死锁的可能性。 - 从相关请求池中检索可用的
UsbRequest
对象时,我将clientData引用设置为新的ByteBuffer
对象,而不是重用先前关联的ByteBuffer
对象并在其上调用clear()
。 -
我已经在代码的关键点添加了大量的日志记录调用(对于logCat),以尝试跟踪其确切失败的位置。 我发现错误最终发生在以下几点(此代码在此之前可以正常运行几次):
public void sendMidiData() { synchronized(_outputLock) { if(_currentOutputRequest == null || _outputBuffer.position() < 2) return; Log.d(_tag, "Queuing Send request"); //// ERROR - happens in this next statement: _currentOutputRequest.queue(_outputBuffer, _maxPacketSize); Log.d(_tag, "Send request queued; resetting references..."); //Initialise for next packet _currentOutputRequest = null; //getAvailableSendRequest(); _outputBuffer = null; //(ByteBuffer)_currentOutputRequest.getClientData(); Log.d(_tag, "Output request & buffer references set."); } }
-
我还尝试将
_currentOutputRequest
和_outputBuffer
引用设置为null
以便仅在将下一个MIDI事件写入缓冲区时才检索可用请求。 如果null
被原始调用替换(如注释所示),则立即检索下一个可用请求。 这没有任何区别。
有什么想法导致这个问题吗? 这可能是Android中以前未被发现的错误吗? 对返回的错误的搜索不会带来太多影响; 主要参考NDK编程(我不做)。
干杯。
好的,我有一点突破,发现了两个问题:
- 调用
_currentOutputRequest.queue(_outputBuffer, _maxPacketSize);
正在传递整个缓冲区容量_maxPacketSize
,其常量值为64(字节)。 显然,这仅适用于批量读取,最多可读取64个字节; 批量发送请求需要指定发送的确切字节数。 -
_currentOutputRequest.queue()
和_connection.requestWait()
方法调用看起来不是线程安全的,特别是在UsbDeviceConnection
(它是_connection
的类型)的实现中。 我怀疑UsbRequest_currentOutputRequest
在排队发送请求时在内部使用UsbConnection对象。
我是如何解决的:
对queue()
的调用更改为:
_currentOutputRequest.queue(_outputBuffer, _outputBuffer.position());
对于第二个问题, queue()
语句已经使用_outputLock
对象发生在synchronized
块内。 在读者线程的Run()
方法中,我不得不使用requestWait()
在synchronized
块中包装对requestWait()
的调用:
UsbRequest request = null; synchronized(_outputLock) { // requestWait() and request.queue() appear not to be thread-safe. request = _connection.requestWait(); }
鉴于这发生在while
循环中并且requestWait
阻塞了线程,我发现的一个主要问题是在尝试对Send请求进行排队时锁会导致饥饿。 结果是,当应用程序及时接收并作用于传入的MIDI数据时,输出的MIDI事件显着延迟。
作为对此的部分修复,我在while
循环结束之前插入了一个yield
语句,以保持UI线程不被阻塞。 (UI线程暂时被用作音符事件由按钮按下触发;这最终将使用单独的播放线程。)因此它更好,但不完美,因为在第一个输出音符之前仍有相当长的延迟已发送。
更好的解决方案:
为了解决异步读取和写入的相互锁定的需求,异步queue()
和requestWait()
方法仅用于读取操作,这些操作保留在单独的“读取器”线程上。 因此,不需要synchronized
块,因此可以将此段简化为以下内容:
UsbRequest request = _connection.requestWait();
至于写/发送操作,其核心被移动到一个单独的线程来执行同步bulkTransfer()
语句:
private class MidiSender extends Thread { private boolean _raiseStop = false; private Object _sendLock = new Object(); private LinkedList _outputQueue = new LinkedList (); public void queue(ByteBuffer buffer) { synchronized(_sendLock) { _outputQueue.add(buffer); // Thread will most likely be paused (to save CPU); need to wake it _sendLock.notify(); } } public void raiseStop() { synchronized (this) { _raiseStop = true; } //Thread may be blocked waiting for a send synchronized(_sendLock) { _sendLock.notify(); } } public void run() { while (true) { synchronized (this) { if (_raiseStop) return; } ByteBuffer currentBuffer = null; synchronized(_sendLock) { if(!_outputQueue.isEmpty()) currentBuffer =_outputQueue.removeFirst(); } while(currentBuffer != null) { // Here's the synchronous equivalent (timeout is a reasonable 0.1s): int transferred = _connection.bulkTransfer(_outPort, currentBuffer.array(), currentBuffer.position(), 100); if(transferred < 0) Log.w(_tag, "Failed to send MIDI packet"); //Process any remaining packets on the queue synchronized(_sendLock) { if(!_outputQueue.isEmpty()) currentBuffer =_outputQueue.removeFirst(); else currentBuffer = null; } } synchronized(_sendLock) { try { //Sleep; save unnecessary processing _sendLock.wait(); } catch(InterruptedException e) { //Don't care about being interrupted } } } } }
起初我担心异步代码与上面的内容冲突(因为它们共享相同的UsbDeviceConnection
),但这似乎不是问题,因为它们使用的是完全不同的UsbEndpoint
实例。
好消息是应用程序运行得更顺畅,并且在我弹奏键盘的同时发送笔记时不会崩溃。
所以一般情况下(对于不同端点上的双向USB通信),似乎异步方法最适合读/输入操作,我们不需要担心定义轮询行为(这是否是内部发生的事情) ),同步bulkTransfer()
方法更好地服务于输出/发送操作。
我认为android_hardware_UsbRequest.cpp中存在一个错误。
在函数android_hardware_UsbRequest_queue_direct()中,在请求排队后设置全局引用,因此等待线程可以在设置引用之前获得控制权。
Global Ref是Java UsbRequest对象,UsbDeviceConnection.requestWait使用它来恢复UsbRequest对象。 如果等待线程在ref设置之前进入,这将是一个陈旧的引用。
显而易见的解决方案是在调用usb_request_queue()之前设置全局引用,如果调用失败则删除引用。
if (usb_request_queue(request)) { request->buffer = NULL; return false; } else { // save a reference to ourselves so UsbDeviceConnection.waitRequest() can find us // we also need this to make sure our native buffer is not deallocated // while IO is active request->client_data = (void *)env->NewGlobalRef(thiz); return true; }
更新:我已将此问题提升为AOSP问题#59467 。