【杰哥带你玩转Android自动化】学穿:ADB
本文为稀土掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
0x1、引言Hi,我是杰哥,在上一节的《开篇漫谈》中提到这样一句话:
所有Android自动化框架和工具中操作Android设备的功能实现都基于adb和无障碍服务AccessibilityService。
本节我们来学习下前者,学习路线安排如下:
简单了解下ADB的概念,是什么?由哪几部分组成?工作原理是咋样的?ADB环境配置,以及两个常见问题的解决(adb端口占用,adb devices无法识别设备)学习ADB命令的语法,掌握一些自动化操作中巨常用的ADB命令;了解如何在Android App中执行adb命令;学习在PC端编写Python程序调用adb命令;实战:某办公软件打卡自动化;就不废话水字数了,我直接开始~
0x2、ADB概念① ADB是什么ADB(Android Debug Bridge) ,译作 **安卓调试桥**,一个能让你与Android设备进行通信的 **命令行工具**。
说人话就是:你可以通过它,在命令行输入命令控制Android设备。
② ADB架构ADB是一种C/S架构的应用程序,由三个部分组成:
服务端→PC端的adb server→ 运行在PC端的后台进程,用于:检测USB端口感知设备连接与拔除;模拟器实例的启动与停止;将adb client的请求通过usb或tcp的方式发送到对应的adbd进程;客户端→PC端的adb client→ 主要用于发送命令解析像:push、shell、install等命令的参数,做必要预处理,然后转移为指令或数据,发送给adb server;守护进程→手机端的adbd→ 由init进程启动处理来自 adb server的命令行请求,获取对应Android设备的信息,再将结果返回给adb server;工作原理
启动adb客户端→ 检查是否有adb服务端进程在运行 → 没有的话启动一个;adb服务端进程启动后会与本地TCP端口5037绑定,并监听adb客户端发出的命令;服务端扫描5555-5585之间的奇数端口查找设备/模拟器,一旦发现adbd进程便会与相应端口建立连接;注:每个adbd会占用两个PC端口,奇数用于adb连接,偶数用于命令行连接,如5554和5555端口是一对;adb服务端与所有设备均建立连接后,你便能使用adb命令访问这些设备;通信流程
对于原理和过程,大概了解,心理有数就行,看不懂也不影响你后面的学习。当然如果你对更深层的原理或源码感兴趣,可以参考下《adb和adbd分析》,笔者就不往下卷了~
0x3、ADB环境配置如果您是尊贵的Android开发,使用Android Studio,并配置过环境变量,就不用再配了。可以直接打开命令行/终端键入adb version验证:
可以看到输出了:adb的版本信息及可执行文件所在的路径。
如果您不是Android开发也不打紧,到官网下个 **SDK Platform-Tools**:
下载完成解压后可以看到:adb、fastboot等工具包
接着复制下文件夹路径,在系统环境变量PATH中加上它,然后就可以在命令行里直接使用adb啦~
同样键入adb version验证:
可以看到adb版本信息和可执行文件的路径都发生了改变,说明配置生效。
常见问题一:adb端口被占用一般发生在Windows系统,如果你电脑安装了一些XX手机助手的软件,那在执行adb命令时,很大概率会遇到这个问题:
说明你的:5037端口被占用了,上面说过这个端口是留给adb server使用的,解决方法有两种:
方法一:干掉占用进程(建议)
键入:**netstat -ano | findstr "5037"**,获取占用端口的进程PID,如:
C:\Users\xxxnetstat-aon|findstr5037
TCP127.0.0.1:50370.0.0.0:0LISTENING2908
键入:**tasklist /fi "PID eq 2908"**,查看进程PID对应的进程,如:
C:\Users\xxxnetstat-aon|findstr5037
映像名称PID会话名会话#内存使用
========================================================================
xxx.exe2908Console111,292K
键入:**taskkill /pid 2908 /f,杀掉占用端口的进程**,如:
C:\Users\xxxtaskkill/pid2908/f
成功:已终止PID为2908的进程。
最后键入adb相关命令,如adb devices,即可启动adb server进程,效果图如下:
方法二:修改adb server端口
总有一些毒瘤进程,可能刚干死又重启了,方法一不一定能生效,打不过,躲得过,除了把毒瘤应用卸载外,还可以考虑下修改adb server的端口号。
建议选一个 **五位的端口号(10000-65535)**,没那么容易重复,接着在系统环境变量中点击新建环境变量,变量名为ANDROID_ADB_SERVER_PORT变量值为端口号,如:
此时关掉命令行再次打开进入adb命令,可以看到端口号已经修改为10024了:
如果不是因为毒瘤,纯粹想改下端口,可以键入adb kill-server把adb server干掉,配置完成后,再键入adb start-server启动 adb server。
Tips:Linux、Mac系统直接终端输入export $ANDROID_ADB_SERVER_PORT = 自定义端口即可设置。
常见问题二:adb devices无法识别设备问题描述:手机连上电脑,键入adb devices,却没输出任何设备?
回答:确定手机USB调试开了吗?开启方法如下 (不同手机系统可能存在差异,可自行搜索关键字):
首次连接,点击手机设置→系统信息→ 点击版本号多次直至出现您已处于开发者模式→ 返回找到开发者模式→ 找到USB调试开启,然后手机一般会弹个授权的对话框,授权就好。此时再键入adb devices看看设备是否显示。
当然,如果你在授权的时候,手滑点了拒绝,此时键入adb devices时,设备的状态会显示为 **unauthorized**,再次拔插手机,授权窗口都不会再弹了,解决方法如下:
adb kill-server关掉adb服务,拔掉手机;找到并删除电脑中的两个配置文件:**/用户名/.android/adbkey** 和 **/用户名/.android/adbkey.pub**;adb start-server启动adb服务,再插手机,授权弹窗应该就出来了;如果使用了上述方法还是无法识别,那可能是USB接口的问题和驱动问题,可以换个手机或者换条线试试,如果正常说明不是USB接口问题。手机官网搜下对应手机型号的驱动,安装后试试。另外,重启试试有时也包治百病~
0x4、ADB命令详解adb完整命令语法如下:
adb[-d|-e|-sserialNumber]command
如果只有一个设备/模拟器连接PC,不用加中括号里的参数,当有多个时,才需要通过这些参数指定目标设备:
-d→ 指定当前唯一通过usb连接的Android设备为命令目标;-e→ 指定当前唯一运行的模拟器为命令目标;-s serialNumber→ 指定对应serialNumber号的设备/模拟器为命令目标,最常用;上面adb devices输出的8c8f689e就是我当前连接的手机序列号,除了真机还有模拟器和无线连接设备,如:
emulator-5554device
10.129.164.6:5555device
另外,adb命令区分权限,有些命令需要root权限才能执行!
如果你手机已经Root了,想给adbd授予Root权限,下述方法二选一:
① 键入adb root,如果正常输出 **restarting adbd as root**,键入 **adb shell**;② 键入adb shell,输入 **su**;当然如果想取消adbd的root权限,也可以键入 **adb unroot**。
对了,有些手机即使Root了,也可能无法让adbd以root权限执行,如三星的部分机型,会提示adbd cannot run as root in production builds,可以先安装adbd Insecure,然后再次尝试。
接着罗列笔者觉得自动化操作最常用的命令,更多命令可到ADB官方文档或mzlogin/awesome-adb自行查阅~
① 查看前台Activity命令:(可以借此拿到应用包名和当前Activity名)
#进入Android终端的命令行模式
adbshell
#输出前台Activity
dumpsysactivityactivities|grepmResumedActivity
结果:
② 启动应用/调起Activity命令:
adbshell
#不指定Activity名称启动,即启动主Activity
monkey-ppackagename-candroid.intent.category.LAUNCHER1
#指定启动Activity名(需要root权限)
amstart-npackagenameactivity类名
结果:
③ 强行停止应用命令:
adbshellamforce-stoppackagename
结果:
④ 模拟按键/输入/滑动/点击命令(完整Keycode列表可见官网:KeyEvent):
adbshell
#模拟按键(Home-3,返回-4,电源-26,亮屏-224、熄屏-223,切换应用-187,小键盘删除-67),如点击Home键:
inputkeyevent3
#在焦点处于某文本框时,使用input命令输入文本
inputtextHello
#滑动,从起始坐标点滑动到结束坐标点,如上滑(300,1000)→(300,500):
inputswipe3001000300500
#点击坐标点
inputtap500500
Tips:adb默认不支持Unicode编码,所以无法input中文内容,即使你默认使用了支持中文的输入法也不行,解决方法如下:
①下载安装ADBKeyBoard;② 安装完后依次打开:手机设置→ 语言和输入法 → 键盘 → 虚拟键盘 → 管理键盘 → 启用ADB keyboard;③默认输入法设置为ADB keyboard (比如原生系统点击右下角小键盘设置)④ 接着使用这个命令即可输入中文 (其他语言也可以):**adb shell am broadcast -a ADB_INPUT_TEXT --es msg '中文输入'**⑤ 截图命令:
adbshell
#截图
screencap-p/sdcard/sc.png
#退出adbshell
exit
#导出到电脑
adbpull/sdcard/sc.png
另外,还有一种一行命令截图并保存到电脑的方法:
#Mac
adbshellscreencap-p|gsed"s/\r$//"sc.png
#Linux和Windows
adbshellscreencap-p|sed"s/\r$//"sc.png
#Tips:上面的截图方法,Windows能获取图片,但是打不开,可以用下述方法获取:
adbexec-outscreencap-psc.png
⑥ 查看分辨率命令:
adbshellwmsize
结果:
0x5、Android App中调用adb命令不是很推荐这种方式弄自动化,第一个是权限问题,如果你的自动化设备是有Root权限的真机或模拟器,在考虑这种吧。
另外一个问题是保活问题,一般我们的自动化场景,都是定时去做一些事情,比如我想每天早上8点半自动打卡,那我需要写一个后台服务,然后让它一直挂着, 到点触发自动打卡的操作。这个保活在PC上很容易实现,但在手机里却很难保证...
写个简单的调用示例吧,有需要的改改就能用,先是布局:
?xmlversion="1.0"encoding="utf-8"?
LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入adb命令"/
androidx.appcompat.widget.AppCompatButton
android:id="@+id/bt_run"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="执行"/
androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="输出结果:"/
androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_output"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text=""/
/LinearLayout
Android中的调用代码:
classTestCmdActivity:AppCompatActivity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_cmd)
findViewByIdTextView(R.id.et_input).text=
"monkey-pcom.tencent.mobileqq-candroid.intent.category.LAUNCHER1"
findViewByIdButton(R.id.bt_run).setOnClickListener{
valresult=execCmd(findViewByIdEditText(R.id.et_input).text.toString())
if(result!=null)findViewByIdTextView(R.id.tv_output).text=result
}
}
/**
*执行普通命令(不需要adbshell)
**/
privatefunexecCmd(cmd:String?):String?{
returnif(cmd.isNullOrBlank()){
shortToast("命令不能为空")
null
}else{
try{
valsb=StringBuffer()
valprocess=Runtime.getRuntime().exec(cmd)
valinputStream=process.inputStream
valbufferedReader=BufferedReader(InputStreamReader(inputStream))
valbuff=CharArray(1024)
varch:Int
while(true){
ch=bufferedReader.read(buff)
if(ch==-1)break
sb.append(buff,0,ch)
}
process.waitFor()
bufferedReader.close()
returnsb.toString()
}catch(e:IOException){
e.toString()
}
}
}
/**
*执行需要Root权限的命令
**/
privatefunexecCmdRoot(cmd:String?):String?{
returnif(cmd.isNullOrBlank()){
shortToast("命令不能为空")
null
}else{
valsuccessMsg=StringBuffer()
valerrorMsg=StringBuffer()
varprocess:Process?=null
varsuccessResult:BufferedReader?=null
varerrorResult:BufferedReader?=null
varos:DataOutputStream?=null
try{
process=Runtime.getRuntime().exec("su")
os=DataOutputStream(process.outputStream)
os.write(cmd.toByteArray())
os.writeBytes("\n")
os.flush()
valresult=process.waitFor()
successResult=BufferedReader(InputStreamReader(process.inputStream))
errorResult=BufferedReader(InputStreamReader(process.errorStream))
vars:String?
while(true){
s=successResult.readLine()
if(s==null)break
successMsg.append(s)
}
while(true){
s=errorResult.readLine()
if(s==null)break
successMsg.append(s)
}
returnsuccessMsg.toString()+"\n\n"+errorMsg.toString()
}catch(e:IOException){
e.toString()
}finally{
process?.destroy()
os?.close()
successResult?.close()
errorResult?.close()
}
}
}
}
运行结果如下:
尽管控制台看到有输出,但实际上并没有唤起QQ,说到底还是权限的问题。本想试下su提权的,结果我手机的Magsik出问题了,获取su权限直接卡死...坑还是挺多的,所以不太建议新手拿这个玩自动化哈~
0x6、Python调用adb命令 (推荐)如题,就是编写Python脚本来调用adb命令,个人比较推荐这种方式,可玩性非常强,可以:
借助PC做定时任务 (保活)、利用PC强大性能进行图片处理、OCR识别、信息上报、多机群控等;
不太懂Python?没关系,杰哥帮你把自动化adb命令都封装一波,你按照你的逻辑直接调方法就行,跟玩积木一样~
所谓的封装,核心就是通过subprocess模块调一下命令行而已,非常简单,直接给出完整代码:
importsubprocess
importre
fromenumimportEnum
importtime
importos
pkg_act_pattern=re.compile(".*(.*?)/(.*?)",re.S)#获取包名和Activity名的正则
chinese_pattern=re.compile("[\u4e00-\u9fa5]",re.S)#筛选中文的正则
size_pattern=re.compile(r"(\d+)x(\d+)",re.S)#获取屏幕尺寸的正则
t=time.time()
classKeyEvent(Enum):
"""
按键事件的枚举
"""
HOME=3
BACK=4
POWER=26
SCREEN_ON=224
SCREEN_OFF=223
SWITCH_APP=187
DELETE=67
defstart_cmd(cmd):
"""
执行命令
:paramcmd:命令字符串
:return:执行后的输出结果列表
"""
print(cmd)
proc=subprocess.Popen(cmd,shell=False,stdout=subprocess.PIPE)
returnproc.stdout.readlines()
defstart_app(package_name):
"""
启动APP
:parampackage_name:应用包名
:return:执行结果字符串
"""
returnanalysis_result(start_cmd(f'adbshellmonkey-p%s-candroid.intent.category.LAUNCHER1'%package_name))
defkill_app(package_name):
"""
杀掉APP
:parampackage_name:应用包名
:return:
"""
returnanalysis_result(start_cmd(f'adbshellamforce-stop%s'%package_name))
defcurrent_pkg_activity():
"""
获取当前页面的包名和Activity类名
:return:
"""
result=analysis_result(start_cmd(f'adbshelldumpsysactivityactivities|grepmResumedActivity'))
print(result)
ifresultisnotNoneandlen(result)0:
match_result=re.search(pkg_act_pattern,result)
ifmatch_result:
returnmatch_result.group(1),match_result.group(2)
returnNone
defkey_event(event):
"""
模拟按键
:paramevent:按键类型
:return:
"""
returnanalysis_result(start_cmd(f'adbshellinputkeyevent%d'%event.value))
definput_text(text):
"""
当焦点处于某文本框时,模拟输入文本
:paramtext:输入文本内容
:return:
"""
match_result=re.findall(chinese_pattern,text)
#判断是否包含中文
iflen(match_result)0:
returnanalysis_result(start_cmd(f'adbshellambroadcast-aADB_INPUT_TEXT--esmsg%s'%text))
#不包含中文调用原命令
returnanalysis_result(start_cmd(f'adbshellinputtext%s'%text))
defswipe(start_x,start_y,end_x,end_y):
"""
滑动,从起始坐标点滑动到终点坐标
:paramstart_x:起始坐标点x坐标
:paramstart_y:起始坐标点y坐标
:paramend_x:终点坐标点x坐标
:paramend_y:终点坐标点y坐标
:return:
"""
returnanalysis_result(start_cmd(f'adbshellinputswipe%d%d%d%d'%(start_x,start_y,end_x,end_y)))
defclick(x,y):
"""
点击坐标点
:paramx:
:paramy:
:return:
"""
returnanalysis_result(start_cmd(f'adbshellinputtap%d%d'%(x,y)))
defanalysis_result(lines):
"""
将执行结果列表转换外字符串输出
:paramlines:执行结果列表
:return:
"""
result=''
forlineinlines:
result+=line.decode(encoding='utf8')
returnresult
defscreenshot(save_dir=None):
"""
获取手机截图,先截图后拉取(一步达成的方法好像有权限问题)
:return:截图文件的完整路径
"""
sc_name="%d.png"%(int(round(t*1000)))
start_cmd('adbshellscreencap/sdcard/%s'%sc_name)
sc_path=os.path.join(os.getcwd()ifsave_dirisNoneelsesave_dir,sc_name)
start_cmd('adbpull/sdcard/%s%s'%(sc_name,sc_path))
returnsc_path
defscreen_size():
"""
获取屏幕分辨率
:return:屏幕的宽和高
"""
size_result=re.search(size_pattern,analysis_result(start_cmd(f'adbshellwmsize')))
ifsize_result:
returnsize_result.group(1),size_result.group(2)
if__name__=='__main__':
#可以在这里写测试代码
print(screen_size())
读者可以直接copy代码,在写测试代码那里,调下方法试试看~
0x7、实战:某办公软件打卡自动化上面把常用的自动化操作都封装了,接着写个超简单的案例来练练手~
早上上班最怕啥?肯定是迟到啊,那最绝望的场景是什么?对于笔者来说莫过于:
9:00,明明已经到打卡范围了,但却因为没网一直Loading打不上,我也不知道是地铁的问题还是我手机的问题,只能反复打开关闭:飞行模式、定位、办公软件,直至时间变成了9:01,然后弹出迟到是否打卡的提示,那一瞬间千言万语化作一句话...
每逢此刻都会想,如何改变这种情况?第一反应想到的是改定位,TM直接在家里就打上卡,这不美滋滋。
实现思路
① 编写Xposed插件,Hook获取定位相关的API,直接返回公司附近的经纬度;② 换个支持位置穿越的手机,比如联想就支持改变定位地点;但这两种方案都很有可能被检测出来,估计是APP运行环境相关的检测(手机Root了,App被Hook了等) +风控,之前老东家用这个被人事警告过 (人事那边能看到XX异常打卡,使用定位软件啥的)。
综上,这种实现方式不简单、不稳、还有高风险,妥妥滴抛弃,有没有容易、稳定、风险较低的方案呢?
当然有:开启极速打卡+编写自动化脚本,开启方法很简单,打卡 → 设置 → 极速打卡
如图,开启极速打卡后,你在打卡范围内打开办公软件,就能自动打卡。啧啧,那我直接:
手机一直接着电脑,定时到点adb命令打开办公软件
是的,就是这么简单,接着一步步来实现,难点的话就一个,定时任务,如果是Linux系统,直接用Crontab或Celery就好,可惜笔者的电脑是Windows,两种实现方法:
① 手动配置定时任务就是手动添加定时任务,到点执行脚本,先把打开办公软件的脚本写出来:
importadb_util
if__name__=='__main__':
#打开办公软件,然后执行这句代码,拿到应用的包名
#print(adb_util.current_pkg_activity()[0])
package_name="办公应用的包名贴到这"
adb_util.start_app(package_name)
开始菜单搜索:任务计划程序→ 打开后点击 →任务计划程序库→创建基本任务
填下任务名称和描述,点下一步:
触发器选择每天,点下一步:
编辑触发时间,点下一步:
选择启动程序,点下一步:
选择解释器所在路径,填写打卡脚本路径,点下一步:
然后可以看到任务的摘要,点击完成:
接着就可以在任务列表看到我们新建的任务了:
可以右键运行试试运行效果~
接着只需把手机插上,静待明天到点自动打卡~ (记得关掉锁屏、屏幕保持常亮、亮度调最低)
② 使用schedule库schedule是一个轻量级的任务调度库,可以完成每分钟、每小时、每天、周几、特定日期的定时任务。
直接命令行键入pip install schedule安装模块,然后写出定时代码:
importadb_util
importschedule
importtime
package_name="办公软件包名"
defclock_in():
adb_util.start_app(package_name)
if__name__=='__main__':
schedule.every().day.at("08:30").do(clock_in)
whileTrue:
schedule.run_pending()
time.sleep(1)
竟简单如斯!!!另外,以python.exe xxx.py方式启动脚本会弹出一个黑色的命令行控制行窗口,关掉的话会导致程序停止运行。如果觉得碍眼,其实可以使用pythonw.exe xxx.py,以标准WIN32 GUI方式启动,无窗口的Python可执行程序,代码在后台执行。
关闭的话,直接打开任务管理器,定位到Python,然后终结进程即可~
0x8、小结不知不觉就到文尾,读者是否还意犹未尽?本节显示科普了一波ADB相关的姿势,然后带着大家写了一个自动打卡的jio本。
虽然简陋但勉强能用,不过问题也是多多,比如这些:
每天8.30准时打卡,这也太假了吧?adb命令的方式启动,办公软件会不会检测到,然后判定为异常打卡?定时执行任务,但是打卡成没成功我不知道啊,万一公司断电、断网了?我下班也要看办公软件,回信息,一直放公司挂着不太现实,能不能搞个自动登录啊?...等等行吧,那下一节就结合文字OCR和其它工具,来完善这个打卡jio本,让它变得更高大上一些~
声明:自动打卡脚本只是个练手案例,如果真的拿来使用造成的后果使用人自担,笔者自己宁愿迟到也不会用,做人还是要诚实,经常迟到的同学建议早上早点出门,早起地铁不挤还是很香的~
参考文献:
ADB官方文档
图解ADB工作原理
ADB详解
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线