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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:09:21