uiautomator2 库中文手册


# 可以完成 Android UI 自动化的 Python 库, 比较详细的文档

UiAutomator是Google提供的安卓自动化测试Java库,功能很强,但测试脚本只能使用Java语言,脚本要打包成jar或者apk包上传到设备上才能运行。
感谢 Xiaocong He (@xiaocong),他用Python编写了uiautomator,原理是在手机上运行了一个http rpc服务,将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库。因为xiaocong/uiautomator已经很久不更新。所以我们直接fork了一个版本,命名为uiautomator2,对原有的库的bug进行了修复,还增加了很多新的Feature。主要有以下部分:
设备和测试电脑可以脱离数据线,通过WiFi互联(基于atx-agent)
集成openstf/minicap达到实时屏幕投频,以及实时截图
集成openstf/minitouch达到精确实时控制设备
修复xiaocong/uiautomator经常性退出的问题
代码进行了重构和精简,方便维护
实现了一个设备管理平台(也支持iOS) atxserver2
这里要先说明下,因为经常有很多人问 openatx/uiautomator2 并不支持iOS测试,需要iOS自动化测试,可以转到这个库 openatx/facebook-wda。
PS: 这个库 https://github.com/NeteaseGame/ATX 目前已经不维护了,请尽快更换。
Android Uiautomator2 Python Wrapper 这是一个可以完成Android的UI自动化的python库。
该项目还在火热的开发中,QQ群号: 499563266(加入有收费)。

一、安装

1.安装uiautomator2;

# 由于uiautomator2仍在开发中,因此您必须添加‘--pre’才能安装开发版本。
pip install --pre uiautomator2
# 也可以从源代码安装
git clone https://github.com/openatx/uiautomator2
pip install -e uiautomator2

如果需要截屏,还要安装pillow

pip install pillow

2.安装设备守护进程;

电脑连接上一个手机或多个手机, 确保adb已经添加到环境变量中,执行下面的命令会自动安装本库所需要的设备端程序:uiautomator-server 、atx-agent、openstf/minicap、openstf/minitouch

# 初始化所有的已经连接到电脑的设备
python -m uiautomator2 init

有时候init也会出错,请参考手动Init
指南,安装提示success即可。

3.安装weditor

pip install -U weditor  # 目前最新的稳定版为 0.1.0

Windows系统可以使用命令在桌面创建一个快捷方式 python -m weditor --shortcut
命令行启动 python -m weditor 会自动打开浏览器,输入设备的ip或者序列号,点击Connect即可。
具体参考文章:浅谈自动化测试工具python-uiautomator2

二、使用指南

1. 连接设备

有3种连接方法:
• 通过WiFi
假如设备IP是10.0.0.1,你的电脑在同一网络

import uiautomator2 as u2

d = u2.connect('10.0.0.1') # alias for u2.connect_wifi('10.0.0.1')
print(d.info)

• 通过USB
假设设备编号为123456f(通过cmd:adb devices查询)

import uiautomator2 as u2

d = u2.connect('123456f') # alias for u2.connect_usb('123456f')
print(d.info)

• 通过ADB WiFi

import uiautomator2 as u2

d = u2.connect_adb_wifi("10.0.0.1:5555")
# Equals to 
# + Shell: adb connect 10.0.0.1:5555
# + Python: u2.connect_usb("10.0.0.1:5555")

无参数调用u2.connect()函数, uiautomator2将从环境变量ANDROID_DEVICE_IP或ANDROID_SERIAL获取设备IP。如果这个环境变量为空,uiautomator将退回connect_usb,你需要确保只有一个设备连接到电脑。

2. 命令行使用

其中的$device_ip代表设备的ip地址。
如需指定设备需要传入—serial,如python3 -m uiautomator2 --serial bff1234 , SubCommand为子命令(init,或者screenshot等)
1.0.3 Added: python3 -m uiautomator2可以简写为uiautomator2

  • init: 为设备安装所需要的程序
uiautomator2 init 
# If you need specify device to init, pass --serial <serial> 
python3 -m uiautomator2 init --serial your-device-serial
  • screenshot: 截图
