最近在网络上看到了各种游戏自动化基础教程,一次课还需要好几千元起步。本身也只是一些简单的基础知识,抱着知识分享的想法,觉得也应该做点什么小贡献才是。于是开启了这个专栏。
对于Android手机的调试,大多数情况下是通过用Type-c或者Micro usb等数据线连接至电脑进行调试的。目前较为主流的方式也是用的该方法,比如网易的Airtest,腾讯的PerfDog和Unity的UPR等等。其中,Google对Android的连线调试提供了一个主要的驱动工具ADB(Android Develop Bridge)使得我们可以更好地进行操作。所以这边主要介绍ADB的一些重要指令。
设备连接 ,注:需要将手机切换到开发者模式,同时开启usb调试,执行如下指令;
adb devices
如果手机已经连接成功,将会获得手机的DeviceId列表
List of devices attached
957HY9EEVCGAIR7T device
启动和关闭应用。 该方法需要实现获知应用的 < package-name >(包名),获取包名的方法有如下几种:
(1)在android手机上安装包名查看器进行查询
(2)先在手机上开启该APP将其处于前台,执行如下指令 “adb shell dumpsys window | findstr mCurrentFocus”
(3)执行指令“ adb shell pm list packages”,获得手机上所有的应用列表,然后自己仔细查看。
获取到包名之后,通过 Package-name 获取 Activity Name(这边注意的一点是,在Android应用中,默认的主Activity Name都是MainActivity, 但是在Unity应用中,默认的是UnityPlayerActivity,同时也可以由开发者在xml中自己进行配置,因此这一步还是需要的,不能省略)
adb shell dumpsys package -name> | findstr MainActivity
之便可以直接通过下面指令启动和关闭APP应用了。
启动APP应用: adb shell am start 包名/Activity
关闭APP应用:
adb shell am force-stop 包名 强制停止APP进程,不会清除APP进程在系统中产生的数据
adb shell pm clear 包名 停止APP进程,而且会清除这个APP进程产生的所有数据
adb forward - forward socket connections
forward specs are one of:
tcp:
localabstract:
localreserved:
localfilesystem:
dev:
jdwp:<process pid> (remote only)
举个例子:
adb forward tcp:2333 localabstract:Unity- //PC上的2333端口指向Unity对应包名的应用所使用的端口
adb forward tcp:6666 tcp:7777 // PC上所有6666端口通信数据将被重定向到手机端7777端口server上
adb forward tcp:5555 dev:/dev/block/by-name/system //将5555端口映射到系统镜像文件夹
对于IOS手机的调试一般情况下需要在mac电脑上进行,不过Libimobile通过对ios instrument的解析让它既可以在mac os x系统上运行也可以在windows系统上运行(需要先下载iTunes并且确保可以连接上iTunes),网上公开的地址为 Libimobile
设备连接 ,这边以windows电脑为例:,执行如下指令;
idevice_id.exe -l
如果手机已经连接成功,将会获得手机的DeviceId列表,其中包括设备的udid
List of devices attached
957HY9EEVCGAIR7T device
获取ios系统名称
idevicename.exe -u [udid]
验证设备是否同电脑已经配对
idevicepair -u [udid] validate
您应该在设备上看到一个对话框,要求“信任此计算机”。选择“信任”,并在出现提示时输入密码。
如果没有配对需要先配对:
idevicepair -u [udid] pair
通过lightning线启动游戏
idevicedebug -u [udid] run [package-name]
端口映射:手机与电脑端的端口映射
iproxy -u [udid] [pc-port] [ios-port]
使用Iproxy指令的时候,会占用当前进程,如果退出当前进程则端口映射失效。
从一个移动设备或虚拟机拉取一个指定文件到电脑。
adb pull /sdcard/Music/voice.mp3 // 示例
// adb -s [device_id] pull [remote-path] [local-path]
向一个移动设备或虚拟机推送一个指定文件。
adb push voice.mp3 /sdcard/Music/ // 示例
// adb -s [device_id] push [local-file] [remote-path]
列出移动设备或虚拟的指定目录。
adb shell ls /sdcard/Music/ // 示例
// adb -s [device_id] shell ls [remote-path]
这边的用法其实相当于使用 "adb -s [deviceId] shell"进入到设备的shell后执行ls指令,另外ls可以通过 -a 等方法列举所有文件、文件详细信息等就不多介绍。
其他文件操作指令
主要还是通过adb shell进入到android设备中进行操作,而android操作系统源于linux操作系统,因此大多数的Linux指令集都可以使用。可以参考如下链接。
(这边的指令并不适用于所有的android手机,部分ROM对系统进行了一定的修改无法执行相应指令,比如一加手机或者金立手机)
查看手机型号
adb shell getprop ro.product.model
通过getprop方法获取到的数据还有如下:
属性名 | 含义 |
---|---|
ro.build.version.sdk | SDK 版本 |
ro.build.version.release | Android 系统版本 |
ro.build.version.security_patch | Android 安全补丁程序级别 |
ro.product.model | 型号 |
ro.product.brand | 品牌 |
ro.product.name | 设备名 |
ro.product.board | 处理器型号 |
ro.product.cpu.abilist | CPU 支持的 abi 列表[节注一] |
persist.sys.isUsbOtgEnabled | 是否支持 OTG |
dalvik.vm.heapsize | 每个应用程序的内存上限 |
ro.sf.lcd_density | 屏幕密度 |
查看电池状况
adb shell dumpsys battery
查看分辨率
adb shell wm size
查看CPU信息
adb shell dumpsys cpuinfo
查看内存信息
adb shell dumpsys memifo
需要更对相应的信息获取操作可以参考如下链接。
adb logcat -c //清除日志缓存
adb logcat //从上次缓存之后开始输出
adb logcat *:E //只输出Error信息
adb logcat | grep pid // 通过系统提供的过滤方法进行过滤
使用V、D、I、W、E、F、S优先级标记进行过滤
V —— Verbose 明细(最低优先级)
D —— Debug 调试
I —— Info 信息
W —— Warn 警告
E —— Error 错误
F —— Fatal 严重错误
S —— Silent 无记载(最高优先级,没有什么会被记载)
修改设备ID
adb shell
echo [新的设备id] >/sys/class/android_usb/android0/iSerial
命令行安装ipa。
ideviceinstaller -u [udid] -i [abc.ipa] //abc.ipa:安装文件路径
命令行卸载ipa。
ideviceinstaller -u [udid] -U [bundleId] //bundleId:应用的包名
查看安装的三方包
(这个指令需要等待一阵子才能获取到,大概是需要解析时间)
ideviceinstaller -u [udid] -l // 指定设备,查看安装的第三方应用
ideviceinstaller -u [udid] -l -o list_user // 指定设备,查看安装的第三方应用
ideviceinstaller -u [udid] -l -o list_system // 指定设备,查看安装的系统应用
ideviceinstaller -u [udid] -l -o list_all // 指定设备,查看安装的系统应用和第三方应用
ideviceinfo -u [udid] // 指定设备,获取设备信息
ideviceinfo -u [udid] -k DeviceName // 指定设备,获取设备名称:iPhone6s
idevicename -u [udid] // 指定设备,获取设备名称:iPhone6s
ideviceinfo -u [udid] -k ProductVersion // 指定设备,获取设备版本:10.3.1
ideviceinfo -u [udid] -k ProductType // 指定设备,获取设备类型:iPhone8,1
ideviceinfo -u [udid] -k ProductName // 指定设备,获取设备系统名称:iPhone OS
idevicesyslog | grep 'xxxx' //通过findstr或者grep过滤需要的信息
counter = 100 # 赋值整型变量
miles = 1000.0 # 浮点型
name = "HelloWorld" # 字符串
lists = [] #列表
tuples = () #元组
sets = set()
dicts = {} #字典
a = b = c = 1 #同时赋值
a, b = 1, 2 #多个赋值
intvar = 1000 #数字整型
intvar = 0b1010 #二进制整型
floatvar = 1000.0 #浮点型
boolvar = False #布尔型
complexvar = 3-91j #复数
complexvar = complex(3,-91) #复数的另一种写法
(2) 字符串
strvar1 = 'Hello' #可以使用单引号
strvar2 = "World" #也可以使用双引号
print(strvar1 + strvar2) #可以直接用加号连接
strvar = r"E:\test\python" #前面加上r表示不转义符
(3)列表(list)
lists = [1, 2.0, "3", ["4"], (5), {"6":6}, True, 7+8j]
lists.append(9) #添加一个int 9
lists.remove(0) #删除index为0的元素
lists = lists[:-1] # 截取到倒数第一个数前的数组
lists = lists[1:2] # 截取第一个元素
List相当于是一个没有数据类型的动态数组,可以通过append和remove的方法进行添加和删除,也可以通过正向和反向的操作进行选取。对于列表的详细用法,可以参考这个博客。
(4)元组(tuple)
tuples = (1, 2.0, "3", ["4"], (5), {"6":6}, True, 7+8j)
乍看一下元组和列表没有什么不同,就是中括号改成了小括号而已,不过还是有以下的一些区别的:
元组的值是不可以修改的,但是元组中如果是一个List, 比如tuples[3], 那么这个tuples[3].append(“abc”),这样实际上是可以的。对于元组的详细用法,可以参考这个博客。
(5)集合(set)
集合可以看做是无序、不可修改、自动去重的列表。写法是个大括号。
由于集合是无序的,因此无法直接访问到集合内的元素。 同时可以和tuple,list互相转化
集合在python使用的情况大多在于去重,但是有需要了解更多的使用方法,可以参考一下这个博客。
setvar = {"hello", "world", "!"}
listvar = ["a","a", "b"]
newset = set(listvar) #这时候只有 {"a", "b"} 会自动去重
(6)字典(dict)
字典的写法和集合的写法一样都是一个大括号,但是字典是一种key-value的存储结构,可以修改value, 也可以通过key直接获取到value,实际上就是相当于把list的index换成了自定义的key。python的字典是使用特别多的数据结构,详细的使用方法和情况可以参考这个博客。
dictvar = {"1":"a", "2":"b", "3":"c"}
print(dictvar["1"])
dictvar["2"] = "d"
# -*- coding: utf-8 -*- #好看点的写法
#coding=utf-8 #难看的写法
不过经常遇到编码需要转换的情况,主要常见的编码有这些。
这里主要介绍一下utf-8 转换为unicode编码再转成gbk的示例
# -*- coding: utf-8 -*-
a = "测试"
a_unicode = a.decode('utf-8') #需要制定原来的编码做解码
a_gbk = a_unicode.encode("gbk") #把unicode编码成gbk
其他的方法如法炮制,有更多的需求可以参考一下链接
if 条件1: #满足条件1
条1件成立时需要做的事 #比上一条多4个空格的缩进
elif 条件2: #满足条件2
条件2成立是需要做的事
if 条件3: #满足条件3
条件3满足需要做的事 #比条件3多3个空格的缩进
if 条件4: #满足条件4
条件4满足需要做的事
else:
条件3不满足需要做的事
else:
条件1不成立,条件2也不成立的情况
if not a > b and b > c or c > d:
这句话的优先级我们通过括号表示就是
if not (a > b and (b > c or c > d)):
如果在同级别的运算符前就是按从左到右的顺序判断
if a > b and b > c:
这样当判断 a < b的时候就直接退出了
if a > b or b > c:
这样当判断 a > b的时候就直接进入条件了
def 函数名(参数列表):
//实现特定功能的多行代码
[return [返回值]] #也可以不返回
对于函数,比较重要的还是一个函数参数的传递机制,是传值还是传引用是个值得细究的问题。
值传递(passl-by-value)过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
引用传递(pass-by-reference)过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
在Python函数中,是不允许程序员选择传递值或者引用的写法,而是使用的叫做“传对象引用”的方式,即如果函数的参数对象是可变对象的引用,如dict或者list,能够修改对象的内容的,就通过传递引用的方式,而如果参数对象是不可变的,如数字,字符或者元组,则通过传值来传递对象。
from 文件夹 import 文件
from 文件 import 类
#直接引用 import只能加 文件夹/文件/
def dosomething(abc):
return abc
res = dosomething("cde")
print(res)
func = lambda x, y : x * y
lambda [arg1 [,arg2, ... argN]] : expression
print(func(a, b))
不过,这样的匿名函数有什么实际的价值呢?我就不缺取名的idea。 重点还是在于方便,比如我们在对dict进行排序的时候,大多数方法还是使用的匿名函数。
dic = {"a":10, "b":9, "c":8, "d":7, "e":8}
sorted(dic.items(),key=lambda item:item[0])
除了自定义函数以外,对于一个对象会有自己的一些自带属性和方法,可以直接进行调用。
class Student:
empCount = 0
def __init__(self, name, age):
self.name = name
self.age = age
self.empCount += 1
def __setattr__(self, key, value):
self.__dict__[key] = value
def __getattr__(self, item):
if item == "age":
return 'age:40'
else:
raise AttributeError(item)
def __delattr__(self, item):
print("你正在删除一个属性")
return super().__delattr__(item)
def __setitem__(self, key, value):
print('正在设置项目')
def __getitem__(self, item):
print("正在获取项目")
def __delitem__(self, key):
print("正在删除项目")
if __name__ == "__main__":
student = Student("ali", 5)
student.sex = "male"
print(student.age)
del student.sex
student["sex"] = "male"
print(student["age"])
del student["sex"]
其中 __init__就是自带的内置方法,用于初始化这个类,初次之外,在Python的面向对象中还存在许多内置方法,由于对之后的自动化帮助不大,所以就简单介绍了。
setarrt 和 getattr
(1) 首先是 setattr , 当我们执行 "student.sex = “male”"这行程序时,就会默认调用的__setattr__的方法,sex作为key, “male” 作为value, 然后将其存储为类的属性值。
(2) 其次是 getattr,这个是用于获取类的属性时进行处理的内置函数,比如我们在执行"student.age"时,student这个对象并不带有age这个属性,但是它会先经过这个类的判断,然后最终返回“age:40”这样的我们事先处理的方法。具体是用于缺失属性的处理。
(3)最后是 delattr,这个属性的话,是在删除数据的时候调用的,当我们删除对象的某个属性时,会先执行我们实现写好的输出"你正在删除一个属性"这样的操作再删除属性。
setitem、getitem 和 delitem
(1) __setitem__同__setattr__类似,当执行student[“sex”] = "male"会进行调用
(2)__gettem__同__getattr__类似,当执行student[“age”]会进行调用,然后再返回对应的值。
(3)__deltem__同__delattr__类似,当执行del student[“sex”]会进行调用,然后再删除对应的值。
内置方法这块对于游戏自动化而言并没有太多意义,这边先简单介绍一下,如果需要详细了解,可以参考这篇博客。
Python语言在大多数情况下都是作为一种脚本语言的存在,因此主要是采用的面向过程的编程方法。而面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。不可避免地,当我们使用Python语言编写大工程的时候就可能会运用到面向对象的思想,因此,在Python中也提供了相应的做法。
类
根据百度百科的解释,类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。我们直接通过代码来进行详解。
class People():
def __init__(self, sex, age, name):
self.name = name
self.sex = sex
self.age = age
def printinfo(self):
print("姓名:",self.name)
print("性别:", self.sex)
print("年龄:", self.age)
if __name__ == "__main__":
people = People('edward', 'male', 16)
people.printinfo()
在这段代码中,我们简单地定义了People这样一个类,这个类包括变量name, sex和age,作为People这种种类的一种属性,同时People这种种类还有一个printinfo的方法,这个方法的功能是能够把自己的相关信息输出。也就是说,只要有个东西是属于People这种类型的,那么就会具有这样的三个属性和一种功能。
我们定义了一个对象,名字叫做people(小写的), 这个people是个实际存在的东西(People大写的相当于是一种模板),它属于People这种类型,因此也会拥有三个属性,名字叫做"edward", 性别是"male", 年龄16岁。让他执行proinfo这种操作的时候,它就会把这样的三个内容给输出来。如果对于类的理解还有些问题的可以查看这个博客,通过图文的方式会更有利于理解类这种类型的定义。
类继承
类的继承有点像是一种扩展,通常情况下我们将继承关系的类分为父类和子类。被继承的类叫做父类,继承的类叫做子类。同正常的父子一样,我们可以认为子类包含了父类的所有属性以及方法,但是子类可以有自己的方法,也可以修改父类的一些方法。我们通过代码来解释一下
class Animal(object):
def walk(self, foot):
print("I walk around the grass with " + foot + "foot")
def talk(self):
print("say hi")
class Dog(Animal):
def talk(self):
print("wang wang wang!")
if __name__ == "__main__":
dog = Dog()
dog.walk(4)
dog.talk()
首先我们定义了一个Animal类,包含了walk和talk两个方法。这时候,我们再定义一个类Dog, Dog这个类继承了Animal类意思就是包含了Animal定义的所有方法,包括talk和walk。 但是,由于狗属于动物,但是狗有自己的特色,狗是汪汪叫的,我们要专门给狗这个类定制化自己的talk方法。这时候就可以再给Dog类自己定义一个talk方法,这个方法就会覆盖了父类的talk方法,这种方式叫做重构。
大致的类继承就这方面的内容,如果有需要进一步了解。可以参考一下这个博客。
静态方法
Python类语法中有三种方法,静态方法、实例方法以及类方法。我们可以通过一个简单的例子进行介绍一下。
class Animal(object):
"""类三种方法语法形式"""
def instance_method(self):
print("是类{}的实例方法,只能被实例对象调用".format(Animal))
@staticmethod
def static_method():
print("是静态方法")
@classmethod
def class_method(cls):
print("是类方法")
if __name__ == "__main__":
dog = Animal()
dog.instance_method()
dog.static_method()
dog.class_method()
print('**************')
Animal.static_method()
Animal.class_method()
实例方法,在函数的参数中有个self参数,表示的是实例对象本身,也就是该方法只能有具体的实例对象进行调用,不能够直接通过类进行调用。
静态方法,有@staticmethod修饰,既可以由实例对象调用,也可以由类直接调用,在参数上没有要求。
类方法,由@classmethod修饰,既可以由类调用也可以由实例调用,第一个参数cls表示的是类本身,默认传类。
静态成员
类的静态成员,包括静态变量和静态方法。静态方法在上一小节已经进行了介绍,需要通过@staticmethod 进行修饰,因此这边主要介绍的就是静态变量。静态变量(Static Variable)在计算机编程领域指在程序执行前系统就为之静态分配(也即在运行时中不再改变分配情况)存储空间的一类变量。可以理解成在程序进行初始化时就为静态变量分配了一个固定的内存空间,某种意义上同全局变量是相同的。在大多数编程语言中,静态变量一般是通过"static"进行表示,而python一般是直接在类中进行初始化。如下面例子:
class NewClass:
args = 0
def __init__(self, arg):
self.args = arg
if __name__ == "__main__":
cls = NewClass("args")
print(cls.args) # get the result args
print(NewClass.args) # get the result 0
上面的例子中,args初始化为0,作为这个类的全局变量,而在init函数中定义的self.args则为一个对象变量,保存在对象的item中,只有通过对象才能调用。而静态变量无需实例化为对象,因此args这个变量在初始化NewClass这个类的时候就已经自动分配了存储空间,可以直接通过类进行调用,这也就是在某种程度上,静态变量可以称为全局变量。
这边既然介绍到了静态变量,就顺便简单介绍一下Python的全局变量。在一般的编程语言中,静态变量和全局变量的写法区别不大。不过在Python中,全局变量需要通过关键字global来进行调用。我们通过下面这个例子说明一下。
args = 100
class NewClass:
args = 0
def __init__(self, arg):
self.args = arg
@staticmethod
def printArgs():
global args
print(args)
if __name__ == "__main__":
cls = NewClass("args")
print(cls.args) # get the result args,调用对象变量
print(NewClass.args) # get the result 0 调用静态变量
NewClass.printArgs() # get the result 100 调用全局变量
args += 1
NewClass.printArgs() # get the result 101
在这个代码片段中,我们新定义了一个静态方法(静态不静态都可以,这边为了区分定义为静态),然后指定了 global args 表示,在当前函数中定义了一个args参数,这个args参数指的就是全局的args参数,而全局的args = 100,因此此时输出的是100。 我们在主方法中对全局变量args加一之后,再次调用printArgs方法,可以很清楚看出,args指向的确实就是全局变量args。
(这边为了区分全局变量,静态变量和对象变量,全都是用args作为变量名字,正经写代码的时候得写一些有用的)
这边首先上一下Airtest的官网地址。
Airtest Project是网易出品的一款基于图像识别和poco控件识别的一款UI自动化测试工具。我们具体可以分为Airtest和POCO两种使用方式。
Airtest的框架是网易团队自己开发的一个图像识别框架,这个框架的祖宗就是一种新颖的图形脚本语言Sikuli。Sikuli这个框架的原理是这样的,计算机用户不需要一行行的去写代码,而是用屏幕截屏的方式,用截出来的图形摆列组合成神器的程序,这是Airtest的一部分。
Poco:是一款基于UI控件识别的自动化测试框架,通过在开发工具中嵌入SDK的方式来识别各种控件,目前支持Unity3D/cocos2dx-*/Android原生app/iOS原生app/微信小程序,也可以在其他引擎中自行接入poco-sdk来使用。(个人认为,Poco使用方式的存在在于Airtest方式对于图像识别需要一定的耗时,同时可能分辨率的不同会对Airtest的识别准确率有一定的影响,因此需要快速地识别控件定位还是需要poco sdk的应用)
Airtest Project主要还是运用于游戏自动化,使用网易自带的IDE进行操作。主要编辑脚本为Python脚本,定义后缀名为air。我们打开Airtest IDE,通过右边的窗口连接至手机。这边主要有三种方式,1.真机连接,则直接点击Connect按钮即可。 2.使用adb connect远程连接。 3.无线连接(无线连接的方式同2类似,这边为具体的操作方式),大多数情况下,为了避免网络波动造成的影响,用户主要都是采用的真机连接,这边也使用真机连接来做一个示范。