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实例,操作蓝牙连接。
验证步骤
- 先设置
offscreenPageLimit = 2,确认是否是Fragment销毁导致连接断开; - 验证成功后,再逐步将蓝牙逻辑迁移到MainActivity或ViewModel中,优化内存占用。
内容的提问来源于stack exchange,提问作者Wiktoria
相关产品推荐
相关产品推荐