$ python -m uiautomator2 screenshot screenshot.jpg
  • uninstall: 卸载
python -m uiautomator2 uninstall <package-name> # 卸载一个包
python -m uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包
python -m uiautomator2 uninstall --all # 全部卸载
  • install: 安装apk,apk通过URL给出 (暂时不能用)
  • clear-cache: 清空缓存 (废弃中,目前已经不需要改接口)
  • app-stop-all: 停止所有应用 (暂不能用)
  • healthcheck: 健康检查 (暂不能用)

三、API手册

1.全局设定

# 设置每次单击UI后再次单击之间延迟1.5秒
d.click_post_delay = 1.5  # 默认无延迟

# 设置默认元素等待超时(秒)
d.wait_timeout = 30.0 # 默认20.0秒

2.检索设备信息

d.info

以下是可能的结果:

{ 
    u'displayRotation': 0,
    u'displaySizeDpY': 640,
    u'displaySizeDpX': 360,
    u'currentPackageName': u'com.android.launcher',
    u'productName': u'takju',
    u'displayWidth': 720,
    u'sdkInt': 18,
    u'displayHeight': 1184,
    u'naturalOrientation': True
}

3.键盘操作

  • 打开/关闭屏幕
d.screen_on() # 开启 screen
d.screen_off() # 关闭screen
  • 获取屏幕开/关状态
d.info.get('screenOn') # 要求android >= 4.4
  • 按下硬/软键
d.press("home") # 按下home键
d.press("back") # 按下back键的常规方式
d.press(0x07, 0x02) # 按下编码 0x07('0') 和 0x02(META ALT)
  • 当前支持的按键
    o home
    o back
    o left
    o right
    o up
    o down
    o center
    o menu
    o search
    o enter
    o delete ( or del)
    o recent (recent apps)
    o volume_up
    o volume_down
    o volume_mute
    o camera
    o power

您可以在Android KeyEvnet
上找到所有按键代码定义。

  • 解锁屏幕
d.unlock()

# 1. 启动 activity: com.github.uiautomator.ACTION_IDENTIFY
# 2. 按下 "home"

4.设备的手势交互

  • 点击屏幕
d.click(x, y)
  • 长按屏幕
d.long_click(x, y)
d.long_click(x, y, 0.5) # long click 0.5s (default)
  • 滑动
d.swipe(sx, sy, ex, ey)
d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
  • 拖动
d.drag(sx, sy, ex, ey)
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)

注意: click, swipe, drag 支撑百分比位置。例:

d.long_click(0.5, 0.5) #长按屏幕中心

5. 屏幕操作

  • 检索/设置方向

可用的方向有:
natural 或 n
left 或l
right 或r
upsidedown 或u (不可设置)

# 检索方向,可以是"natural" 或"left" 或"right" 或"upsidedown"
orientation = d.orientation

# 警告:未在我的TT-M1通过测试
# 设定orientation(方向) 和 冻结旋转.
d.set_orientation("n") # 或"natural"
d.set_orientation("l") # 或"left"
d.set_orientation("r") # 或"right"
d.set_orientation("n") # or "natural"
  • 冻结/解冻旋转
# 冻结旋转
d.freeze_rotation()
# 取消冻结旋转
d.freeze_rotation(False)
  • 截图
# 截图并保存到本地文件“home.jpg”.
d.screenshot("home.jpg")
# 获取PIL.Image 格式,需要先安装pillow
image = d.screenshot()
image.save("home.jpg") # 或 home.png

# 获取opencv 格式, 需要先安装numpy 和cv2
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)
  • 转储窗口层次结构
# 获取转储的内容(unicode).
xml = d.dump_hierarchy()
  • 打开通知或快速设置
d.open_notification()
d.open_quick_settings()

6.推送和提取文件

  • 将文件推送到设备
#推送到一个文件夹 
d.push("foo.txt", "/sdcard/")
# 推送并重命名
d.push("foo.txt", "/sdcard/bar.txt")
# 推送fileobj
with open("foo.txt", 'rb') as f:
    d.push(f, "/sdcard/")
# 推送并修改文件模式
d.push("foo.sh", "/data/local/tmp/", mode=0o755)
  • 从设备中提取文件
