You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 21:42:43