PyQt5调用ADB、scrcpy投屏安卓设备时UI冻结问题
问题根因
PyQt5的UI交互完全运行在主线程事件循环上,只要主线程被阻塞,界面就会冻结无响应。你的代码存在两个直接导致阻塞的问题:
- 点击连接按钮后,启动scrcpy的逻辑直接在UI主线程执行
- 启动scrcpy的
subprocess.Popen后调用了communicate()和wait()方法,这两个方法会持续阻塞到scrcpy进程完全退出才会返回,直接卡死主线程事件循环,直到手动关闭scrcpy进程、阻塞解除,UI才会恢复响应。
修复步骤
1. 改造scrcpy启动逻辑,移除常驻进程的阻塞等待
短生命周期的adb配置命令可以保留wait/communicate(执行完立刻返回不会卡顿),但scrcpy是常驻前台的投屏进程,启动后绝对不能阻塞等待,只需要保存进程实例,后续断开时主动终止即可。
修改后的twt_start.py代码:
from subprocess import Popen as send import subprocess import os # 全局存储scrcpy进程实例,方便后续断开时终止 scrcpy_process = None def twt(window_title='Twitedb3rn', width='480', height='900'): global scrcpy_process try: orientation() # 不要给scrcpy设置stdout/stderr=PIPE,否则缓冲区写满会卡住scrcpy进程 scrcpy_process = send( f"scrcpy --always-on-top --window-title={window_title} --window-width={width} --window-height={height} ", stdin=subprocess.PIPE, text=True) print('\n\tTWT Started') except Exception as e: print(f'\nScrcpy Failed {str(e)}\n\n') def twt_ws(window_title='Twitedb3rn', width='480', height='900', serial='99031FFBA0083T'): global scrcpy_process try: orientation_ws(serial) os.chdir('resources/scrcpy') scrcpy_process = send( f'scrcpy --always-on-top --window-title={window_title} --window-width={width} --window-height={height} -s{serial}', stdin=subprocess.PIPE, text=True) print(f'\n\tTWT Started (serial: {serial})') except Exception as e: print(f'\nTWT Failed\n\n{str(e)}') def stop_scrcpy(): """主动终止scrcpy进程""" global scrcpy_process if scrcpy_process and scrcpy_process.poll() is None: scrcpy_process.terminate() scrcpy_process.wait(timeout=3) scrcpy_process = None # orientation与orientation_ws为短生命周期adb命令,保留等待逻辑,增加超时避免卡死 def orientation_ws(serial): try: console_send = send( f"adb -s {serial} shell content insert --uri content://settings/system --bind name:s:accelerometer_rotation --bind value:i:0", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) _orientation, errors = console_send.communicate(timeout=5) console_send.wait(timeout=5) print(f'\nScreen Rotation Disabled') try: home = send(f'adb -s {serial} shell input keyevent KEYCODE_HOME') home.communicate(timeout=5) home.wait(timeout=5) console_send = send( f"adb -s {serial} shell content insert --uri content://settings/system --bind name:s:user_rotation --bind value:i:1", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) _orientation, errors = console_send.communicate(timeout=5) console_send.wait(timeout=5) print(f'\nScreen Set Landscape') except Exception as e: print(f'\nScreen Landscape Failed {str(e)}') except Exception as e: print(f'\nScreen Rotation Not Disabled {str(e)}') def orientation(): try: console_send = send( f"adb shell content insert --uri content://settings/system --bind name:s:accelerometer_rotation --bind value:i:0", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) _orientation, errors = console_send.communicate(timeout=5) console_send.wait(timeout=5) print(f'\nScreen Rotation Disabled') try: home = send(f'adb shell input keyevent KEYCODE_HOME') home.communicate(timeout=5) home.wait(timeout=5) console_send = send( f"adb shell content insert --uri content://settings/system --bind name:s:user_rotation --bind value:i:0", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) _orientation, errors = console_send.communicate(timeout=5) console_send.wait(timeout=5) print(f'\nScreen Set Porttrait') except Exception as e: print(f'\nScreen Portrait Failed {str(e)}') except Exception as e: print(f'\nScreen Portrait Not Disabled {str(e)}')
2. 将scrcpy启动逻辑移到子线程执行,避免占用UI主线程
用PyQt5自带的QThread实现子线程任务,通过信号和主线程交互,禁止在子线程直接操作UI控件。修改后的主程序代码:
from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5 import uic import os import sys from ADBee import * from twt_start import * devs = devices() log_window = [] os.chdir('resources/ui/') # 子线程工作类 class ScrcpyWorker(QObject): finished = pyqtSignal() error = pyqtSignal(str) def __init__(self, serial=None): super().__init__() self.serial = serial def run(self): try: if self.serial: twt_ws(serial=self.serial) else: twt() self.finished.emit() except Exception as e: self.error.emit(str(e)) class TB_Main_Window(QMainWindow): def __init__(self): super(TB_Main_Window, self).__init__() uic.loadUi('main.ui', self) self.find_button.clicked.connect(lambda: self.find_devices(devs)) self.connect_button.clicked.connect(self.connect_to_device) self.disconnect_button.clicked.connect(self.disconnect_from_device) self.connect_button.setEnabled(False) self.disconnect_button.setEnabled(False) # 存储线程和工作实例 self.thread = None self.worker = None self.show() def find_devices(self, devs): count = 0 try: self.device_listbox.clear() if len(devs) == 0: print(" --- No Devices Found --- \n") elif len(devs) > 0: for d in devs: self.device_listbox.addItem(devs[count]) count += 1 self.connect_button.setEnabled(True) except: print("\nCould Not Find Devices\n") def get_selected_items(self): serial = self.device_listbox.currentText() print(serial) return serial def connect_to_device(self): _serial = self.device_listbox.currentText() self.find_button.setEnabled(False) self.connect_button.setEnabled(False) self.disconnect_button.setEnabled(True) num_devices = self.device_listbox.count() if num_devices == 0: print(f'\nNo Devices Found\n') self.find_button.setEnabled(True) self.connect_button.setEnabled(True) self.disconnect_button.setEnabled(False) return # 创建子线程运行启动逻辑 self.thread = QThread() target_serial = _serial if num_devices > 1 else None self.worker = ScrcpyWorker(serial=target_serial) self.worker.moveToThread(self.thread) # 绑定信号槽 self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.error.connect(lambda msg: print(f"Connect error: {msg}")) self.thread.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.start() def disconnect_from_device(self): # 先终止scrcpy进程 stop_scrcpy() # 退出子线程 if self.thread and self.thread.isRunning(): self.thread.quit() self.thread.wait() self.device_listbox.setEnabled(True) self.find_button.setEnabled(True) self.connect_button.setEnabled(True if self.device_listbox.count() > 0 else False) self.disconnect_button.setEnabled(False) try: kill_server() print(f"Device Disconnect Successfully") except: print(f"Can't Disconnect From Device") app = QApplication([]) window = TB_Main_Window() window.show() sys.exit(app.exec_())
额外注意点
- 所有subprocess等待逻辑最好增加timeout参数,避免异常时永久卡死
- 子线程内禁止直接修改UI控件状态,所有UI更新必须回到主线程通过信号槽触发

内容的提问来源于stack exchange,提问作者Roketteere
相关产品推荐
相关产品推荐

