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

Android ViewPager应用切换至第三个页面时蓝牙连接断开问题排查

问题:ViewPager切换至第三个Fragment时蓝牙连接断开

我正在开发一款基于Kotlin的Android移动应用,用于通过蓝牙远程控制机器人。MainActivity中通过ViewPager承载三个Fragment:负责蓝牙接口的FragmentMain、FragmentCamera和FragmentDisplayText。

当滑动页面切换至第三个页面时,蓝牙连接会断开。我尝试交换FragmentCamera和FragmentDisplayText的顺序,结果依然是切换到最后一个页面时连接断开,切换到第二个页面则正常。

相关代码

MyPagerAdapter类

class MyPagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) {
    override fun getItem(position: Int): Fragment {
        return when (position) {
            0 -> FragmentMain()
            1 -> FragmentCamera()
            2 -> FragmentDisplayText()
            else -> FragmentMain()
        }
    }

    override fun getCount(): Int {
        return 3
    }
}

MainActivity代码

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
        val viewPager = findViewById<ViewPager>(R.id.viewPager)
        val adapter = MyPagerAdapter(supportFragmentManager)

        viewPager.adapter = adapter
        tabLayout.setupWithViewPager(viewPager)
    }
}

蓝牙线程实现代码

/* ============================ Thread to Create Bluetooth Connection =================================== */

inner class CreateConnectThread(private val activity: Activity, private val context: Context, private val bluetoothAdapter: BluetoothAdapter, private val address: String) :
    Thread() {

    init {
        val bluetoothDevice = bluetoothAdapter.getRemoteDevice(address)
        var tmp: BluetoothSocket? = null
        var uuid: UUID? = null

        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) {
            uuid = bluetoothDevice.uuids[0].uuid
        } else {
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(
                    Manifest.permission.BLUETOOTH,
                    Manifest.permission.BLUETOOTH_CONNECT,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.BLUETOOTH_SCAN
                ), PERMISSION_REQUEST_CODE
            )
        }

        try {
            tmp = bluetoothDevice.createInsecureRfcommSocketToServiceRecord(uuid)
        } catch (e: IOException) {
            Log.e(TAG, "Socket's create() method failed", e)
        }
        if (tmp != null) {
            mmSocket = tmp
        }
    }

    override fun run() {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
            && ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) {
            bluetoothAdapter.cancelDiscovery()
            try {
                mmSocket.connect()
                Log.e("Status", "Device connected")
                handler.obtainMessage(CONNECTING_STATUS, 1, -1).sendToTarget()
            } catch (connectException: IOException) {
                try {
                    mmSocket.close()
                    Log.e("Status", "Cannot connect to device")
                    handler.obtainMessage(CONNECTING_STATUS, -1, -1).sendToTarget()
                } catch (closeException: IOException) {
                    Log.e(TAG, "Could not close the client socket", closeException)
                }
                return
            }
        } else {
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(
                    Manifest.permission.BLUETOOTH,
                    Manifest.permission.BLUETOOTH_CONNECT,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.BLUETOOTH_SCAN
                ), PERMISSION_REQUEST_CODE
            )
        }
        bluetoothAdapter.cancelDiscovery()
        connectedThread = ConnectedThread(mmSocket)
        connectedThread.start()
    }

    fun cancel() {
        try {
            mmSocket.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the client socket", e)
        }
    }
}

/* =============================== Thread for Data Transfer =========================================== */

inner class ConnectedThread(socket: BluetoothSocket) : Thread() {
    private val mmSocket: BluetoothSocket = socket
    private val mmInStream: InputStream
    private val mmOutStream: OutputStream

    init {
        var tmpIn: InputStream? = null
        var tmpOut: OutputStream? = null
        try {
            tmpIn = socket.inputStream
            tmpOut = socket.outputStream
        } catch (_: IOException) {
        }
        mmInStream = tmpIn!!
        mmOutStream = tmpOut!!
    }

    override fun run() {
        val buffer = ByteArray(1024)
        var bytes = 0
        while (true) {
            try {
                buffer[bytes] = mmInStream.read().toByte()
                var readMessage: String
                if (buffer[bytes] == '\n'.toByte()) {
                    readMessage = String(buffer, 0, bytes)
                    Log.e("Arduino Message", readMessage)
                    handler.obtainMessage(MESSAGE_READ, readMessage).sendToTarget()
                    bytes = 0
                } else {
                    bytes++
                }
            } catch (e: IOException) {
                e.printStackTrace()
                break
            }
        }
    }

    fun write(input: String) {
        val bytes = input.toByteArray()
        try {
            mmOutStream.write(bytes)
        } catch (e: IOException) {
            Log.e("Send Error", "Unable to send message", e)
        }
    }

    fun cancel() {
        try {
            mmSocket.close()
        } catch (e: IOException) {
        }
    }
}

原因分析与解决方案

核心原因:FragmentStatePagerAdapter的销毁机制

你使用的FragmentStatePagerAdapter会销毁超出预加载范围的Fragment实例。ViewPager默认预加载数量为1,也就是说:

  • 停留在第0个Fragment(FragmentMain)时,会预加载第1个Fragment;
  • 滑动到第2个Fragment时,第0个Fragment会被销毁(距离当前页面超过1个位置)。

如果蓝牙连接逻辑(包括线程、Socket变量)是放在FragmentMain内部的,FragmentMain被销毁时,其内部线程会终止,持有的蓝牙Socket也会被关闭,直接导致连接断开。

解决方案

1. 临时方案:调整ViewPager预加载数量

在MainActivity中设置ViewPager的预加载数量为Fragment总数-1,让三个Fragment始终保持存活:

viewPager.offscreenPageLimit = 2

缺点是会占用更多内存,不适合Fragment数量多的场景。

2. 推荐方案:迁移蓝牙逻辑至MainActivity或单例类

把蓝牙连接、线程管理逻辑从FragmentMain抽离:

  • 将CreateConnectThread、ConnectedThread、mmSocket、connectedThread等移到MainActivity中;
  • FragmentMain通过requireActivity() as MainActivity获取实例,调用蓝牙相关方法;
  • 在MainActivity的onDestroy生命周期中正确关闭蓝牙连接,避免内存泄漏。

3. 长期最优方案:改用ViewPager2+ViewModel

ViewPager2是ViewPager的升级版,配合ViewModel可以更好地管理跨Fragment的状态:

  • 将蓝牙连接的状态和逻辑放到ViewModel中,ViewModel生命周期与Activity绑定,不受Fragment销毁影响;
  • 各个Fragment通过ViewModelProvider获取同一个ViewModel实例,操作蓝牙连接。

验证步骤

  1. 先设置offscreenPageLimit = 2,确认是否是Fragment销毁导致连接断开;
  2. 验证成功后,再逐步将蓝牙逻辑迁移到MainActivity或ViewModel中,优化内存占用。

内容的提问来源于stack exchange,提问作者Wiktoria

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 15:40:01