Kotlin中每20秒稳定调用函数的实现方案(BLE设备通信场景)
我正在开发一款与BLE设备通信的应用,希望每20秒调用一次指定函数(每次调用时传入的参数值会有所不同)。我曾尝试使用Timer()实现该需求,也试过通过while循环搭配20秒延迟的方式,但均出现内存耗尽或抛出线程异常的问题,导致应用崩溃。由于我刚接触Kotlin,不清楚如何在不引发问题的前提下实现该功能。
相关代码如下:
BleExtensions.kt
fun BluetoothGatt.printGattTable() { ConnectionManager.writeCharacteristic(device, BluetoothGattCharacteristic( UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e"), BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE ), bytes1) }
ConnectionManager.kt
fun writeCharacteristic( device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, payload: ByteArray ) { val writeType = when { characteristic.isWritable() -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT characteristic.isWritableWithoutResponse() -> { BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE } else -> { Timber.e("Characteristic ${characteristic.uuid} cannot be written to") return } } if (device.isConnected()) { enqueueOperation(CharacteristicWrite(device, characteristic.uuid, writeType, payload)) } else { Timber.e("Not connected to ${device.address}, cannot perform characteristic write") } }
刚接触Kotlin的时候踩定时任务的坑太正常了!你用Timer()和while循环加延迟遇到的内存耗尽、线程异常问题,本质是这两种方式没处理好生命周期绑定和线程阻塞的问题——Timer容易持有上下文引用导致泄漏,while循环如果在主线程跑会直接卡死,在子线程跑又容易没管理好生命周期。结合BLE操作的特殊性,推荐你用下面两种更稳定的实现方式:
方式一:Kotlin协程(推荐,前台场景优先)
Kotlin协程是处理周期性任务最优雅安全的方式,它非阻塞、能和页面生命周期绑定,完美解决你之前的问题。
具体实现:
首先,先修改你的printGattTable函数,让它支持传入可变参数(毕竟你每次要传不同的bytes):
// BleExtensions.kt fun BluetoothGatt.printGattTable(payload: ByteArray) { ConnectionManager.writeCharacteristic(device, BluetoothGattCharacteristic( UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e"), BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE ), payload) }
然后在你的Activity/Fragment或者ViewModel里,用协程实现周期性调用:
import kotlinx.coroutines.* import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.Lifecycle class BleControlActivity : AppCompatActivity() { private var periodicTaskJob: Job? = null // 模拟你每次要变化的参数集合,实际可以根据业务逻辑动态生成 private val payloadList = listOf( byteArrayOf(0x01), byteArrayOf(0x02), byteArrayOf(0x03) ) private var currentPayloadIndex = 0 // 假设你已经有一个连接好的BluetoothGatt实例 private var connectedGatt: BluetoothGatt? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ble_control) // 绑定页面生命周期,只有页面活跃时才执行任务 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { periodicTaskJob = launch { while (isActive) { // 获取当前要传入的参数 val currentPayload = payloadList[currentPayloadIndex] // 先检查设备是否连接,再调用BLE函数 connectedGatt?.let { gatt -> gatt.printGattTable(currentPayload) } ?: run { Timber.e("BLE设备未连接,跳过本次任务") } // 更新参数索引,循环复用 currentPayloadIndex = (currentPayloadIndex + 1) % payloadList.size // 非阻塞延迟20秒,不会卡死线程 delay(20000) } } } } } override fun onDestroy() { super.onDestroy() // 页面销毁时取消协程,彻底释放资源,避免内存泄漏 periodicTaskJob?.cancel() // 别忘了关闭BLE连接 connectedGatt?.close() } }
为什么这个方式靠谱?
lifecycleScope是和页面生命周期绑定的协程作用域,页面销毁时协程会自动取消,从根源避免内存泄漏。delay是非阻塞的,不会占用主线程或者创建多余线程,性能友好。isActive会自动判断协程是否处于活跃状态,确保任务能及时停止。
方式二:WorkManager(后台长期运行场景)
如果你的定时任务需要在应用后台也能稳定执行(比如APP退到后台后还要继续和BLE设备通信),那WorkManager是更好的选择——它会适配Android的后台限制,保证任务不会被系统轻易杀死。
具体实现:
首先创建一个Worker类,用来执行BLE任务:
import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import android.content.Context import android.bluetooth.BluetoothManager class BlePeriodicWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { private var currentPayloadIndex = 0 private val payloadList = listOf( byteArrayOf(0x01), byteArrayOf(0x02), byteArrayOf(0x03) ) override suspend fun doWork(): Result { // 获取蓝牙管理器,尝试获取已连接的BLE设备 val bluetoothManager = applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val connectedDevice = // 这里需要你自己实现获取已连接BLE设备的逻辑 connectedDevice?.let { device -> // 假设你有办法获取对应的BluetoothGatt实例 val gatt = getConnectedGattInstance(device) gatt?.let { val currentPayload = payloadList[currentPayloadIndex] it.printGattTable(currentPayload) currentPayloadIndex = (currentPayloadIndex + 1) % payloadList.size } } ?: run { Timber.e("没有已连接的BLE设备,任务执行失败") return Result.retry() // 可以选择重试或者失败 } return Result.success() } // 这里需要你自己实现获取已连接BluetoothGatt实例的逻辑 private fun getConnectedGattInstance(device: BluetoothDevice): BluetoothGatt? { // 比如从ConnectionManager中维护的连接集合里查找 return ConnectionManager.getGattForDevice(device) } }
然后在合适的地方(比如Activity或者Application)启动周期性任务:
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import java.util.concurrent.TimeUnit fun startBleBackgroundTask(context: Context) { // 注意:Android 12及以上,后台周期性任务的最小间隔是15分钟 // 如果必须严格20秒,前台场景还是用协程更好 val periodicWorkRequest = PeriodicWorkRequestBuilder<BlePeriodicWorker>(20, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueue(periodicWorkRequest) }
注意事项:
- WorkManager的
PeriodicWorkRequest在Android 12+系统中,后台任务的最小执行间隔是15分钟,如果你的需求是严格20秒,那前台场景务必用协程。 - Worker中获取BLE连接需要确保设备处于连接状态,最好在ConnectionManager中维护已连接的Gatt实例集合,方便Worker查找。
额外提醒
- BLE操作必须在主线程或者专门的BLE线程执行(大部分Android BLE API要求在主线程发起操作),协程的
lifecycleScope默认在主线程运行,完全符合要求。 - 每次调用BLE函数前,一定要检查设备是否连接,避免因为设备断开导致的异常崩溃。
- 如果你的参数生成逻辑比较复杂,可以把它封装成单独的函数,每次调用前实时生成最新参数即可。
内容的提问来源于stack exchange,提问作者Henrique Guimarães

