【测试】Python手机自动化测试库uiautomator2和weditor的详细使用
1.说明
我们之前在电脑操作手机进行自动化测试,基本上都是通过Appium的,这个工具确实强大,搭配谷歌官方的UiAutomator基本上可以完成各种测试,但缺点也很明显,配置环境太麻烦了,需要jdk、sdk等,后来有人在UiAutomator的基础上使用http请求rpc服务的方式做了一个uiautomator,但这个项目很久没有维护了,后面有人在此基础上进行重构和精简,所以又有了uiautomator2,这是目前为止比较好用的Python操作手机的测试库之一
2.安装环境
2.1 安装uiautomator2
使用uiautomator2的要求是,Android版本 4.4或以上版本,Python 3.6或以上版本,社区反馈3.8.0不支持, 但是3.8.2支持
pip install uiautomator2
2.2 安装weditor
uiautomator2是控制手机的, 还需要一个查看手机元素的库,与uiautomator2配套的是weditor。
注意,如果你安装0.6.5或以上版本的weditor可能会遇到编码错误
UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xad in position 825: illegal multibyte sequence。所以,你可以选择0.6.4版本
pip install weditor==0.6.4
或者可以把Python环境的默认编码改成utf-8(在cmd执行)
set PYTHONUTF8=1
2.3 安装adb
adb的作用是连接安卓设备,比如说查看当前已连接上电脑的设备,我们可以在android官网下载platform-tools,里面包含了adb程序,下载之后无需安装,进行解压就能用,要是觉得不方便使用的话你可以把adb.exe添加到系统环境变量
https://developer.android.com/studio/releases/platform-tools
3.初始化
首先使用adb查看你的手机是否已连接上电脑(需要打开开发者选项),会显示设备序列号
adb.exe devices
之后初始化uiautomator2 ,它会在你的手机上安装一个叫做atx-agent的应用
uiautomator2 init
我这里使用的是模拟器,大概结果如下图,初始化成功的话后面会有Successfully init AdbDevice(serial=XXX)
的提示
4.uiautomator2连接设备
通过USB连接
import uiautomator2 as u2d = u2.connect('emulator-5554') # emulator-5554就是在adb devices显示的序列号
print(d.info)
通过WiFi连接(确保手机与电脑处于同一个局域网并且能ping同手机)
import uiautomator2 as u2
d = u2.connect('192.168.0.100')
通过adbWiFi连接
import uiautomator2 as u2d = u2.connect_adb_wifi("192.168.0.101:5555")
5.weditor定位元素
现在已经可以通过uiautomator2操作手机了,如果想要定位元素的话,刚刚安装好的weditor就可以派上用场了,在cmd输入weditor,它会自动打开浏览器
weditor
我们在打开的网页上面输入设备号或者ip,点击“Connect”,如果“Connect”按钮出现一个绿色的树叶图标说明已经连接上。再往右一点有一个“Dump hierarchy”按钮,点击一下就刷新页面了,你也可以打开“实时”选项,这样它会一直刷新页面。下面的窗口会显示当前手机的页面,点击一下某个控件就能显示它的信息,右边还有一个写测试代码的窗口,虽然我们一般都是通过Python控制,但Python端每次运行都需要花费时间,可以先在网页端测试一下定位是否准确
6.操作APP
6.1 查看APP包名
对于操作APP,基本上都是通过包名和activity控制的,比如说打开关闭、安装卸载等操作,所以获取包名是第一步,你可以通过weditor查看某个应用的包名,也可以通过uiautomator2获取
import uiautomator2 as u2d = u2.connect("emulator-5554")# 获取所有包名,返回一个列表
all_pkg_list = d.app_list()
print(len(all_pkg_list), all_pkg_list)# 获取正在运行的APP的包名
running_pkg_list = d.app_list_running()
print(len(running_pkg_list), running_pkg_list)
6.2 获取包信息
info = d.app_info("com.android.settings")
print(info)
# 获取到的信息如下
# {'packageName': 'com.android.settings', 'mainActivity': 'Settings', 'label': 'Settings', 'versionName': '9', 'versionCode': 28, 'size': 49083357}
6.3 启动停止APP
# 直接启动APP,若wait设置为True则一直等到启动结束
d.app_start("com.android.settings", wait=True)# 等待APP启动结束,默认20秒超时,启动后 返回pid,如果启动失败则pid为0
pid = d.app_wait("com.android.settings", timeout=20)
print(pid)# 停止APP
d.app_stop("com.android.settings")
# 停止所有APP,excludes参数可以指定哪些APP不停止
d.app_stop_all(excludes=["com.android.browser", "com.android.bluetooth"])
6.4 安装卸载APP
# 安装APP,参数可以是本地文件也可以是url
d.app_install(r"C:\Users\admin\Desktop\IcyFtpServer_v1.0.apk")# 卸载APP
d.app_uninstall("com.ice.icyftpserver")
# 卸载所有APP,excludes参数指定要保留的APP
# d.app_uninstall_all(excludes=["app_uninstall_all"])
7.操作元素
当打开APP之后,我们就可以操作APP了,包括定位元素,点击、长按等操作
7.1 选择器
定位元素很重要,只有获取到某个元素才可以操作。定位元素的方式有多种,比如说index、resourceId、className、text、textContains等,其中比较准确的是通过index和resourceId进行定位(速度很快),index、resourceId和className的值都可以在weditor看到。text是指元素的全部文本,textContains是指包含部分文本,textStartsWith是指以某文本开始,这几种选择器都会搜索页面,所以速度会比较慢
# 通过index定位
d(index=1).click()# 通过资源id定位
d(resourceId="com.android.settings:id/search_action_bar_title").click()# 通过该className定位
d(className="android.widget.TextView").click()# 通过文本定位,速度比较慢
d(text="在设置中搜索").click()
# 包含XXX文本
d(textContains="在设置中搜索").click()
# 以XXX开始的文本
d(textStartsWith="在设置中").click()
我们注意到,除了index和resourceId可以准确定位到唯一的元素,其他选择器都可能定位到多个元素,为了提高准确率,我们可以同时使用多个选择器
# 同时使用多个选择器提供准确率
d(className="android.widget.TextView", textContains="在设置中").click()
谷歌官网给出的选择还有很多,感兴趣的话可以看一下
https://developer.android.com/reference/android/support/test/uiautomator/UiSelector
选择选择器返回的是一个UiObject对象,但要注意,即使没有定位到,它也不会报错,直到你进行点击等操作的时候才会(超时)报错
UiObject可以同时包含多个元素,即它是一个容器,因此,我们可以查看该对象包含多少个元素,并且可以通过遍历的方式依次访问选中的元素
selected_el = d(resourceId="com.android.settings:id/search_action_bar_title")
# 查看元素的格式
print(selected_el.count)
print(len(selected_el))# 访问元素
print(selected_el[0])
for el in selected_el:print(el)# 只要第一个元素
selected_el = d(className="android.widget.TextView", instance=0)
print(len(selected_el))
另外,也可以通过当前选择的元素选择子元素或者兄弟元素,目前不支持选择父元素
# 通过child()选择子元素
d(className="android.widget.LinearLayout").child(text="网络和互联网")
# 通过sibling()选择兄弟元素
d(className="android.widget.LinearLayout").sibling(className="android.widget.ImageView")# 选择上面的元素
d(resourceId="android:id/title").up()
# 选择下面的元素
d(resourceId="android:id/title").down()
# 选择左边的元素
d(resourceId="android:id/title").left()
# 选择右边的元素
d(resourceId="android:id/title").right()
7.2 元素信息
通过选择器定位元素之后,可以调用一下exists属性看是否真的存在,如果存在再进行其他操作,如果想知道元素的边界、中心点的位置也是可以获取到的
selected_el = d(resourceId="com.android.settings:id/search_action_bar_title")
# 判断元素是否存在
if selected_el.exists:# 元素的信息,与在weditor上看到的基于一致print("info:", selected_el.info)# 元素边界,左上点和右下点的x、yprint(selected_el.bounds())# 元素的中心位置print(selected_el.center())# 元素个数,即len(selected_el)print(selected_el.count)
7.3 点击和长按
一般来说,获取到元素之后再进行点击或者长按操作
selected_el = d(resourceId="com.android.settings:id/search_action_bar_title")
# 点击元素,timeout是等待元素出现超时时间,offset是点击位置的偏移量
selected_el.click(timeout=None, offset=None)
# 长按元素,duration长按事件,默认是0.5秒,timeout是等待元素出现的超时时间
selected_el.long_click(duration=0.5, timeout=None)
# 一直点击直到元素小时,maxretry是最大点击次数,默认是10,interval是每次点击间隔,默认是1秒
selected_el.click_gone(maxretry=10, interval=1.0)
如果你不想通过定位到的元素进行点击,而是通过指定的x、y位置,也是可以的
x, y = 384.0, 191.5
# 点击
d.click(x, y)
# 长按
d.long_click(x, y, duration=0.5)
# 双击
d.double_click(x, y, duration=0.1)
7.4 设置和清除文本
除了点击,还经常需要在文本框输入文本或者清除文本,或者获取文本的内容
d.app_start("com.android.settings", wait=True)
d(resourceId="com.android.settings:id/search_action_bar_title").click()
selected_el = d(resourceId="android:id/search_src_text")
# 设置文本
selected_el.set_text("wifi")
# 获取文本
print(selected_el.get_text())
# 清除文本
selected_el.clear_text()
print(selected_el.get_text())
7.5 滑动操作
可以调用swipe()方法进行滑动,支持上下左右四个方向,它需要两个参数,direction是方向,支持up、down、left、right,steps是长度,默认值是10(一步大概5毫秒)
d(resourceId="com.android.settings:id/main_content").swipe(direction="up", steps=10)
基于时间的滚动感觉不是很好控制,所以还可以调用swipe_ext()方法,控制滑动比例,比如说向上滑动80%的距离
d.swipe_ext("up", scale=0.8)
8.屏幕操作
8.1 屏幕分辨率
d.info可以获取到屏幕分辨率和物理分辨率,也调用window_size()方法直接返回屏幕分辨率
print(d.info)
print(d.window_size())
8.2 截屏
如果看到一个漂亮的界面,可以选择截个图。可以调用screenshot()方法截图,第一个参数是图片保存的路径,第二个参数是处理图片的库,默认是pillow(也支持opencv),如果你没有安装pillow或opencv,那就指定第二个参数为raw
# 使用pillow
d.screenshot("./test.jpg")# 使用opencv
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)# 直接保存源数据
img = d.screenshot(format='raw')
with open("test.jpg", "wb") as f:f.write(img)
8.3 息屏亮屏
我们可以锁屏和亮屏,可以在info信息里面找到当前是息屏还是亮屏
# 亮屏
d.screen_on()
# 屏幕状态
print(d.info.get('screenOn')) # True
# 锁屏
d.screen_off()
print(d.info.get('screenOn')) # False# 解锁屏幕,实际过程是先按下power键再滑动屏幕
d.unlock()
8.4 自动旋转屏幕
# 设置屏幕反向
d.set_orientation("left")
time.sleep(2)
d.set_orientation("right")
time.sleep(2)
d.set_orientation("natural")
time.sleep(2)
# 自动旋转屏幕
d.freeze_rotation(True)
9.按键事件
目前uiautomation2支持的按键还不是很多,大概有home, back, left, right, up, down, center, menu, search, enter, delete(or del), recent(recent apps), volume_up, volume_down, volume_mute, camera, power.
d.press("home") # 主页键
d.press("recent") # 近期任务键
d.press("back") # 返回键
d.press("power") # 电源键
d.press("menu") # 菜单键
d.press("volume_up") # 音量+键
d.press("volume_down") # 音量-键
d.press("volume_mute") # 静音键
除了上面的press某些按键之外,也可以调出键盘,然后发送按键。不过,如果是输入框输入文本,还是建议使用set_text()更方便
# 依次键入a、b、c、d、e、f、g键
d(resourceId="android:id/search_src_text").send_keys("abcdefg")
# 直接把输入框文本设置成abcdefg
d(resourceId="android:id/search_src_text").set_text("abcdefg")
10.上传下载文件
有时候需要把电脑端的文件上传到手机端或者从手机端下载到电脑端。不过一定要注意,存储位置一定要存在并且有读写权限
# 上传到手机端
d.push(r"C:\Users\admin\Desktop\test.txt", "/storage/emulated/0/")
# 从手机端下载
d.pull("/storage/emulated/0/test.txt", "test2.txt")
11.执行shell命令
如果uiautomator2的操作都不能满足你的需求,你也可以直接通过adb执行原生shell命令,支持传参
# 调用shell命令,执行成功时返回码是0
output, exit_code = d.shell("pwd", timeout=60)
print(output, exit_code)# 当stream为True,则只返回Response对象
output = d.shell("pwd", stream=True)
print(output.text)# 如果该命令有参数,可以使用列表的方式
output, exit_code = d.shell(["ls", "-l"])
print(output, exit_code)
相关文章:
【测试】Python手机自动化测试库uiautomator2和weditor的详细使用
1.说明 我们之前在电脑操作手机进行自动化测试,基本上都是通过Appium的,这个工具确实强大,搭配谷歌官方的UiAutomator基本上可以完成各种测试,但缺点也很明显,配置环境太麻烦了,需要jdk、sdk等,…...
《NFL橄榄球》:旧金山49人·橄榄1号位
旧金山四九人(San Francisco 49ers,又译旧金山淘金者) 是美国全国橄榄球联盟球队。成立于1946年,最初作为全美橄榄球联合会(AAFC)的一员参加比赛,后于1950年与克利夫兰布朗一同加入由美国橄榄球联合会合并而成的NFL。现任主教练为…...
spark为什么比hadoop快
网上一堆人根本对计算框架一知半解就出来糊弄人,常见解答有: spark是基于内存计算,所以快。这跟废话似的,mr计算的时候不也是基于内存? mr shuffle落盘。这也是胡扯, spark shuffle不落盘? 实际…...
跨境人都在用的指纹浏览器到底有什么魔力?三分钟带你了解透彻
什么是指纹浏览器?这是东哥近期收到最多的粉丝私信咨询,指纹两个字大家都很熟悉,指纹浏览器就变得陌生起来。之前东哥也跟大家分享过很多次指纹浏览器的用法,鉴于还是很多人不认识这个好用的工具,东哥今天就来详细给大…...
机器学习概述
机器学习是人工智能的核心研究领域之一,其研究动机是为了让计算机系统具有人的学习能力以便实现人工智能。目前被广泛采用的机器学习的定义是“利用经验来改善计算机系统自身的性能”。由于“经验在计算机系统中主要是以数据的形式存在的,因此机器学习需…...
企业网站自动生成系统的设计和实现
技术:Java、JSP等摘要:随着Internet技术的发展,人们的日常生活已经离不开网络。未来社会人们的生活和工作将越来越依赖于数字技术的发展,越来越数字化、网络化、电子化、虚拟化。Internet的发展历程以及目前的应用状况和发展趋势&…...
sikuli+eclipse对于安卓app自动化测试的应用
Sikuli是什么? 下面是来自于官网的介绍:Sikuli is a visual technology to automate and test graphical user interfaces (GUI) using images (screenshots). Sikuli includes Sikuli Script, a visual scripting API for Jython, and Sikuli IDE, an …...
react源码分析:babel如何解析jsx
同作为MVVM框架,React相比于Vue来讲,上手更需要JavaScript功底深厚一些,本系列将阅读React相关源码,从jsx -> VDom -> RDOM等一些列的过程,将会在本系列中一一讲解 工欲善其事,必先利其器 经过多年的…...
搜广推 WideDeep 与 DeepCrossNetwork (DCN) - 记忆+泛化共存
😄 这节来讲讲Wide&Deep与Deep&CrossNetwork (DCN)。从下图可看出WD非常重要,后面衍生出了一堆WD的变体。本节要讲的WD和DCN结构都非常简单,但其设计思想值得学习。 🚀 wide&deep:2016年,谷歌提出。 🚀 Deep&CrossNetwork (DCN):2017年,谷歌和斯坦…...
项目管理工具dhtmlxGantt甘特图入门教程(十四):导出/导入 Excel到 iCal
这篇文章给大家讲解利用dhtmlxgantt导入/导出Excel到iCal的操作方法。 dhtmlxGantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表,可满足应用程序的所有需求,是完善的甘特图图表库 DhtmlxGantt正版试用下载(qun;765665…...
k-means聚类总结
1.概述 聚类算法又叫做‘无监督学习’,其目的是将数据划分成有意义或有用的组(或簇)。 2.KMeans 关键概念:簇与质心 KMeans算法将一组N个样本的特征矩阵X划分为K个无交集的簇,直观上来看是簇是一组一组聚集在一起的…...
char * 和const char *的区别
一、含义的不同 char* 表示一个指针变量,并且这个变量是可以被改变的。 const char*表示一个限定不会被改变的指针变量。 二、模式的不同 char*是常量指针,地址不可以改变,但是指针的值可变。 const char*是指向常量的常量指针ÿ…...
【剑指offer】JZ3 数组中重复的数字、 JZ4 二维数组中的查找
目录 JZ3 数组中重复的数字 思路: 解题步骤: JZ4 二维数组中的查找 思路 JZ3 数组中重复的数字 描述: 在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每…...
数据采集 - 笔记
1 redis GitHub - redis/redis: Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, Streams, HyperLogLogs, Bitmaps. Redis 通常被称为数…...
8年测开经验面试28K公司后,吐血整理出高频面试题和答案
#01、如何制定测试计划? ❶参考点 1.是否拥有测试计划的制定经验 2.是否具备合理安排测试的能力 3.是否具备文档输出的能力 ❷面试命中率 80% ❸参考答案 测试计划包括测试目标、测试范围、测试环境的说明、测试类型的说明(功能,安全&am…...
spring读取properties顺序,重复key问题
最近搞个开源工具,涉及到配置问题。 举例 有个应用A工具,打成jar给人用。应用B引用了A的jar A应用里resources/sys.properties文件里有个coreSize1 B引用了A,期望修改coreSize的值,改成2 开始天真以为,B应用里有同…...
什么是api接口?(基本介绍)
API:应用程序接口(API:Application Program Interface) 应用程序接口是一组定义、程序及协议的集合,通过 API 接口实现计算机软件之间的相互通信。API 的一个主要功能是提供通用功能集。程序员通过调用 API 函数对应用程序进行开发,可以减轻编程任务。 …...
【2023全网最全教程】从0到1开发自动化测试框架(建议收藏)
一、序言 随着项目版本的快速迭代、APP测试有以下几个特点: 首先,功能点多且细,测试工作量大,容易遗漏;其次,代码模块常改动,回归测试很频繁,测试重复低效;最后&#x…...
3-5天炒股短线战法指标思想结合----超级短线源码无未来
超级短线以3-5个交易日获利3-5个点为目标,经过长期总结、实践、实盘操作编写的一个短线指标和思想! 如果你认为这一个指标像股市提款机一个,可以随意的赚钱,请你不要购买; 如果你你购买了指标又不想思考分析,想随意的赚…...
原始GAN-pytorch-生成MNIST数据集(代码)
文章目录原始GAN生成MNIST数据集1. Data loading and preparing2. Dataset and Model parameter3. Result save path4. Model define6. Training7. predict原始GAN生成MNIST数据集 原理很简单,可以参考原理部分原始GAN-pytorch-生成MNIST数据集(原理&am…...
注意,这些地区已发布2023年上半年软考报名时间
距离2023年上半年软考报名越来越近了,目前已有山西、四川、山东等地区发布报名简章,其中四川3月13日、山西3月14日、山东3月17日开始报名。 四川 报名时间:3月13日至4月3日。 2.报名入口:https://www.ruankao.org.cn/ 缴费时间…...
Html引入外部css <link>标签 @import
Html引入外部css 方法1: <link rel"stylesheet" href"x.css"> <link rel"stylesheet" href"x.css" /><link rel"stylesheet" href"x.css" type"text/css" /><link rel"sty…...
React源码分析8-状态更新的优先级机制
这是我的剖析 React 源码的第二篇文章,如果你没有阅读过之前的文章,请务必先阅读一下 第一篇文章 中提到的一些注意事项,能帮助你更好地阅读源码。 文章相关资料 React 16.8.6 源码中文注释,这个链接是文章的核心,文…...
如何在ChatGPT的API中支持多轮对话
一、问题 ChatGPT的API支持多轮对话。可以使用API将用户的输入发送到ChatGPT模型中,然后将模型生成的响应返回给用户,从而实现多轮对话。可以在每个轮次中保留用户之前的输入和模型生成的响应,以便将其传递给下一轮对话。这种方式可以实现更…...
华为OD机试模拟题 用 C++ 实现 - 猜字谜(2023.Q1)
最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 最多获得的短信条数(2023.Q1)) 文章目录 最近更新的博客使用说明猜字谜题目输入输出描述备注示例一输入输出示例二输入输出思路Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,...
Containerd容器运行时将会替换Docker?
文章目录一、什么是Containerd?二、Containerd有哪些功能?三、Containerd与Docker的区别四、Containerd是否会替换Docker?五、Containerd安装、部署和使用公众号: MCNU云原生,欢迎微信搜索关注,更多干货&am…...
java虚拟机中对象创建过程
java虚拟机中对象创建过程 我们平常创建一个对象,仅仅只是使用new关键字new一个对象,这样一个对象就被创建了,但是在我们使用new关键字创建对象的时候,在java虚拟机中一个对象是如何从无到有被创建的呢,我们接下来就来…...
3485. 最大异或和
Powered by:NEFU AB-IN Link 文章目录3485. 最大异或和题意思路代码3485. 最大异或和 题意 给定一个非负整数数列 a,初始长度为 N。 请在所有长度不超过 M的连续子数组中,找出子数组异或和的最大值。 子数组的异或和即为子数组中所有元素按位异或得到的…...
SpringBoot:SpringBoot配置文件.properties、.yml 和 .ymal(2)
SpringBoot配置文件1. 配置文件格式1.1 application.properties配置文件1.2 application.yml配置文件1.3 application.yaml配置文件1.4 三种配置文件优先级和区别2. yaml格式2.1 语法规则2.2 yaml书写2.2.1 字面量:单个的、不可拆分的值2.2.2 数组:一组按…...
QT 学习之QPA
QT 为实现支持多平台,实现如下类虚函数 Class Overview QPlatformIntegration QAbstractEventDispatcherQPlatformAccessibilityQPlatformBackingStoreQPlatformClipboardQPlatformCursorQPlatformDragQPlatformFontDatabaseQPlatformGraphicsBufferQPlatformInput…...
唯美网站建设/现在学seo课程多少钱
今天的调式代码的时候,想找到网站的Hover样式,可是找了很长时都都没有找到。之后在百度一下,才明白当打开网页的时候,默认是非hover样式,如果需要看hover样式,需要进行勾选一下。勾选之后,才是h…...
网站建设教程视频/2022年seo还值得做吗
对w3wp.exe的一点认识!很想看一个网站的内容,但是这个网站没有RSS,还得整天的去打这个网站!感觉比较郁闷,正好今天有时间,你不能提供订阅,我自己来生成RSS,自己来搞定订阅的问题. 实现这个功能的思路比较简单:获取该网址的Html源代码,通过正则表达式过滤出有用的数据,将这些有用…...
广东省网站建设网站/百度推广运营公司
还记得小菜,上篇介绍的Hyper-V Manager command line吗?我们继续来说它..大家对.RDP文件因该很熟悉,我们可不可以让向运行.RDP文件的方式去连接本地或者远程的虚拟机呢?答案是可以的.其实篇最后有提到过.这次同样利用这个命令行工具.在桌面创建个快捷方式.用以下命令.c:\Prog…...
wordpress图片轮播插件/网站维护的内容有哪些
1、使用 gitlab 创建合并请求 master 合并到 branch_test --> compare branches and continue --> submit merge request 提示:合并冲突。 2、使用 idea 打开项目。 (1)执行:git fetch origin (2)切换到 master 分支,更新到最新版本。 (3)切换到 branch_test 分支,更…...
怎么样将网站内容做的漂亮/seo站内优化包括
2019独角兽企业重金招聘Python工程师标准>>> 在objective-c中要实现一个单例类,至少需要做以下四个步骤: 1、为单例对象实现一个静态实例,并初始化,然后设置成nil, 2、实现一个实例构造方法检查上面声明的静…...
wordpress导航栏目/软文范例100字以内
音箱由哪几部分组成? 市面上的音箱形形色色,但无论哪一种,都是由喇叭单元(术语叫扬声器单元)和箱体这两大最基本的部分组成,另外,绝大多数音箱至少使用了两只或两只以上的喇叭单元实行所谓的多…...