如果你每天也要在手机上重复点十几下才能完成一个操作,比如上班打卡、清理缓存、定时备份相册,那你大概能理解我为什么花了300天折腾出一个自动化助手。简单说,这个东西就是一个跑在手机里的数字管家,我给它定义好规则,它自动点击屏幕、填写内容、判断状态,让手机像有了自己的脑子一样去处理那些机械重复的活。
这个项目从0到1整整磨了300天,中间推翻过两次架构,踩过无数坑,最后稳定运行的时候,我反而有点恍惚:原来手机自动化这件事,难的不是让手机“动起来”,而是让手机“知道什么时候该动、动了之后对不对、错了之后怎么办”,以及“万一搞砸了能不能自己醒过来”。
下面我从项目起源、技术选型、核心模块拆解、迭代记录、问题排查和给后来人的建议几个方面,完整复盘这次开发。文章很长,但都是实打实的过程和代码级细节,适合想自己动手做手机自动化、或者对后台任务调度感兴趣的朋友参考。
1. 项目起源:为什么我决定写一个手机自动化助手
1.1 从“手机越用越累”到“让手机自己干”
我的日常有一个很大的痛点:每天早上到公司,要打开打卡App,等启动动画,点“考勤打卡”,再等它转圈确认;中午要手动清理一次通知栏的垃圾推送;晚上回家前要打开相册,把当天拍的照片同步到私有网盘。这些操作逻辑固定、步骤明确,但每做一次就要消耗我几十秒的注意力和耐心。
一开始我试过市面上现成的自动化工具,比如按键精灵、Tasker、Automate。说实话这些工具确实能跑,但总有几个隔靴搔痒的问题:规则复杂以后界面配置像蛛网一样乱;跨应用操作时偶尔失效;最关键的是它们都是“黑盒”,我想在里面加一个“如果打卡失败就截图并震动提醒我”的逻辑,得去研究它私有语法的边界,实在别扭。
所以第30天时我做了个决定:自己写一个Android上的自动化助手,核心目标只有一个——让手机在没有人工干预的情况下,把我指定的重复操作跑完。这个项目后来被我命名为“AutoRun”,但我更愿意叫它“数字管家”。它不是一个单点的小工具,而是一整套“任务定义 + 触发器 + 执行引擎 + 异常兜底”的体系。
1.2 300天的目标拆解:我先想清楚了三件事
开发这种工具,最忌讳一上来就写代码。我花了将近一周时间,把需求拆成了三个层次:
第一层是“固定流程自动化”。每天定时、按固定顺序执行一组动作,比如打开App、点击按钮、等待结果。这一层解决“重复劳动”的问题。
第二层是“条件感知”。不是所有动作都适合定时,有些任务要等特定条件,比如连接到公司Wi-Fi时才打卡、电量低于20%时才关闭后台同步、收到特定通知时才弹出提醒。这一层解决“什么时候做”的问题。
第三层是“自愈与安全”。自动化跑起来容易,但跑错了怎么办?比如误点了某个不可逆的按钮、连续三次找不到目标节点、网络超时卡在中间状态。这一层解决“出错了不会造成麻烦”的问题。
这三个层次就像盖房子的地基、墙体和屋顶,缺一不可。后来300天的开发过程中,超过一半的时间其实都花在了第三层上,因为一个不能自我纠错的自动化助手,用起来比手动操作更让人提心吊胆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型的纠结与定案
2.1 为什么选了安卓平台 + 无障碍服务这条路线
先聊聊平台选型。排除iOS的原因是权限限制太多,后台任务和跨应用控制根本不是给普通开发者留的口子;排除在PC上跑ADB脚本的方案,是因为我想要的是“手机本体独立运行”,总不能整天挂着电脑,而且ADB那套在无人值守场景下只要USB断开一次就全盘崩。
剩下就是Android原生方案。Android上做“模拟用户操作”有两条路:无障碍服务和无障碍服务,嗯,这句话有点绕,准确说是两条截然不同的路线:一条是辅助功能API层面去拿节点、执行点击;另一条是更低层的输入事件注入。
我最终选择了无障碍服务,也就是AccessibilityService。它有几个关键优势:不需要机器或者ADB连接;服务由系统托管,声明权限后可以常驻;它能读取当前界面的节点树,能拿到控件的文本、描述、可点击状态,远远比盲目的坐标点击更稳定。
当然它也有代价。无障碍服务的设计初衷是给残障用户提供辅助操作,拿来做自动化算是一种“出圈”用法。使用时要格外注意两点:一是只做用户明确授权的自动化任务,不碰任何账号密码之类的敏感信息;二是遵循Android系统对无障碍服务的资源和频率限制,不能无节制地扫描界面。
2.2 核心语言与框架:Kotlin为主,JSON规则为辅
开发语言我选了Kotlin,原因很简单:它是Android的一等公民,协程让异步逻辑清爽很多,而且空安全机制在处理“界面节点可能为null”的场景时能少写一堆防御代码。
自动化任务本身用什么来描述?我的选择是JSON。理由很朴素:第一,规则是数据而不是代码,方便动态下发和修改,不用每次改逻辑都发版本;第二,我可以在外面写一个规则编辑器,生成JSON后导入手机,调试效率高;第三,JSON天然适合结构化,一个任务就是一组触发器加一组动作。
json复制{
"taskId": "daily_checkin",
"name": "每日考勤打卡",
"triggers": [
{
"type": "cron",
"expr": "0 30 8 * * ?"
},
{
"type": "condition",
"condition": "wifi_ssid == CompanyWiFi"
}
],
"actions": [
{ "type": "launch_app", "package": "com.example.checkin" },
{ "type": "wait", "ms": 3500 },
{ "type": "click", "target": { "text": "考勤打卡" }, "timeout": 10 },
{ "type": "wait", "ms": 2000 },
{ "type": "assert", "target": { "text": "打卡成功" } }
]
}
这套JSON规则成了整个项目的“通用语言”。执行引擎只认规则,不关心规则从哪来。我甚至后来在电脑上写了个小网页,拖拽生成规则后扫码导入手机,体验还挺顺。
2.3 调度、存储与前台保活的三板斧
手机自动化最怕的就是“被系统杀掉”。Android的后台限制越来越严格,尤其国产ROM对“自启动”极其敏感。我在第90天左右被这个问题折磨得够呛,最终定下一套组合方案:
- AlarmManager负责定时任务的唤醒,即使应用进程被回收,闹钟照样能拉起广播接收器。
- 前台服务负责执行任务时不被杀,配合一个常驻通知告诉用户“自动化助手正在运行”。
- WorkManager处理一些可以延迟、可重试的后台任务,比如日志上传。
存储层我用SQLite记录每一次任务的执行日志,规则文件则放在应用私有目录,通过SharedPreferences保存少量全局状态。不同任务之间完全隔离,避免一个任务崩溃影响其他任务。
这一套组合下来,实测在MIUI、ColorOS、鸿蒙等主流系统上都能稳定驻留。当然,前提是用户要在系统设置里手动允许自启动和忽略电池优化,这是国产安卓绕不开的一步。
3. 核心模块的实操拆解
3.1 任务定义层:怎么把“需求”翻译成“规则”
我设计了一个三层的数据结构:Task(任务)、Trigger(触发器)、Action(动作)。Task是顶层,包含元信息和两个子列表;Trigger决定“什么时候跑”;Action决定“跑什么”。
每个Task还可以配置一些全局属性,比如最大重试次数(retryCount)、失败策略(onFailure)、任务间互斥锁(mutexKey)。互斥锁这个设计很关键,比如“打卡任务”和“截图任务”同时触发时,避免两个执行引擎并发点击同一个界面,会互相干扰。
动作类型我实现了十来种,常用的就这几个:launch_app(打开应用)、wait(等待)、click(点击文字/描述/坐标)、input(输入文本)、swipe(滑动)、assert(断言校验)、notify(发通知提醒)、screenshot(截屏)。每种动作都支持timeout参数,防止界面加载太慢导致动作提前超时。
写规则时最大的心得是:每个动作都要“可重入”。也就是说,同一个规则跑两次,第二次不应该产生副作用。比如点击“确认”按钮之前,先检查当前界面是否已经处于“已完成”状态,如果是就直接跳过。这样能极大降低重复执行的误操作风险。
3.2 触发器引擎:时间、条件、事件三种触发方式
最初的版本只支持定时触发,用Cron表达式表示“每天8:30跑一次”。但很快我就发现,定时触发有个天然缺陷:如果任务执行时手机正被占用,或者网络还没准备好,硬跑很容易失败。
所以我在第120天引入了条件触发。条件引擎会监听一个“事实表”,里面维护了环境状态:当前Wi-Fi名称、电量百分比、网络连通性、是否在充电、当前日期是否节假日等。每条Condition是一段简单的表达式,比如wifi_ssid == "CompanyWiFi",解析成语法树后去事实表里取真值。
第三类是事件触发,主要靠NotificationListenerService监听通知。比如收到“快递已签收”的通知,就自动截屏留存;收到“内存不足”的系统警告,就自动清理缓存。事件触发的粒度更细,反应更快,但也最容易误触,所以我会给每个事件触发规则加一个“冷却时间”,同一个事件在5分钟内最多触发一次。
3.3 执行引擎:查找节点、模拟点击与状态校验
执行引擎是整个项目的心脏,它的核心逻辑就是一个循环:取一个动作,执行它,判断结果,决定是进下一个还是重试。我在重试机制上花了很大功夫:每个动作可以单独配置retry次数,比如点击动作如果找不到目标文字,每隔1秒重查一次,最多查10秒,超过就抛异常并走上报流程。
查找节点用的是AccessibilityNodeInfo的API。这里有个很重要的经验:不要只盯着目标节点本身,要学会“向上找父节点”。很多控件本身没有clickable属性,但它的父容器或兄弟节点才是真正接收点击事件的地方。我的实现里有一个循环,从目标节点向上遍历最多三层,找到第一个isClickable的节点再执行点击。
kotlin复制fun findClickableNode(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (node.isClickable) return node
var parent = node.parent
var depth = 0
while (parent != null && depth < 3) {
if (parent.isClickable) return parent
parent = parent.parent
depth++
}
return null
}
点击之后还不能立刻继续下一个动作。我实现了一个“自校验机制”:点击动作执行后,等500毫秒,然后检查界面树里是否出现了预期结果。比如打卡之后,断言“打卡成功”文本出现,如果没有,说明点击可能没生效,就触发重试。这一步是自动化稳定性的分水岭,没有自校验的自动化,就像闭着眼睛开车。
3.4 异常兜底:熔断、降级与日志体系
异常处理是我在整个项目里最得意、也最痛苦的部分。我的原则很简单:自动化助手可以失败,但不能造成事故。所以我在执行引擎里加了一个“熔断开关”:同一个任务连续失败3次,自动暂停该任务,并推送一条高优先级通知让用户人工处理。用户手动恢复之前,这类任务不再自动执行。
另一个重要的兜底是“全局看门狗”。一个任务如果执行时间超过预估值(我会给每个任务预估一个duration),看门狗就会强制打断执行流程,并记录“超时中断”。这个看门狗本质上是一个独立的协程,它持有当前执行任务的句柄,能在必要时安全地终止动作循环。
日志体系我是当作核心功能来设计的。每条执行记录包括任务ID、触发方式、开始时间、结束时间、每个动作的执行时长、失败原因、当时的界面节点摘要,以及自动截屏的缩略图。这些日志存在SQLite里,不仅用于排查问题,还可以用来做“规则优化”:比如统计出某个点击动作平均耗时500毫秒,那下次设置等待时间时我就知道要留足800毫秒。
4. 300天里的关键里程碑与迭代记录
4.1 第1~60天:跑通“定时打卡”的完整链路
前60天我只做了一件事:让一个自动化任务稳定跑通“打开App、点击打卡、确认结果”的完整链路。最初版本大概1200行代码,非常粗糙,连日志都没好好做,全靠Toast和Logcat硬扛。但正是这段“只解决一个场景”的时间,让我把无障碍服务的来龙去脉摸清楚了。
这个阶段踩得最深的一个坑是:点击动作执行后,界面状态不一定立刻更新。我以为自己点上了,但系统还在渲染动画,紧接着的assert提前执行,拿到旧状态直接误判成功。这个教训后来催生了“动作间隐式等待+显式校验”的统一模式。简单说就是:每个动作后都有一个默认的界面稳定延迟,再配合自定义校验点。
4.2 第61~180天:从“定时执行”进化到“条件感知”
第二阶段开始加条件触发和事件触发。我把上报里“每天8:30打卡”改成了“8:30之后第一次连上公司Wi-Fi时打卡”,这下就算哪天早上堵车迟到也不会漏打。为了支撑这种“确定一个不早于X时刻的Y事件”的语义,我在触发器引擎里做了一个有趣的组合:Cron触发器提供候选时刻,条件触发器提供“此刻是否正确”,两者同时命中才放行。
这段时间还引入了对话框处理机制。自动化最怕的就是操作到一半突然弹出系统弹窗,比如“允许发送通知吗”“应用无响应,要等待还是关闭”。我写了一个专门的“弹窗拦截器”,监听窗口状态变化,如果检测到系统级弹窗,优先处理掉再继续原任务的剩余动作。这个拦截器后来帮我避免了很多说不清的诡异失败。
4.3 第181~300天:稳定性打磨和功耗治理
最后100天我干了两件事:一是对之前所有规则做“回放测试”,把几百条日志里的失败案例全部导出来,逐个分析根因;二是优化功耗。
功耗问题其实很现实,无障碍服务本身不耗电,但如果逻辑写得不好,频繁唤醒、频繁查节点,电量会肉眼可见地掉。我的优化方向有三个:降低轮询频率,能用事件驱动就不用轮询;不做全界面树扫描,只根据目标文本做定向查找;所有定时任务的唤醒尽量对齐到一个时间窗口,减少系统频繁苏醒的次数。
做完功耗治理后,待机一晚上耗电从原来的8%降到了2%以下。说实话这个数字让我挺有成就感的,因为它意味着这个自动化助手真正达到了“可以常驻”的标准。
5. 高频问题与排查技巧实录
5.1 无障碍服务被系统回收怎么办
这是所有做无障碍自动化的开发者都会遇到的问题。症状是运行几天后任务突然不触发了,进设置一看,无障碍服务已经自动关闭。原因包括内存压力下系统回收服务进程、部分系统策略强制复位等。
我的处理方案分成三层:应用内增加“心跳自检”,每30分钟检查一次服务是否存活,如果发现服务异常就提醒用户;利用一条定期执行的保活任务唤醒进程并重新绑定服务;同时引导用户把应用加入系统不清理的白名单。实测下来,配合这三层,服务连续运行一个月没有被回收过。
5.2 界面元素识别不准
节点查找的经典痛点:不同应用对相同语义的控件可能有完全不同的写法,有的用TextView显示文本,有的用Button,还有的是自定义View根本不在节点树里暴露文本。遇到这种情况,我的经验是退而求其次:用坐标点击。
坐标点击当然不优雅,但它稳定。我会让用户在规则配置阶段手动校准一次坐标,配合“预览当前界面布局”功能,直接在截图上标注点击位置。对于动态列表项,不能写死坐标,而是先找到列表容器节点,再计算目标子项的相对偏移量,这样在不同分辨率下也能基本准确。
5.3 后台被杀和任务漏执行的排查
如果任务完全没有触发,第一步永远是查日志,看AlarmManager到底有没有收到广播。我在日志里为每个定时触发加了一条“schedule_fire”标记,如果连这个标记都没有,说明闹钟都没响过,问题出在系统调度层面;如果有标记但任务没执行,说明在执行前就被条件拦截或者进程被杀。
排查后我发现,漏执行相当大比例是“省电策略”惹的祸。国产系统的省电策略会推迟AlarmManager的精准闹钟。解决方案是把关键任务的闹钟类型从setExactAndAllowWhileIdle改为同时注册一个“用户主动按下的补跑入口”,让用户在方便的时候能手动触发一次。
5.4 高频问题速查表
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 任务完全没触发 | 后台任务被省电策略拦截 | 检查调度日志,加入白名单/忽略电池优化 |
| 点击找不到目标 | 界面节点没有文本 | 改用文本模糊匹配、内容描述或坐标方案 |
| 执行过程中断 | 系统弹窗抢占焦点 | 启用弹窗拦截器,或增加前置弹窗处理步骤 |
| 连续执行误触 | 缺少自校验和互斥锁 | 为任务配置互斥锁,断言增加状态检查 |
| 电量莫名下降 | 轮询太频繁 | 优化为事件驱动,记录唤醒次数 |
| 服务自动关闭 | 内存回收/系统策略 | 前台服务+心跳自检+白名单 |
6. 给后来人的几点实在建议
第一,自动化规则宁少勿多。我见过很多人一上来就想实现“全自动”。实际上,规则越多,出错的叠加概率越高。先把三条最高频、最固定的流程跑稳,再去扩展。一个能稳定跑1个月的10条规则,远比一个能跑3天就崩的50条规则有价值。
第二,全局熔断开关必须在一开始就设计进去。这个开关和数量无关,它是安全底线。我的实现是一个悬浮窗快捷按钮,无论任何时候点一下,所有任务立即暂停执行,并且停止一切自动动作。它不是给用户用的,是给“意外”留的后门。
第三,日志越“啰嗦”越好。很多自动化工具的项目代码里,日志是最被忽视的部分。但我300天的经验告诉我,排查“为什么不生效”的时候,日志比代码更有用。我甚至在每个动作的开始和结束都打日志,算上耗时。这种啰嗦的日志,在后期调优时是宝藏。
第四,不要依赖单一的定时触发。条件触发和事件触发带来的体验提升是巨大的。定时触发只回答“什么时间”,条件触发回答“什么情况”,两者结合才能应对真实世界的复杂性。我最后悔的就是没有更早引入条件引擎,前60天里至少有一半的失败案例,其实是条件不满足导致的。
最后一个小技巧:多做回放测试。我会定期导出所有任务的历史执行日志,挑出失败案例,然后构造同样的界面状态让执行引擎重新跑一遍。这种“录像回放式调试”比凭空看代码找bug高效得多。说白了,自动化助手就是一个不断拿现实反馈来校准自己判断的系统,校准得越勤,它就越懂你的手机。
我个人在实际操作中的体会是:花300天做一个自动化助手,看似漫长,其实大部分时间都花在理解“手机到底是怎么被系统管着的”这件事上。每一次系统升级、每一个国产ROM的省电策略变化,都会让已有的规则失效。所以如果让我再选择一次,我会把架构设计得更“防御性”,从一开始就把“异常、日志、熔断”当成和“点击、滑动”同等地位的核心能力来对待。希望这篇复盘能让你少走一些弯路,早点让你的手机学会自己“工作”。