d.pull("/sdcard/tmp.txt", "tmp.txt")
# 设备中没有文件会引发FileNotFoundErr
d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")

7.APP管理

包括APP安装,启动和停止

  • APP安装

目前仅支持从url安装。

d.app_install(' HTTP://some-domain.com/some.apk ')
  • APP启动
d.app_start("com.example.hello_world") # 以package name启动
  • APP停止
# 执行强制停止
d.app_stop("com.example.hello_world") 
# 执行应用清除
d.app_clear('com.example.hello_world')
  • 停止所有APP的运行
# 停止所有 
d.app_stop_all()
#停止除com.examples.demo以外的所有应用程序 
d.app_stop_all(excludes=['com.examples.demo'])

8.选择器Selecor

选择器用于识别当前窗口中的特定ui对象.

# 要选text是'Clock',className 是'android.widget.TextView' 的元素
d(text='Clock', className='android.widget.TextView')

选择器支持以下参数. 详见UiSelecor java doc
.
• text, textContains, textMatches, textStartsWith
• className, classNameMatches
• description, descriptionContains, descriptionMatches, descriptionStartsWith
• checkable, checked, clickable, longClickable
• scrollable, enabled,focusable, focused, selected
• packageName, packageNameMatches
• resourceId, resourceIdMatches
• index, instance

获取儿孙和同级UI对象

儿孙级
# 获取 child 或 grandchild(递归获取)
d(className="android.widget.ListView").child(text="Bluetooth")
同级
# 获取 sibling 或 child of sibling
d(text="Google").sibling(className="android.widget.ImageView")
通过text或description或instance,获取儿孙级对象
# 获取儿孙中符合类名"android.widget.LinearLayout",text包含"Bluetooth"
d(className="android.widget.ListView", resourceId="android:id/list")\ .child_by_text("Bluetooth", className="android.widget.LinearLayout")

# 允许页面滚动搜索获得儿孙对象
d(className="android.widget.ListView", resourceId="android:id/list") \
 .child_by_text(
    "Bluetooth",
    allow_scroll_search=True,
    className="android.widget.LinearLayout"
  )

o child_by_description 用于获取儿孙中包含指定描述的对象, 其余和child_by_text相同.
o child_by_instance 用于获取屏幕上可见的儿孙对象中的特定实例.
详细信息,请参见以下链接:
o UiScrollable
, getChildByDescription, getChildByText, getChildByInstance
o UiCollection
, getChildByDescription, getChildByText, getChildByInstance
上面的方法支持链式调用,例如,对于以下层次结构

<node index="0" text="" resource-id="android:id/list" class="android.widget.ListView" ...>
  <node index="0" text="WIRELESS & NETW或KS" resource-id="" class="android.widget.TextView" .../>
  <node index="1" text="" resource-id="" class="android.widget.LinearLayout" ...>
    <node index="1" text="" resource-id="" class="android.widget.RelativeLayout" ...>
      <node index="0" text="Wi Fi" resource-id="android:id/title" class="android.widget.TextView" .../>
    </node>
    <node index="2" text="ON" resource-id="com.android.settings:id/switchWidget" class="android.widget.Switch" .../>
  </node>
  ...
</node>

System Setting
我们要单击“Wi-Fi”右侧的开关来打开Wi-Fi。由于几个开关具有几乎相同的属性,因此我们不能使用像

d(className="android.widget.Switch")来选择对象,可以使用下面的代码来选。
d(className="android.widget.ListView", resourceId="android:id/list")\
  .child_by_text("Wi Fi", className="android.widget.LinearLayout")\
  .child(className="android.widget.Switch")\
  .click()
相对位置

我们可以用相对位置的方法来获取当前视图中的对象: left, right, top, bottom.
o d(A).left(B), 表示选择A左侧的B.
o d(A).right(B), 表示选择A右侧的B.
o d(A).up(B), 表示选择A上方的B.
o d(A).down(B), 表示选择在A下面的B.
因此,对于“Wi-Fi”开关,我们可以编写如下代码:

# 选择"Wi Fi"右侧的"switch" 
d(text="Wi Fi").right(className="android.widget.Switch").click()
多个实例

