本文为稀土技术社区首发签约文章,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\xxx>netstat -aon|findstr 5037
TCP 127.0.0.1:5037 0.0.0.0:0 LISTENING 2908
键入:tasklist /fi "PID eq 2908"
,检查 进程PID对应的进程,如:
C:\Users\xxx>netstat -aon|findstr 5037
映像称号 PID 会话名 会话# 内存运用
========================= ======== ================ =========== ============
xxx.exe 2908 Console 1 11,292 K
键入:taskkill /pid 2908 /f
,杀掉占用端口的进程,如:
C:\Users\xxx>taskkill /pid 2908 /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|-s <serialNumber>] <command>
假如 只要一个设备/模仿器衔接PC,不必加中括号里的参数,当有多个时,才需求经过这些参数 指定方针设备:
-
-d
→ 指定当时仅有经过usb衔接的Android设备为指令方针; -
-e
→ 指定当时仅有运转的模仿器为指令方针; -
-s serialNumber
→ 指定对应serialNumber号的设备/模仿器为指令方针,最常用;
上面 adb devices
输出的 8c8f689e 便是我当时衔接的手机 序列号,除了真机还有 模仿器 和 无线衔接设备,如:
emulator-5554 device
10.129.164.6:5555 device
别的,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终端的指令行模式
adb shell
# 输出前台Activity
dumpsys activity activities | grep mResumedActivity
成果:
② 发动运用/调起Activity
指令:
adb shell
# 不指定Activity称号发动,即发动主Activity
monkey -p <packagename> -c android.intent.category.LAUNCHER 1
# 指定发动Activity名 (需求root权限)
am start -n <packagename>/<activity类名>
成果:
③ 强行中止运用
指令:
adb shell am force-stop <packagename>
成果:
④ 模仿按键/输入/滑动/点击
指令 (完好Keycode列表可见官网:KeyEvent):
adb shell
# 模仿按键 (Home-3,返回-4,电源-26,亮屏-224、熄屏-223,切换运用-187,小键盘删去-67),如点击Home键:
input keyevent 3
# 在焦点处于某文本框时,运用input指令输入文本
input text Hello
# 滑动,从开始坐标点滑动到结束坐标点,如上滑(300,1000) → (300,500):
input swipe 300 1000 300 500
# 点击坐标点
input tap 500 500
Tips:adb默许不支撑Unicode编码,所以无法input中文内容,即便你默许运用了支撑中文的输入法也不行,处理办法如下:
- ① 下载装置 ADBKeyBoard;
- ② 装置完后依次翻开:手机设置 → 语言和输入法 → 键盘 → 虚拟键盘 → 管理键盘 → 启用ADB keyboard;
- ③ 默许输入法 设置为ADB keyboard (比方原生体系点击右下角小键盘设置)
- ④ 接着运用这个指令即可输入中文 (其他语言也能够):
adb shell am broadcast -a ADB_INPUT_TEXT --es msg '中文输入'
⑤ 截图
指令:
adb shell
# 截图
screencap -p /sdcard/sc.png
# 退出adb shell
exit
# 导出到电脑
adb pull /sdcard/sc.png
别的,还有一种一行指令截图并保存到电脑的办法:
# Mac
adb shell screencap -p | gsed "s/\r$//" > sc.png
# Linux和Windows
adb shell screencap -p | sed "s/\r$//" > sc.png
# Tips:上面的截图办法,Windows能获取图片,可是打不开,能够用下述办法获取:
adb exec-out screencap -p > sc.png
⑥ 检查分辨率
指令:
adb shell wm size
成果:
0x5、Android App中调用adb指令
不是很引荐 这种办法弄主动化,第一个是 权限问题,假如你的主动化设备是有 Root权限的真机或模仿器,在考虑这种吧。
别的一个问题是 保活问题,一般咱们的主动化场景,都是 守时去做一些工作,比方我想每天早上8点半主动打卡,那我需求写一个后台服务,然后让它 一向挂着, 到点触发主动打卡的操作。这个 保活 在PC上很简略完结,但在手机里却很难确保…
写个简略的调用示例吧,有需求的改改就能用,先是布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns: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中的调用代码:
class TestCmdActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_cmd)
findViewById<TextView>(R.id.et_input).text =
"monkey -p com.tencent.mobileqq -c android.intent.category.LAUNCHER 1"
findViewById<Button>(R.id.bt_run).setOnClickListener {
val result = execCmd(findViewById<EditText>(R.id.et_input).text.toString())
if (result != null) findViewById<TextView>(R.id.tv_output).text = result
}
}
/**
* 履行普通指令 (不需求adb shell)
* */
private fun execCmd(cmd: String?): String? {
return if (cmd.isNullOrBlank()) {
shortToast("指令不能为空")
null
} else {
try {
val sb = StringBuffer()
val process = Runtime.getRuntime().exec(cmd)
val inputStream = process.inputStream
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
val buff = CharArray(1024)
var ch: Int
while (true) {
ch = bufferedReader.read(buff)
if (ch == -1) break
sb.append(buff, 0, ch)
}
process.waitFor()
bufferedReader.close()
return sb.toString()
} catch (e: IOException) {
e.toString()
}
}
}
/**
* 履行需求Root权限的指令
* */
private fun execCmdRoot(cmd: String?): String? {
return if (cmd.isNullOrBlank()) {
shortToast("指令不能为空")
null
} else {
val successMsg = StringBuffer()
val errorMsg = StringBuffer()
var process: Process? = null
var successResult: BufferedReader? = null
var errorResult: BufferedReader? = null
var os: DataOutputStream? = null
try {
process = Runtime.getRuntime().exec("su")
os = DataOutputStream(process.outputStream)
os.write(cmd.toByteArray())
os.writeBytes("\n")
os.flush()
val result = process.waitFor()
successResult = BufferedReader(InputStreamReader(process.inputStream))
errorResult = BufferedReader(InputStreamReader(process.errorStream))
var s: 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)
}
return successMsg.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
模块调一下指令行而已,十分简略,直接给出完好代码:
import subprocess
import re
from enum import Enum
import time
import os
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()
class KeyEvent(Enum):
"""
按键事件的枚举
"""
HOME = 3
BACK = 4
POWER = 26
SCREEN_ON = 224
SCREEN_OFF = 223
SWITCH_APP = 187
DELETE = 67
def start_cmd(cmd):
"""
履行指令
:param cmd: 指令字符串
:return: 履行后的输出成果列表
"""
print(cmd)
proc = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE)
return proc.stdout.readlines()
def start_app(package_name):
"""
发动APP
:param package_name: 运用包名
:return: 履行成果字符串
"""
return analysis_result(start_cmd(f'adb shell monkey -p %s -c android.intent.category.LAUNCHER 1' % package_name))
def kill_app(package_name):
"""
杀掉APP
:param package_name: 运用包名
:return:
"""
return analysis_result(start_cmd(f'adb shell am force-stop %s' % package_name))
def current_pkg_activity():
"""
获取当时页面的包名和Activity类名
:return:
"""
result = analysis_result(start_cmd(f'adb shell dumpsys activity activities | grep mResumedActivity'))
print(result)
if result is not None and len(result) > 0:
match_result = re.search(pkg_act_pattern, result)
if match_result:
return match_result.group(1), match_result.group(2)
return None
def key_event(event):
"""
模仿按键
:param event: 按键类型
:return:
"""
return analysis_result(start_cmd(f'adb shell input keyevent %d' % event.value))
def input_text(text):
"""
当焦点处于某文本框时,模仿输入文本
:param text: 输入文本内容
:return:
"""
match_result = re.findall(chinese_pattern, text)
# 判别是否包括中文
if len(match_result) > 0:
return analysis_result(start_cmd(f'adb shell am broadcast -a ADB_INPUT_TEXT --es msg %s' % text))
# 不包括中文调用原指令
return analysis_result(start_cmd(f'adb shell input text %s' % text))
def swipe(start_x, start_y, end_x, end_y):
"""
滑动,从开始坐标点滑动到结尾坐标
:param start_x: 开始坐标点x坐标
:param start_y: 开始坐标点y坐标
:param end_x: 结尾坐标点x坐标
:param end_y: 结尾坐标点y坐标
:return:
"""
return analysis_result(start_cmd(f'adb shell input swipe %d %d %d %d' % (start_x, start_y, end_x, end_y)))
def click(x, y):
"""
点击坐标点
:param x:
:param y:
:return:
"""
return analysis_result(start_cmd(f'adb shell input tap %d %d' % (x, y)))
def analysis_result(lines):
"""
将履行成果列表转换外字符串输出
:param lines: 履行成果列表
:return:
"""
result = ''
for line in lines:
result += line.decode(encoding='utf8')
return result
def screenshot(save_dir=None):
"""
获取手机截图,先截图后拉取(一步达成的办法好像有权限问题)
:return: 截图文件的完好途径
"""
sc_name = "%d.png" % (int(round(t * 1000)))
start_cmd('adb shell screencap /sdcard/%s' % sc_name)
sc_path = os.path.join(os.getcwd() if save_dir is None else save_dir, sc_name)
start_cmd('adb pull /sdcard/%s %s' % (sc_name, sc_path))
return sc_path
def screen_size():
"""
获取屏幕分辨率
:return: 屏幕的宽和高
"""
size_result = re.search(size_pattern, analysis_result(start_cmd(f'adb shell wm size')))
if size_result:
return size_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,两种完结办法:
① 手动装备守时使命
便是手动添加守时使命,到点履行脚本,先把翻开作业软件的脚本写出来:
import adb_util
if __name__ == '__main__':
# 翻开作业软件,然后履行这句代码,拿到运用的包名
# print(adb_util.current_pkg_activity()[0])
package_name = "作业运用的包名贴到这"
adb_util.start_app(package_name)
开始菜单查找:使命计划程序 → 翻开后点击 → 使命计划程序库 → 创建基本使命
填下使命称号和描绘,点下一步:
触发器挑选每天,点下一步:
修改触发时间,点下一步:
挑选发动程序,点下一步:
挑选解释器地点途径,填写打卡脚本途径,点下一步:
然后能够看到使命的摘要,点击完结:
接着就能够在使命列表看到咱们新建的使命了:
能够右键运转试试运转作用~
接着只需把手机插上,静待明天到点主动打卡~ (记住关掉锁屏、屏幕保持常亮、亮度调最低)
② 运用schedule库
schedule是一个轻量级的使命调度库,能够完结每分钟、每小时、每天、周几、特定日期的守时使命。
直接指令行键入 pip install schedule
装置模块,然后写出守时代码:
import adb_util
import schedule
import time
package_name = "作业软件包名"
def clock_in():
adb_util.start_app(package_name)
if __name__ == '__main__':
schedule.every().day.at("08:30").do(clock_in)
while True:
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详解