首页 > 解决方案 > 我从蓝牙中丢失了在活动之间移动和使用服务的数据

问题描述

我正在构建一个通过蓝牙 spp 连接与外部设备交换数据的应用程序。在此处搜索了一下如何保持活动之间的连接后,我发现更好的方法是实现一个服务并将其绑定到活动。我认为我编写的代码是正确的,但是当我从第一个活动移动到第二个活动并向外部设备发送请求时,我从读取缓冲区中丢失了一些答案消息。如果我在发出请求之前等待大约 5 秒钟,它就可以正常工作。

这是我的课程,我正在使用一个名为BlueFlow的蓝牙库:

class BluetoothService : Service() {

    private val btChannel: BtChannel by inject()

    inner class LocalBinder: Binder() {
        val bindService = this@BluetoothService
    }

    override fun onBind(intent: Intent?): IBinder {
        return LocalBinder()
    }

    fun sendRequest(data: ByteArray): Boolean {
        return btChannel.send(data)
    }

    fun readChannel(): LiveData<ByteArray>? {
        return btChannel.read()?.cancellable()?.asLiveData()
    }
}

class BtChannel(private val blueFlow: BlueFlow) {

    fun read(): Flow<ByteArray>? {
        return blueFlow.getIO()?.readByteArrayStream()
    }

    fun send(data: ByteArray): Boolean {
        return blueFlow.getIO()?.send(data) ?: false
    }

}

// This is the readByteArrayStream function from the blueFlow library
@ExperimentalCoroutinesApi
    fun readByteArrayStream(
        delayMillis: Long = 1000,
        minExpectedBytes: Int = 2,
        bufferCapacity: Int = 1024,
        readInterceptor: (ByteArray) -> ByteArray? = { it }
    ): Flow<ByteArray> = channelFlow {

        if (inputStream == null) {
            throw NullPointerException("inputStream is null. Perhaps bluetoothSocket is also null")
        }
        val buffer = ByteArray(bufferCapacity)
        val byteAccumulatorList = mutableListOf<Byte>()
        while (isActive) {
            try {
                if (inputStream.available() < minExpectedBytes) {
                    delay(delayMillis)
                    continue
                }
                val numBytes = inputStream.read(buffer)
                val readBytes = buffer.trim(numBytes)
                if (byteAccumulatorList.size >= bufferCapacity)
                    byteAccumulatorList.clear()

                byteAccumulatorList.addAll(readBytes.toList())
                val interceptor = readInterceptor(byteAccumulatorList.toByteArray())

                if (interceptor == null)
                    delay(delayMillis)

                interceptor?.let {
                    offer(it)
                    byteAccumulatorList.clear()
                }

            } catch (e: IOException) {
                byteAccumulatorList.clear()
                closeConnections()
                error("Couldn't read bytes from flow. Disconnected")
            } finally {
                if (bluetoothSocket?.isConnected != true) {
                    byteAccumulatorList.clear()
                    closeConnections()
                    break
                }
            }
        }
    }.flowOn(Dispatchers.IO)


class FirstActivity: AppCompatActivity() {

  private lateinit var viewModel: FirstViewModel
  private lateinit var mBluetoothService: BluetoothService
  private var mBound = false

  private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as BluetoothService.LocalBinder
            mBluetoothService = binder.bindService
            mBound = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            mBound = false
        }
    }

  override fun onStart() {
      super.onStart()
      startBluetoothService()
      setObservers()
  }

  override fun onStop() {
      super.onStop()
      unbindService(serviceConnection)
      mBound = false
      removeObservers()
    }

   private fun startBluetoothService() {
    val intent = Intent(this, BluetoothService::class.java)
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
   }

    private fun setObservers() {
        viewModel.hours.observe(this) {hoursTv?.text = it }
        mBluetoothService?.readChannel()?.observe(this, { viewModel.getData(it) })
    }

    private fun removeObservers() {
        viewModel.hours.removeObservers(this@FirstActivity)
        mBluetoothService?.readChannel()?.removeObservers(this@FirstActivity)
    }
}

class FirstViewModel: ViewModel() {

  val hours: MutableLiveData<String> by lazy {
      MutableLiveData<String>()
  }

  fun getData(bytes: ByteArray) = viewModelScope.launch {
        getDataSafeCall(bytes)
  }

    @ExperimentalUnsignedTypes
  private fun getDataSafeCall(bytes: ByteArray) {
      PacketFrame.decodePacket(bytes.toUByteArray(), bytes.size).onEach { updateData(it) }.launchIn(viewModelScope)
  }

  fun updateData(packet: HashMap<PacketHeader, UByteArray>) {
            packet.forEach { packet ->
                when (packet.key) {
                    PacketHeader.Hour -> {
                        hours.value = PacketFrame.payloadToString(packet.value)
                    }
                    else -> {
                    }
                }
            }
    }
}

class SecondActivity: AppCompatActivity() {
  //same as FirstActivity
}

class SecondViewModel: ViewModel() {
  // same as FirstViewModel
}

PacketFrame 是一个单例。我用它来组合使用自定义协议从蓝牙接收到的数据包。

你有什么建议吗?

更新:

我正在阅读文档,试图找出问题所在,我发现了这一点:

流是冷流 是类似于序列的冷流——流构建器中的代码在收集流之前不会运行。

但我也发现了这个:

channelFlow 创建一个冷流实例,其中包含发送到通过 ProducerScope 提供给构建器代码块的 SendChannel 的元素。它允许元素由在不同上下文或同时运行的代码生成。结果流是冷的,这意味着每次将终端运算符应用于结果流时都会调用该块。此构建器确保线程安全和上下文保存,因此提供的 ProducerScope 可以从不同的上下文中同时使用。一旦块中的代码及其所有子代码完成,生成的流程就会完成。

我的理解是 channelFlow (用于库中的 readByteArrayStream 函数)继续运行,直到消费者还活着并请求元素。

在我的代码中,我在 ViewModel 中启动协程,当我调用第二个 Activity 时它没有被清除,因为它保留在堆栈中,因此第一个 ViewModel 上的函数继续从蓝牙接收数据,直到在第二个 ViewModel 中创建流程和使用 LiveData e 观察它来消耗它。

你怎么看待这件事?关于如何解决的任何建议?

标签: androidkotlinservicekotlin-coroutinesandroid-bluetooth

解决方案


我将尝试提出一些建议,尽管很难知道您要实现的目标以及为什么要在读数中间启动新活动。

我建议您将readChannel()返回类型更改为SharedFlow并将readByteArrayStream()函数转换为 SharedFlow 以及使用shareIn()调用。然后按原样收集它而不将其转换为实时数据。该库是之前构建的StateFlow并被SharedFlow引入,因此这些新的流类型在这种情况下可能会派上用场。

另一种方法可能是SharedFlow再次使用将充当事件总线并重新发送通过readByteArrayStream()查看此处收集的数据以获取此类 EventBus 实现的示例。

这就是我现在所能建议的。我真的不确定您的用例,如果可以更好地理解,我想检查一个与相关问题相关的示例项目。


推荐阅读