有时屏幕上有包含相同特点(例如文本)的多个对象,那么您将不得不在选择器中使用“instance”属性,如下所示:

d(text="Add new", instance=0)  # 获取第一个带有文本“Add new”的元素对象

uiautomator也提供了类似列表的方法来处理类似的元素对象.

# 获取当前屏幕上带有文本“Add new”的元素的总数
d(text="Add new").count

# len函数与count属性功能相同
len(d(text="Add new"))

# 通过index获取元素实例
d(text="Add new")[0]
d(text="Add new")[1]
...

# 迭代
for view in d(text="Add new"):
    view.info  # ...

注意: 使用选择器(如列表)时,必须确保屏幕保持不变,否则可能会出现ui not found error.

获取ui对象的状态和信息

检查指定ui对象是否存在
d(text="Settings").exists # 如果存在,则为True ,否则为 False
d.exists(text="Settings") # 以上属性的别名.
检索指定ui对象的信息
d(text="Settings").info

以下是可能的结果:

{ u'contentDescription': u'',
u'checked': False,
u'scrollable': False,
u'text': u'Settings',
u'packageName': u'com.android.launcher',
u'selected': False,
u'enabled': True,
u'bounds': {u'top': 385,
            u'right': 360,
            u'bottom': 585,
            u'left': 200},
u'className': u'android.widget.TextView',
u'focused': False,
u'focusable': True,
u'clickable': True,
u'chileCount': 0,
u'longClickable': True,
u'visibleBounds': {u'top': 385,
                    u'right': 360,
                    u'bottom': 585,
                    u'left': 200},
u'checkable': False
}
设置/清除可编辑字段的text
d(text="Settings").clear_text()  # 清除文本
d(text="Settings").set_text("My text...")  # 设置文本

对指定ui对象执行click

单击指定ui object
# 点击UI对象的中心
d(text="Settings").click()
# 最长等待(元素显示)10秒,并单击(默认值),超时没有显示会报错
d(text="Settings").click(timeout=10)
# click别名,键盘操作的短名
d(text="Settings").tap()
# 不等element show
d(text="Settings").tap_nowait()
长按指定ui对象
# 长按指定ui object
d(text="Settings").long_click()

指定ui object手势动作

将对象拖到定点或另一个对象

# 注意:在Android 4.3之前无法设置拖动.
# 拖动ui object到point (x, y)
d(text="Settings").drag_to(x, y, duration=0.5)
# 拖动ui object 到另一个ui object(中心)
d(text="Settings").drag_to(text="Clock", duration=0.25)
两点手势
d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))  # s起点,e终点
指定ui object两点手势

支持两种手势:
o In, 从边缘到中心
o Out, 从中心到边缘

# 注意:在Android 4.3之前无法设置缩放.
# 从边缘到中心。
d(text="Settings").pinch_in(percent=100, steps=10)
# 从中心到边缘
d(text="Settings").pinch_out()
等待指定的ui对象出现或消失
# 等待ui对象出现
d(text="Settings").wait(timeout=3.0) # return bool
# 等待ui object消失
d(text="Settings").wait_gone(timeout=1.0)

默认超时20s. 详见"1.全局设定"

对(可滚动)ui对象执行fling

Fling(飞滑)一般用于水平或垂直翻页,可能的属性:
o horiz 或 vert
o forward 或 backward 或 toBeginning 或 toEnd

# fling默认垂直向前 
d(scrollable=True).fling()
# fling水平向前
d(scrollable=True).fling.horiz.forward()
# fling垂直向后
d(scrollable=True).fling.vert.backward()
# 垂直fling到开始
d(scrollable=True).fling.h或iz.toBeginning(max_swipes=1000)
# 垂直fling到末尾
d(scrollable=True).fling.toEnd()
对(可滚动)ui对象执行scroll

可能的属性:
o horiz 或 vert
o forward 或 backward 或 toBeginning 或 toEnd, 或 to

