场景:
1.随着游戏的上线经常会面临安装不同渠道包到不同模拟器或手机上的操作,每次手动操作感觉很麻烦。
2.在手机上测试有 BUG 的时候开发期望能能把完整日志拉出来排查 BUG,每次都 adb 手动操纵效率很低。
基于这两点把之前做的东西升级了一下,让它能给不同设备安装不同 apk,同时还可以直接拉出完整日志
界面:
代码:
DeviceManager.py 用来处理整个界面逻辑
InstallItem.py 用来处理每个 device 的安装、销毁、拉日志等逻辑
# -*- coding: utf-8 -*-
import os
import tkinter as tk
from tkinter import * # 导入 Tkinter 库
import multiprocessing
import threading
from InstallItem import InstallItem
class DeviceManager():
def __init__(self):
try:
cmd = 'adb connect 127.0.0.1:7555' # 木木模拟器比较常用,所以写在程序里
os.system(cmd)
except EXCEPTION as e:
print(e)
self.root = Tk()
self.root.geometry('1050x300')
self.root.title("DeviceManager")
self.log_path=StringVar()
self.log_path.set("D:\\DeviceLog")
self.apk_path = StringVar()
self.apk_path.set("D:\\")
#连接模拟器
self.init_port =IntVar()
#nox模拟器 62001 第二个开始 62025+index
self.init_port.set(62001)
self.increase_num = IntVar()
self.increase_num.set(1)
self.apk_name = StringVar()
self.apk_name.set("请输入apk完整包名")
self.select_device_list =[]
self.device_list = self.get_device_list()
self.cb_list =[]
self.pool = multiprocessing.Pool(processes=6)
self.button_list=[]
self.button_pull_log_list=[]
self.install_item_list=[]
def get_log_path(self):#得到log存放路径
return self.log_path.get()
def get_apk_path(self):#得到apk更目录路径
return self.apk_path.get()
def draw_log_path(self):
label_log=tk.Label(self.root, text="日志保存路径:")
label_log.grid(row=0,column=1,sticky='w')
entry_log_path = Entry(self.root, width=50, textvariable=self.log_path)
entry_log_path.grid(row=0,column=2,sticky='w')
button_open_log = Button(self.root, text="打开", command=lambda: self.open_file(entry_log_path.get()))
button_open_log.grid(row=0 ,column=4)
#刷新adb
button_refresh = Button(self.root,text="刷新adb",bg="LightSteelBlue",command=lambda:self.refresh_adb())
button_refresh.grid(row=0,column=7)
#关闭adb
button_close_adb = Button(self.root,text="关闭adb",command=self.close_adb)
button_close_adb.grid(row=0,column=8)
#连接模拟器
button_connect_device = Button(self.root, text="连接模拟器", command=self.start_connect_device_thread)
button_connect_device.grid(row=1, column=12)
def draw_device_connect(self):
label_device_start_port = tk.Label(self.root,text="起始端口:")
label_device_start_port.grid(row=1,column=7,sticky='w')
entry_device_start_port = Entry(self.root, width=5, textvariable=self.init_port)
entry_device_start_port.grid(row=1,column=8,sticky='w')
label_device_num = tk.Label(self.root, text="连接数量:")#模拟器多开时想要连接的数量
label_device_num.grid(row=1, column=9, sticky='w')
entry_device_num = Entry(self.root, width=5, textvariable=self.increase_num)
entry_device_num.grid(row=1, column=10, sticky='w')
def close_adb(self):
print("关闭 adb")
try:
cmd = 'adb kill-server'
os.system(cmd)
except EXCEPTION as e:
print(e)
def refresh_adb(self):
print("刷新adb")
try:
cmd = 'adb devices'
os.system(cmd)
except EXCEPTION as e:
print(e)
old_device_list =self.device_list
new_device_list =self.get_device_list()
#old-new 的差集用old_new表示
old_new = set(old_device_list)-set(new_device_list)
print("old-new:",list(old_new))
for item in self.install_item_list:
if item.device in list(old_new):
item.destroy()
#new-old 的差集用new_old表示
new_old = set(new_device_list)-set(old_device_list)
start_draw_index = len(set(old_device_list)&set(new_device_list))
self.draw_installer_item(list(new_old),start_draw_index)
self.device_list=new_device_list
def connect_device(self,index):
try:
port =self.init_port.get()+index
print(port)
cmd = 'adb connect 127.0.0.1:'+str(port)
print(cmd)
os.system(cmd)
except:
print("连接第%d个模拟器有问题" % index)
def start_connect_device_thread(self):
print(self.increase_num.get())
for i in range(self.increase_num.get()):
t= threading.Thread(target=self.connect_device,args=(i,))
t.start()
def draw_apk_path(self):
label_apk_path = tk.Label(self.root, text="APK根目录:")
label_apk_path.grid(row=1, column=1, sticky='w')
entry_apk_path = Entry(self.root, width=50, textvariable=self.apk_path)
entry_apk_path.grid(row=1, column=2, sticky='w')
button_open_apkPath = Button(self.root, text="打开", command=lambda : self.open_file(entry_apk_path.get()))
button_open_apkPath.grid(row=1, column=4)
def draw_installer_item(self,device_list,old_device_num=0):
for index,device in enumerate(device_list):
start_row=index+old_device_num+2
install_item=InstallItem(self.root,device,start_row,self)
self.install_item_list.append(install_item)
self.root.grid_columnconfigure((0,3,5), minsize=20)
def mainloop(self):
self.draw_log_path()
self.draw_device_connect()
self.draw_apk_path()
self.draw_installer_item(self.device_list)
self.root.mainloop()
def get_device_list(self):#得到设备列表
os.system("adb devices")
res = os.popen("adb devices").readlines()
device_list = [sub.split('\t')[0] for sub in res[1:-1]]
# device_list = [sub for sub in res[1:-1]]
return device_list
def open_file(self,path):#打开文件
os.system("explorer "+path)
if __name__=='__main__':
multiprocessing.freeze_support()#不加这句打包成exe后会出现死循环
apkInstaller= DeviceManager()
apkInstaller.mainloop()
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import * # 导入 Tkinter 库
import os
import time
import threading
class InstallItem:
def __init__(self,root,device,index,DeviceManager):
self.DeviceManager = DeviceManager
self.device = device
self.entry_default = StringVar()
self.entry_default.set("请输入apk完整包名")
self.root=root
self.lab_device = tk.Label(self.root, text=device)
self.lab_device.grid(row=index,column=1,stick='w')
self.entry_apk = Entry(self.root, width=65, textvariable=self.entry_default)
self.entry_apk.grid(row=index,column=2,stick='w')
self.button_install = Button(self.root, text="安装",command=self.thread_install_apk)
self.button_install.grid(row=index,column=4,stick='w')
self.button_pull_log = Button(self.root,text="拉日志",command=self.pull_log)
self.button_pull_log.grid(row=index,column=6,stick='w')
def destroy(self):
self.button_install.destroy()
self.button_pull_log.destroy()
self.entry_apk.destroy()
self.lab_device.destroy()
def pull_log(self):
print("device",self.device)
if "emulator" in self.device or "127.0.0.1" in self.device:#如果是模拟器
device_log_path = "sdcard/Android/data/*****/files/Logs"
else:# 如果是手机
device_log_path = "/storage/emulated/0/Android/data/******/files/Logs"
if ':' in self.device:
device_name = self.device.replace(':', '-') # 这里用来处理模拟器多开,冒号在路径名中无法使用,所以替换一下
else:
device_name = self.device
computer_copy_path = self.DeviceManager.get_log_path() +'\\' + device_name+"\\" + self.set_file_name() # 本地的路径,存在指定的文件夹再加上设备名作为区分
print(computer_copy_path)
if not os.path.exists(computer_copy_path):
os.makedirs(computer_copy_path)
cmd = 'adb -s {0} pull {1} {2}'.format(self.device, device_log_path, computer_copy_path)
# print(cmd)
os.system(cmd)
def set_file_name(self):#设置文件名
now=time.strftime("%Y-%m-%d-%H-%M-%S",time.localtime(time.time()))
return now
def install_apk(self):
# 安装游戏
print(self.entry_apk.get())
cmd = "adb -s " + self.device + " install -r " + self.DeviceManager.get_apk_path() + "\\" + self.entry_apk.get()
print("installing ", cmd)
os.system(cmd)
def thread_install_apk(self):#用多线程来安装,不然点击安装后会卡住主线程,无法实现多apk同时安装
t = threading.Thread(target=self.install_apk, )
t.start()
** 一些细节 **
【刷新 adb】
当接入新的设备或者拔掉旧的设备时需要进行刷新操作,如果直接执行 adb devices 命令来进行操作的话,就会把已经正在执行安装操作的条目刷新成空白,所以这里的刷新逻辑是这样:
1.取刷新前的设备列表为 old_device_list ,取执行 adb devices 后得到的列表为 new_device_list
2.用 old_new 表示 old-new 的差集,这个差集代表已经去掉的设备
3.用 new_old 表示 new_old 的差集,这个差集代表新增的设备
4.先销毁已经去掉的设备再增加新增的设备到界面上
5.新增设备的 index 从 old_device_list 和 new_device_list 的交集数量往上加
def refresh_adb(self):
print("刷新adb")
try:
cmd = 'adb devices'
os.system(cmd)
except EXCEPTION as e:
print(e)
old_device_list =self.device_list
new_device_list =self.get_device_list()
#old-new 的差集用old_new表示
old_new = set(old_device_list)-set(new_device_list)
print("old-new:",list(old_new))
for item in self.install_item_list:
if item.device in list(old_new):
item.destroy()
#new-old 的差集用new_old表示
new_old = set(new_device_list)-set(old_device_list)
start_draw_index = len(set(old_device_list)&set(new_device_list))
self.draw_installer_item(list(new_old),start_draw_index)
self.device_list=new_device_list
【连接模拟器】
如果数量=1 ,就直接连接这个模拟器
如果数量>1 ,以所填的端口为基础依次往上加
比如 nox 模拟器多开时,第二个端口是 62025 ,第三个是 62026 ,如果要连接这两个只需要起始端口填 62025,连接数量填 2 即可。
用多线程操作避免阻塞。
def connect_device(self,index):
try:
port =self.init_port.get()+index
print(port)
cmd = 'adb connect 127.0.0.1:'+str(port)
print(cmd)
os.system(cmd)
except:
print("连接第%d个模拟器有问题" % index)
def start_connect_device_thread(self):
print(self.increase_num.get())
for i in range(self.increase_num.get()):
t= threading.Thread(target=self.connect_device,args=(i,))
t.start()
【拉日志】
拉出的日志按设备名分不同文件夹,再按操作时间分子文件夹
总结
多线程安装明显没有手动安装快,但是比较方便,如果不是很急我可以挂着这边安装另一边同时做其他事情。
拉日志的功能明显比自己用 adb 命令操作便捷了很多,帮我节省了很多时间。