Android 自动化测试项目
1 前言
在 Android自动化测试框架uiautomator2详解 中,介绍了 uiautomator2 框架的环境配置、元素定位工具以及常用接口。
本文对 uiautomator2 框架进一步封装,用户只需要重写模板类(Template)的 first() 和 circle_body() 方法,并配置 app.cfg 文件,即可实现各种测试挂机需求。
本文使用到的主要 python 模块如下:
- uiautomator2:Android 手机 UI 自动化控制器
- aircv:图片匹配
- configparser:配置文件解析器
- subprocess:子进程管理,本文主要用于获取 adb 命令的返回结果(check_output方法)
- os:系统管理,本文主要用于创建目录(mkdir 方法)、文件路径管理(path)和执行 adb 命令(system 方法)
2 项目结构
- phone.py:封装了对手机的操作,包括:连接设备、获取手机信息(分辨率、虚拟 display id)、解锁、打开应用、点击、长按、输入文本、滑动、截手机屏、截虚拟屏、旋转、点击匹配的图片等动作
- template.py:封装了一个测试模板,用户的只需要继承 Template 类,并重写 first 方法(初始状态)、circle_body 方法(循环体),即可实现各种复杂的挂机任务
- matcher:图片匹配器,在当前手机界面匹配目标图片,并返回匹配到的中心位置
- app.cfg:应用包名和控件标识(resourceId、text 等)配置
- parser:应用配置解析器,用于解析 app.cfg 文件
- img:存放用户截图
- log:存放日志
- test.py:用户挂机任务类,此类继承了 Template 类
项目路径见→Android 自动化测试项目
3 安装环境
install.py
import os
# https://pypi.douban.com/simple # 豆瓣镜像
# https://pypi.tuna.tsinghua.edu.cn/simple # 清华镜像
mirror = " -i https://pypi.douban.com/simple"
os.system("python -m pip install --upgrade pip" + mirror) # 更新 pip
os.system("pip install --pre -U uiautomator2" + mirror) # 安装 uiautomator2
os.system("python -m uiautomator2 init") # 安装 atx-agent 至手机
os.system("pip install weditor" + mirror) # 安装 weditor
os.system("pip install aircv" + mirror) # 安装 aircv
os.system("pip install opencv-python" + mirror) # 安装 cv2
运行此文件,即可安装所需的模块。 若运行时报错,可以参考→Android自动化测试框架uiautomator2详解
4 自动化测试
1)手机类(Phone)
Phone 类封装了对手机的操作,包括:连接设备、解锁、打开应用、点击、长按、输入文本、滑动、截屏、旋转等动作。
phone.py
import os
import re
import shutil
import subprocess as sp
import threading
import time
from time import sleep
import uiautomator2 as u2
from base.parser import Parser
from base.matcher import Matcher
# 手机类(封装了对手机的操作)
class Phone:
# 初始化(connect:连接类型,wait_time:操作之间的等待时间, clear:是否清空 img 和 log 目录)
def __init__(self, connect_type="usb", wait_time=1, clear=False):
print("Phone 初始化...")
global d, w_time, parser, matcher, screen_shot_path, log_path
d = self.get_device(connect_type)
self.get_phone_model()
self.get_phone_size()
self.get_vs_display_id()
w_time = wait_time
parser = Parser(d, self.size)
matcher = Matcher()
screen_shot_path = os.path.abspath(os.path.abspath(os.path.dirname(__file__)) + "\\..\\img") + "\\"
log_path = os.path.abspath(os.path.abspath(os.path.dirname(__file__)) + "\\..\\log") + "\\"
self.clear_img_log(clear)
# 获取设备(connect_type:连接类型)
def get_device(self, connect_type="usb"):
print("获取设备...")
if connect_type == "usb":
device = self.connect_by_usb() # 通过 usb 连接设备
elif connect_type == "wifi":
device = self.connect_by_wifi() # 通过 wifi 连接设备
return device
# 通过 usb 连接设备(程序运行期间需要保持有线连接)
def connect_by_usb(self):
rst = sp.check_output('adb devices')
id = re.findall("[A-Z0-9]{16}", str(rst))[0]
print("通过 usb 连接设备,id:", id)
device = u2.connect_usb(id)
return device
# 通过 wifi 连接设备(程序启动时需要有线连接,运行后可以断开数据线)
def connect_by_wifi(self):
rst = sp.check_output('adb shell \"ip addr | grep global\"')
ip = re.findall("\d+.\d+.\d+.\d+", str(rst))[0]
print("通过 wifi 连接设备,ip:", ip)
try:
device = u2.connect(ip)
except:
device = u2.connect(ip + ":5555")
return device
# 获取手机型号
def get_phone_model(self):
rst = sp.check_output('adb shell getprop ro.product.model')
self.model = re.findall("\w+-\w+", str(rst))[0]
print("手机型号:", self.model)
# 获取手机分辨率
def get_phone_size(self):
rst = sp.check_output('adb shell wm size')
str_size = re.findall("\d+", str(rst))
x = eval(str_size[0])
y = eval(str_size[1])
self.size = (x, y)
print("手机分辨率:", self.size)
# 获取 visual display id
def get_vs_display_id(self):
rst = sp.check_output('adb shell dumpsys activity activities')
displays = re.findall("Display #\d+", str(rst))
vs_id = -1
for e in displays:
id = eval(re.findall("\d+", e)[0])
if id > 0:
vs_id = id
break
self.vs_display_id = vs_id
print("visual display id:", self.vs_display_id)
# 解锁(password:解锁密码)
def unlock(self, password=""):
print("解锁...")
d.screen_on() # 亮屏
sleep(2)
self.swipe("up") # 向上滑动
edt_password = parser.get_element("system", "edt_password")
sleep(1)
if edt_password.exists:
if not password == "":
for i in password:
key = parser.get_element_item_num("system", "key", i)
sleep(0.1)
if key.exists:
key.click()
sleep(0.3)
sleep(w_time)
# 锁屏
def lock(self):
print("锁屏...")
d.screen_off() # 息屏
# d.press("power") # 按电源键
sleep(w_time)
# 打开指定应用(app_name:应用名)
def start_app(self, app_name):
print("打开应用,app_name:", app_name, "...")
package_name = parser.get_package(app_name)
sleep(1)
d.app_start(package_name)
sleep(w_time)
# 关闭指定应用(app_name:应用名)
def stop_app(self, app_name):
print("关闭应用,app_name:", app_name, "...")
package_name = parser.get_package(app_name)
sleep(1)
d.app_stop(package_name)
sleep(w_time)
# 关闭所有应用
def stop_all_app(self):
print("关闭所有应用...")
d.app_stop_all()
sleep(w_time)
# 清除所有应用
def clear_all_app(self):
print("清除所有应用...")
d.press("recent")
sleep(w_time)
btn_clear = parser.get_element("launcher", "btn_clear")
sleep(1)
if btn_clear.exists: # 清除按钮存在
btn_clear.click() # 清除所有 app
else: # 清除按钮不存在
print("没有打开的应用")
d.press("home") # 返回桌面
sleep(w_time)
# 截手机屏(clear:是否清空 img 目录)
def screen_shot(self, clear=False):
print("截手机屏...")
time_str = time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime()) # 获取当前时间
path = screen_shot_path + time_str + ".png"
d.screenshot(path)
sleep(w_time)
# 截虚拟屏(clear:是否清空 img 目录)
def screen_shot_vs(self, clear=False):
if self.vs_display_id > 0:
print("截虚拟屏...")
time_str = time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime()) # 获取当前时间
path = screen_shot_path + time_str + "_vs.png"
os.system("adb shell screencap -d " + str(self.vs_display_id) + " -p /sdcard/1.png")
os.system("adb pull /sdcard/1.png " + path)
sleep(w_time)
else:
print("虚拟屏不存在")
# 转屏
def rotate(self):
orientation = d.orientation
print("转屏,当前方向:", orientation)
if orientation == "natural":
d.set_orientation("left") # 向左转为横屏(left 可以简写为 l)
elif orientation == "left":
d.set_orientation("natural") # 转为竖屏(nutural 可以简写为 n)
sleep(w_time)
# 按键 key = {"back", "home", "recent", "power", "volume_up", "volume_down", "volume_mute", "enter",...}
def press(self, key):
print("按键:" + key)
d.press(key)
sleep(w_time)
# 单击控件(app_name:应用名,item:控件名,index:同名控件中的编号,type:控件标识符的类型)
def click(self, app_name, item, index=0, type="id"):
print("单击控件,app_name:", app_name, ",item:", item, ",index:", index, ",type:", type)
element = parser.get_element(app_name, item, type)
sleep(1)
if element[index].exists:
element[index].click()
sleep(w_time)
# 通过坐标单击控件(app_name:应用名,item:控件名,relative:相对坐标)
def click_coord(self, app_name, item, relative="left_top"):
print("通过坐标单击控件,app_name:", app_name, ",item:", item, ",relative:", relative)
x, y = parser.get_coord(app_name, item, relative)
sleep(1)
d.click(x, y)
sleep(w_time)
# 通过图标单击控件(target: 目标图片, threshold: 可信阈值)
def click_icon(self, target, threshold = 0.6):
print("通过图标单击控件,target:", target, ",threshold:", threshold)
pos = matcher.get_coord(target, threshold)
if pos == None:
print("控件不存在")
else:
sleep(1)
d.click(pos[0], pos[1])
sleep(w_time)
# 长按控件(app_name:应用名,item:控件名,index:同名控件中的编号,type:控件标识符的类型)
def long_click(self, app_name, item, index=0, type="id"):
print("长按控件,app_name:", app_name, ",item:", item + ",index:", index, ",type:", type)
element = parser.get_element(app_name, item, type)
sleep(1)
if element.exists:
element[index].long_click()
sleep(w_time)
# 输入文本(text:待输入的文本)
def set_text(self, text):
print("输入文本,text:", text)
d.set_fastinput_ime(True) # 打开输入法
d.send_keys(text)
d.set_fastinput_ime(False) # 关闭输入法
sleep(w_time)
# 输入文本(app_name:应用名,item:控件名,text:待输入的文本,type:控件标识符的类型)
def set_text(self, app_name, item, text, type="id"):
print("输入文本,app_name:", app_name, ",item:", item, ",text:", text, ",type:", type)
element = parser.get_element(app_name, item, type)
sleep(1)
if element.exists:
element.set_text(text)
sleep(w_time)
# 获取文本(app_name:应用名,item:控件名,type:控件标识符的类型)
def get_text(self, app_name, item, type):
print("获取文本,app_name:", app_name, ",item:", item, ",type:", type)
element = parser.get_element(app_name, item, type)
sleep(1)
text = ""
if element.exists:
text = element.get_text()
return text
# 清空文本(app_name:应用名,item:控件名,type:控件标识符的类型)
def clear_text(self, app_name, item, type):
print("获取文本,app_name:", app_name, ",item:", item, ",type:", type)
element = parser.get_element(app_name, item, type)
sleep(1)
if element.exists:
element.clear_text()
sleep(w_time)
# 滑动屏幕
def swipe(self, direction):
print("滑动屏幕,direction:" + direction)
(width, hight) = d.window_size()
width_d = width / 6
hight_d = hight / 6
if direction == "up": # 上滑
d.swipe(width / 2, hight - hight_d, width / 2, hight_d)
elif direction == "down": # 下滑
d.swipe(width / 2, hight_d, width / 2, hight - hight_d)
elif direction == "left": # 左滑
d.swipe(width - width_d, hight / 2, width_d, hight / 2)
else: # 右滑
d.swipe(width_d, hight / 2, width - width_d, hight / 2)
sleep(w_time)
# 将 pc 端文件 push 到 phone 端指定文件夹,若此文件夹不存在,会自动创建
def push(self, pc_file_path, phone_dir_path="/sdcard/000/"):
# pc_file_path = "..\\img\\" + pc_file_path
print("push,pc_file_path:", pc_file_path, ",phone_dir_path", phone_dir_path)
d.push(pc_file_path, phone_dir_path)
sleep(w_time)
# 将 phone 端的文件 pull 到 pc 端指定文件夹,若此文件夹不存在,会自动创建
def pull(self, phone_file_path, pc_dir_path="..\\img\\"):
print("pull,phone_file_path:", phone_file_path, ",pc_dir_path", pc_dir_path)
if not os.path.exists(pc_dir_path):
os.mkdir(pc_dir_path)
pc_dir_path += phone_file_path.split("/")[-1]
d.pull(phone_file_path, pc_dir_path)
sleep(w_time)
# 打印日志
def log(self):
time_str = time.strftime('%Y-%m-%d__%H-%M-%S', time.localtime()) # 获取当前时间
print("记录日志,结束时间:", time_str)
path = log_path + time_str + ".txt"
threading.Thread(target=self.log_temp(path)).start()
def log_temp(self, path):
os.system("adb logcat > " + path)
# 清空 img 和 log 目录
def clear_img_log(self, clear):
if clear and os.path.exists(screen_shot_path):
shutil.rmtree(screen_shot_path) # 清空 img 目录
if not os.path.exists(screen_shot_path):
os.mkdir(screen_shot_path) # 创建 img 目录
if clear and os.path.exists(log_path):
shutil.rmtree(log_path) # 清空 log 目录
if not os.path.exists(log_path):
os.mkdir(log_path) # 创建 log 目录
2)配置文件
app.cfg
# app 配置文件
# 系统控件
[system]
package = "com.android.systemui"
edt_password = "com.android.systemui:id/fixedPinEntry" # 输入密码框
key = "com.android.systemui:id/key"
pos_capsule = (100, 16)
# 启动控件
[launcher]
package = "com.huawei.android.launcher"
btn_clear = "com.huawei.android.launcher:id/clear_all_recents_image_button" # 清除所有应用按钮
# 备忘录
[note]
package = "com.huawei.notepad"
btn_new = "com.huawei.notepad:id/fab_add" # 新建按钮
edt_text = "com.huawei.notepad:id/editor_view" # 编辑框
# 图库
[photos]
package = "com.android.gallery3d"
# QQ
[qq]
package = "com.tencent.mobileqq"
btn_login = "com.tencent.mobileqq:id/login" # 登录按钮
img_icon = "com.tencent.mobileqq:id/icon" # 最近联系人头像
edt_input = "com.tencent.mobileqq:id/input" # 消息编辑框
btn_send = "com.tencent.mobileqq:id/fun_btn" # 发送按钮
btn_send_my = "com.tencent.mobileqq:id/imy" # “我的电脑”中发送按钮
# 微信
[wechat]
package = "com.tencent.mm"
# 腾讯视频
[tencent_video]
package = "com.tencent.qqlive"
注意:QQ 中“我的电脑”界面【发送】按钮的 id(imy)与联系人聊天界面的【发送】按钮的 id(fun_btn)不一样。
3)应用配置解析器(Parser)
Parser 类用于根据 app.cfg 文件中的 key 值,获取其 value 值,并定位到控件(获取控件句柄)。
parser.py
from configparser import ConfigParser
import os
# 应用解析器
class Parser:
# 初始化(device 为设备句柄, size 为手机分辨率)
def __init__(self, device, size):
print("应用解析器初始化...")
global d, conf, phone_size
d = device
phone_size = size
conf = ConfigParser()
path = os.path.abspath(os.path.abspath(os.path.dirname(__file__)) + "\\..\\config\\app.cfg")
conf.read(path, encoding='utf-8')
# 解析包名(app_name:应用名)
def get_package(self, app_name):
print("解析包名,app_name:", app_name)
conf_value = conf.get(app_name, "package")
package_name = eval(conf_value)
if package_name == "":
print("应用不存在,app_name:", app_name)
return package_name
# 解析元素(app_name:应用名,item:控件标识,type:控件标识类型)
def get_element(self, app_name, item, type="id"):
print("解析元素,app_name:", app_name, ",item:", item, ",type:", type)
conf_value = conf.get(app_name, item)
value = eval(conf_value)
element = d(resourceId="None")
if type == "id":
element = d(resourceId=value)
elif type == "text":
element = d(text=value)
if not element.exists:
print("元素不存在,item:", item)
return element
# 解析同类元素(app_name:应用名,item + num:控件标识,type:控件标识类型)
def get_element_item_num(self, app_name, item, num, type="id"):
print("解析元素,app_name:", app_name, ",item_num:", item + num, ",type:", type)
conf_value = conf.get(app_name, item)
value = eval(conf_value) + num
if type == "id":
element = d(resourceId=value)
elif type == "text":
element = d(text=value)
if not element.exists:
print("元素不存在,item:", item)
return element
# 解析元素(app_name:应用名,item + num:控件标识,type:控件标识类型)
def get_coord(self, app_name, item, relative = "left_top"):
print("解析元素坐标,app_name:", app_name, ",item_num:", item, ",relative:", relative)
conf_value = conf.get(app_name, item)
rela_x, rela_y = eval(conf_value)
print("相对坐标:(", rela_x, ", ", rela_y, ")")
abs_x = rela_x
abs_y = rela_y
if relative == "left_top":
abs_x = rela_x
abs_y = rela_y
elif relative == "right_top":
abs_x = phone_size[0] + rela_x
abs_y = rela_y
elif relative == "left_bottom":
abs_x = rela_x
abs_y = phone_size[1] + rela_y
elif relative == "right_bottom":
abs_x = phone_size[0] + rela_x
abs_y = phone_size[1] + rela_y
print("绝对坐标:(", abs_x, ", ", abs_y, ")")
return abs_x, abs_y
4)图片匹配器(Matcher)
图片匹配器(Matcher)用于在当前手机界面匹配目标图片,并返回匹配到的中心位置。
matcher.py
import aircv as ac
import os
# 图片匹配器
class Matcher:
# 初始化
def __init__(self):
print("图片匹配器初始化...")
global path
path = os.path.abspath(os.path.abspath(os.path.dirname(__file__)) + "\\..\\match") + "\\"
# 获取控件坐标(target: 目标图片, threshold: 可信阈值)
def get_coord(self, target, threshold = 0.6):
os.system("adb shell screencap -p /sdcard/bg.png")
os.system("adb pull /sdcard/bg.png " + path)
imsrc = ac.imread(path + "bg.png")
imobj = ac.imread(path + target + ".png")
res = ac.find_template(imsrc, imobj, threshold = threshold)
if res == None:
print("图片匹配失败")
return None
else:
pos_f = res['result']
pos = (int(pos_f[0]), int(pos_f[1]))
print("图片匹配成功,res:", res)
return pos
5)模板类(Template)
模板类(Template)为测试类(MyTest)提供模板,所有测试类都继承模板类,测试类只需重写 first 和 circle_body 方法,即可实现各种复杂的挂机任务。
template.py
from base.phone import Phone
from time import sleep
# 模板类(为测试类提供模板,所有测试类都继承此类,测试类只需重写 first 和 circle_body 方法)
class Template:
def __init__(self, connect_type="usb", wait_time=1, log=False):
print("Template 初始化...")
self.phone = Phone(connect_type, wait_time)
self.log = log
# 设置测试初始状态
def first(self):
print("设置测试初始状态...")
# 设置循环体
def circle_body(self):
print("设置循环体...")
# 开始循环
def loop(self, max_times=1000000000):
print("开始循环...")
i = 1;
while i <= max_times:
print("********************", i, "********************")
# try:
self.circle_body()
# except BaseException:
# if self.log:
# self.phone.log()
# raise Exception("循环异常...")
i = i + 1
sleep(3)
# 主调用方法
def main(self):
self.first()
self.loop()
6)测试类(MyTest)
用户需要挂机的具体任务,可以写在此类中,每个任务建一个测试文件,如 qq_test.py(QQ测试任务)、notepad_test.py(备忘录测试任务),各个测试类中都继承了 Template 类,并重写其 first() 方法和 circle_body() 方法。
test.py
from base.template import Template
# 测试类(类名不要以 Test 开头)
class MyTest(Template):
def __init__(self):
print("Test 初始化...")
super().__init__(connect_type="usb", wait_time=2, log=True)
self.phone.unlock("123456") # 解锁
# 设置测试初始状态
def first(self):
print("执行测试初始状态...")
self.phone.start_app("note") # 打开备忘录
self.phone.start_app("photos") # 打开图库
self.phone.clear_all_app() # 清除所有打开的应用
# 设置循环体
def circle_body(self):
print("执行循环体...")
self.phone.start_app("qq") # 打开 QQ
self.phone.click("qq", "img_icon", 0) # 点击最近联系人中第1个
self.phone.set_text("qq", "edt_input", "我正在做自动化测试,请忽略此消息") # 编辑消息
self.phone.click("qq", "btn_send_my") # 点击发送,这里是给【我的电脑】发送,请将【我的电脑】置顶
self.phone.stop_app("qq") # 关闭 QQ
if __name__ == '__main__':
test = MyTest()
test.main()
声明:本文转自Android 自动化测试项目
Android 自动化测试项目的更多相关文章
- Android开源项目分类汇总
目前包括: Android开源项目第一篇——个性化控件(View)篇 包括ListView.ActionBar.Menu.ViewPager.Gallery.GridView.ImageView. ...
- 使用 flow.ci 实现 Android 自动化测试与持续集成
在上篇文章--如何实现 Android 应用的持续部署中,我们使用的是 flow.ci + Github + fir.im 实现 Android 应用的持续部署.对于 Android 开发者,他们可能 ...
- fir.im Weekly - 从零开始创建 Android 新项目
今年的 Google I/O 大会上,人工智能和虚拟现实的产品发布让我们对未来多了几分惊喜.对于开发者部分,Google 发布了 Android N 系统,感受最深的是全新的 Android Stud ...
- Android开源项目(二)
第二部分 工具库 主要包括那些不错的开发库,包括依赖注入框架.图片缓存.网络相关.数据库ORM建模.Android公共库.Android 高版本向低版本兼容.多媒体相关及其他. 一.依赖注入DI 通过 ...
- GitHub上史上最全的Android开源项目分类汇总 (转)
GitHub上史上最全的Android开源项目分类汇总 标签: github android 开源 | 发表时间:2014-11-23 23:00 | 作者:u013149325 分享到: 出处:ht ...
- 【Android】Android开源项目分类汇总
第一部分 个性化控件(View) 主要介绍那些不错个性化的View,包括ListView.ActionBar.Menu.ViewPager.Gallery.GridView.ImageView.Pro ...
- 解放双手——Android自动化测试
解放程序猿宝贵的右手(或者是左手) http://blog.csdn.net/eclipsexys/article/details/45622813 --Android自动化测试技巧 Google大神 ...
- Python+Selenium进行UI自动化测试项目中,常用的小技巧2:读取配置文件(configparser,.ini文件)
在自动化测试项目中,可能会碰到一些经常使用的但 很少变化的配置信息,下面就来介绍使用configparser来读取配置信息config.ini 读取的信息(config.ini)如下: [config ...
- Android开源项目汇总【转】
主要介绍那些不错个性化的View,包括ListView.ActionBar.Menu.ViewPager.Gallery.GridView.ImageView.ProgressBar.TextView ...
- GitHub上史上最全的Android开源项目分类汇总
今天在看博客的时候,无意中发现了 @Trinea 在GitHub上的一个项目 Android开源项目分类汇总 ,由于类容太多了,我没有一个个完整地看完,但是里面介绍的开源项目都非常有参考价值,包括很炫 ...
随机推荐
- 一个容易弄错的.textContent和text()问题
最近在写一些前端页面,遇到需要通过点击事件获取被点击对象的文本内容,正确代码如下: <!DOCTYPE html> <html lang="en"> < ...
- 例2.6 设计一个高效的算法,从顺序表L中删除所有值为x的元素,要求时间复杂度为0(n)空间复杂度为0(1)。
1.题目 例2.6 设计一个高效的算法,从顺序表L中删除所有值为x的元素,要求时间复杂度为0(n)空间复杂度为0(1). 2.算法思想 3.代码 void DeleteX(SeqList LA, Se ...
- 电机控制和Linux驱动开发哪个方向更好呢?
电机控制和Linux驱动开发哪个方向更好呢? 先说结论:任何一个领域,就像世间的五行,阴阳结合,虚实结合,利弊结合.对于哪个更好,不能一概而论,最重要的是要搞清楚,你更适合哪个? 1.共鸣 当我看到这 ...
- http-长连接
1. 短链接 http1.0 -- 1个请求-响应过程会创建且1个新的连接 2. 长连接 http1.1 -- 同域下可以创建1个tcp连接,多个请求在同一个tcp上串行处理请求 http2.0 -- ...
- [转帖]Jmeter正则提取器常用的几种方式
https://www.cnblogs.com/a00ium/p/10483741.html 使用jmeter的同学都知道,jmeter提供了各种各样的提取器,如jsonpath.Beanshell. ...
- 【转帖】Linux查看二进制文件:一招制敌(linux二进制查看文件)
https://www.dbs724.com/146055.html 一招制敌:学会Linux查看二进制文件 在Linux操作系统中,二进制文件是一种常见的文件类型.如果你想深入了解一个二进制文件,可 ...
- [转帖]深入理解mysql-第十章 mysql查询优化-Explain 详解(上)
目录 一.初识Explain 二.执行计划-table属性 三.执行计划-id属性 四.执行计划-select_type属性 一条查询语句在经过MySQL查询优化器的各种基于成本和规则的优化会后生成一 ...
- [转帖]一文解决内核是如何给容器中的进程分配CPU资源的?
https://zhuanlan.zhihu.com/p/615570804 现在很多公司的服务都是跑在容器下,我来问几个容器 CPU 相关的问题,看大家对天天在用的技术是否熟悉. 容器中的核是真 ...
- 【转帖】FIO磁盘性能测试工具
https://www.jianshu.com/p/70b8c7d5d217 FIO工具介绍 FIO 工具是一款用于测试硬件存储性能的辅助工具,兼具灵活性.可靠性从而从众多性能测试工具中脱颖而出.磁盘 ...
- [转帖]AMD第四代宵龙 9174F 亮眼
https://www.amd.com/zh-hans/processors/epyc-9004-series#%E8%A7%84%E6%A0%BC 型号规格 型号 CPU 核心数量 线程数量 最 ...