# scroll 默认垂直向前
d(scrollable=True).scroll(steps=10)
# scroll水平向前
d(scrollable=True).scroll.h或iz.forward(steps=100)
# scroll垂直向后
d(scrollable=True).scroll.vert.backward()
# 水平scroll到开始
d(scrollable=True).scroll.h或iz.toBeginning(steps=100, max_swipes=1000)
# 垂直scroll到末尾
d(scrollable=True).scroll.toEnd()
# scroll 向前垂直,直到出现指定ui object 
d(scrollable=True).scroll.to(text="Security")

9.触发器(Watcher)

你可以注册触发器来执行选择器匹配不到的ui对象动作。

注册有名触发器(Watcher)

当选择器找不到匹配项时,uiautomator将运行所有已注册的触发器.

条件匹配时点击目标
d.watcher("AUTO_FC_WHEN_ANR").when(text="ANR").when(text="Wait") \
                             .click(text="Force Close")
# d.watcher(name) # 创建并命名一个触发器.
#  .when(condition)  # 触发条件.
#  .click(target)  # 对目标对象执行click 动作.
条件匹配时按键
d.watcher("AUTO_FC_WHEN_ANR").when(text="ANR").when(text="Wait") \
                             .press("back", "home")
# d.watcher(name)  # 创建并命名一个触发器.
#  .when(condition)  # 触发条件.
#  .press(<keyname>, ..., <keyname>.()  # 依次按键.

检查指定名字的Watcher是否被触发过

一个Watcher被触发过, 意味着所有触发条件都匹配,Watcher程序已运行.

d.watcher("watcher_name").triggered  # 触发过为true ,否则为false

删除有名触发器

# 删除触发器
d.watcher("watcher_name").remove()

列出所有触发器

d.watchers # 返回已注册触发器的列表

检查任意Watcher是否被触发过

d.watchers.triggered  # 任意触发器触发过为true ,否则为false

重置所有触发过的触发器

# 重置所有触发过的触发器, 之后d.watchers.triggered 返回 False.
d.watchers.reset()

删除触发器

# 删除所有注册的触发器
d.watchers.remove()
# 删除指定触发器, 效果和d.watcher("watcher_name").remove()相同
d.watchers.remove("watcher_name")

强制运行所有触发器

# 强制运行所有注册过的触发器
d.watchers.run()

另外本文还有很多没写,推荐直接去看源码init.py

10.输入法

通常用于不知道控件的情况下的输入。第一步切换输入法,然后发送adb广播命令,具体使用方法如下

d.set_fastinput_ime(True) # 切换成FastInputIME输入法
d.send_keys("你好123abcEFG") # adb广播输入
d.clear_text() # 清除输入框所有内容(要求 android-Uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切换成正常的输入法

11.Toast消息

显示Toast消息

d.toast.show("Hello world")
d.toast.show("Hello world", 1.0) # show for 1.0s, default 1.0s

获取Toast

# [Args]
# 5.0: max wait timeout. Default 10.0
# 10.0: cache time. return cache toast if already toast already show up in recent 10 seconds. Default 10.0 (Maybe change in the furture)
# "default message": return if no toast finally get. Default None
d.toast.get_message(5.0, 10.0, "default message")

# common usage
assert "Short message" in d.toast.get_message(5.0, default="")

# clear cached toast
d.toast.reset()
# Now d.toast.get_message(0) is None

12.XPath定位

举例: 某节点内容

<android.widget.TextView
  index="2"
  text="05:19"
  resource-id="com.netease.cloudmusic:id/qf"
  package="com.netease.cloudmusic"
  content-desc=""
  checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false"
  scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true"
  bounds="[957,1602][1020,1636]" />

xpath定位和使用方法

有些属性的名字有修改需要注意

description -> content-desc
resourceId -> resource-id

常见用法

# wait exists 10s
d.xpath("//android.widget.TextView").wait(10.0)
# find and click
d.xpath("//*[@content-desc='分享']").click()
# check exists
if d.xpath("//android.widget.TextView[contains(@text, 'Se')]").exists:
    print("exists")
# get all text-view text, attrib and center point
for elem in d.xpath("//android.widget.TextView").all():
    print("Text:", elem.text)
    # Dictionary eg: 
    # {'index': '1', 'text': '999+', 'resource-id': 'com.netease.cloudmusic:id/qb', 'package': 'com.netease.cloudmusic', 'content-desc': '', 'checkable': 'false', 'checked': 'false', 'clickable': 'false', 'enabled': 'true', 'focusable': 'false', 'focused': 'false','scrollable': 'false', 'long-clickable': 'false', 'password': 'false', 'selected': 'false', 'visible-to-user': 'true', 'bounds': '[661,1444][718,1478]'}
    print("Attrib:", elem.attrib)
    # Coordinate eg: (100, 200)
    print("Position:", elem.center())

其他XPath常见用法,见: https://github.com/openatx/uiautomator2/blob/master/uiautomator2/ext/xpath/README.md

13.常见问题

很多没写在这个地方的,都放到了这里 Common Issues

Stop UiAutomator

停止UiAutomator守护服务

https://github.com/openatx/uiautomator2/wiki/Common-issues

因为有atx-agent的存在,Uiautomator会被一直守护着,如果退出了就会被重新启动起来。但是Uiautomator又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的uiautomatorviewer 就都不能用了,除非关掉该框架本身的uiautomator。下面就说下两种关闭方法

方法1:

直接打开uiautomator app(init成功后,就会安装上的),点击关闭UIAutomator

方法2:

d.service("uiautomator").stop()

# d.service("uiautomator").start() # 启动
# d.service("uiautomator").running() # 是否在运行

ATX与Maxim共存AccessibilityService的方法

14.Uiautomator与Uiautomator2的区别

https://www.cnblogs.com/insist8089/p/6898181.html

  • 新增接口:UiObject2、Until、By、BySelector
  • 引入方式:2.0中,com.android.uiautomator.core.* 引入方式被废弃。改为 android.support.test.uiautomator
  • 构建系统:Maven 和/或 Ant(1.x);Gradle(2.0)
  • 产生的测试包的形式:从zip /jar(1.x) 到 apk(2.0)
  • 在本地环境以adb命令运行UIAutomator测试,启动方式的差别:
    adb shell uiautomator runtest UiTest.jar -c package.name.ClassName(1.x)
    adb shell am instrument -e class com.example.app.
    MyTest com.example.app.test/android.support.test.runner.AndroidJUnitRunner(2.0)
  • 能否使用Android服务及接口? 1.x不能;2.0能。
  • og输出? 使用System.out.print输出流回显至执行端(1.x); 输出至Logcat(2.0)
  • 执行?测试用例无需继承于任何父类,方法名不限,使用注解 Annotation进行(2.0); 需要继承UiAutomatorTestCase,测试方法需要以test开头(1.x)

四、常见问题

1.提示502错误

尝试手机连接PC,然后运行下面的命令

adb shell am instrument -w -r  -e debug false -e class com.github.Uiautomator.stub.Stub \
	com.github.Uiautomator.test/android.support.test.runner.AndroidJUnitRunner

如果运行正常,启动测试之前增加一行代码

d.healthcheck()

如果报错,可能是缺少某个apk没有安装,使用下面的命令重新初始化

python -m Uiautomator2 init --reinstall

五、尝鲜功能

手机python -m Uiautomator2 init之后,浏览器输入 <手机IP:7912>,会发现一个远程控制功能,延迟非常低噢。_

六、关于

项目重构自 https://github.com/openatx/atx-Uiautomator

七、更新日志

由pbr自动生成: CHANGELOG
重大更新

  • 1.0.0
    移除 d.watchers.watched (会拖慢自动化的执行速度并且还会降低稳定性)

八、依赖项目

• Uiautomator守护程序 https://github.com/openatx/atx-agent
• Uiautomator jsonrpc server https://github.com/openatx/android-Uiautomator-server/

九、贡献者

• codeskyblue (@codeskyblue)
• Xiaocong He (@xiaocong)
• Yuanyuan Zou (@yuanyuan)
• Qian Jin (@QianJin2013)
• Xu Jingjie (@xiscoxu)
• Xia Mingyuan (@mingyuan-xia)
• Artem Iglikov, Google Inc. (@artikz)
其他贡献者

十、许可

Under MIT

  • 19
    点赞
  • 175
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值