
写安卓自动化脚本写到一定阶段坐标和控件这两件事是绕不过去的。Auto.Js 给了我们两把钥匙一把是直接往屏幕上某个点戳下去的坐标派另一把是顺着无障碍节点树摸到控件再动手的控件派。这一章要解决的核心问题就一个——怎么让脚本点得准同时点得像个人在操作。真实场景里我们面对的往往是分辨率五花八门的设备、随时会变的控件 ID、还有那些一看就是机器人刷出来的固定节拍。模拟真人操作不是玄学它是一堆具体工程的集合坐标换算、控件树容错、随机延时、轨迹曲线、节奏控制。这份内容适合已经能跑通第一个 Auto.Js 脚本、但发现脚本总是点歪或者跑两天就被识别的人也适合刚接触自动化、想一次性把坐标体系搞明白的新手。下面我按自己这几年的实际踩坑顺序把坐标、控件、拟人化这三块拆开讲。1. 坐标派还是控件派先把这个选择题做对刚上手的人最容易犯的错是把坐标和控件当成互斥的两种写法非此即彼。实际情况是这两套东西覆盖的页面范围不一样选错了不是写不出来而是写出来的东西换个手机就跑不动。1.1 坐标点击的适用边界在哪坐标点击就是click(x, y)这一套它的本质是往系统的输入子系统里注入一个屏幕绝对坐标的触摸事件。优点是快、无依赖、不受控件树结构影响任何画在屏幕上的东西它都能点包括游戏画面、Canvas 绘制的图表、原生相机预览这些压根没有无障碍节点的界面。缺点也很直接它对屏幕尺寸和状态极度敏感设计分辨率是 1080×2340 的机型上量出来的(540, 1980)换到 720×1600 的机器上就点到了别的地方同尺寸机型之间状态栏高度、导航栏是手势还是三键、有没有挖孔都会让同一个坐标偏移几十像素。我的判断标准很朴素如果目标界面能用控件定位就绝不用坐标只有在控件树完全拿不到信息的时候坐标才是主角。典型必须用坐标的场景有这么几类——游戏类应用、Flutter 或 Unity 渲染的界面、视频播放器的进度条拖拽区、还有那种整页就是一个 WebView 但内部 H5 没做无障碍标记的页面。反过来说原生 Android 应用里的按钮、输入框、列表项九成以上都能通过控件找到硬用坐标属于给自己找麻烦。1.2 控件定位强在哪代价是什么控件定位走的是无障碍服务那条路text(登录).findOne()这种写法实际执行的是遍历当前窗口的节点树按条件筛选出对应的节点然后把点击事件派发到这个节点的中心或者可点击父节点上。它最大的好处是语义化你描述的是我要点那个写着登录的按钮而不是我要点第 540 列第 1980 行那个像素。这意味着分辨率变了、按钮位置挪了脚本大概率还是能跑。代价主要有三块。一是性能遍历节点树是跨进程调用一次全量find()在复杂页面上可能要几十到几百毫秒如果在一个循环里反复查脚本会明显变慢。二是稳定性很多应用的控件 ID 是混淆过的今天叫btn_a1下个版本变成btn_zz纯靠id()定位的脚本会在某次更新后集体失效。三是覆盖范围前面说的游戏、Canvas、部分 WebView 内容节点树里可能只有一个空壳容器你在里面找不到任何有用的子节点。所以实操中我更倾向于用文本和描述desc作为主锚点id 作为兜底这两类信息通常比混淆 ID 稳定得多。1.3 我实际在用的混合策略真实项目里我一般按这个优先级往下掉先试text()或desc()精确定位找不到就退一步用textContains()做模糊匹配再找不到就用className()圈定一类容器然后按bounds()的位置关系挑出我要的那一个最后才是硬编码坐标兜底。这个阶梯式降级的写法值得在项目初期就固化成一个工具函数后面所有页面都复用改起来只改一处。还有一个组合玩法值得单独提一句就是截屏找图找色加坐标点击。当控件树失效但画面稳定的时候可以请求截屏权限用images.findImage()或者images.findColor()在指定区域内搜特征拿到返回的坐标点之后再click()。这条路比死坐标灵活因为它跟着画面走代价是每次都要截屏一帧截图在中等性能机型上大概 100 到 300 毫秒而且不同机型渲染有细微色差找色要用threshold参数放宽容差找图则建议把模板裁小一点、只保留特征最明显的部分匹配阈值设在 0.85 到 0.95 之间比较稳。这套方案适合少数几个关键节点做兜底不适合整条流程都这么干。2. 坐标系、分辨率和适配公式一次算清楚坐标派最大的坑不是写不出来是换台手机就歪。这一节把坐标系的原点、单位、换算关系捋一遍后面写适配就是套公式的事。2.1 Auto.Js 的坐标原点和取值规则Auto.Js 里click(x, y)用的是屏幕绝对像素坐标原点在屏幕物理左上角x 向右递增y 向下递增。这里有几个细节新手经常搞混。第一这个原点包含状态栏区域也就是说状态栏下方的第一个像素y 值并不是 0而是等于状态栏的高度通常在 60 到 120 像素之间浮动。第二device.width和device.height拿到的是屏幕的物理像素尺寸不是 dp 也不是逻辑分辨率所以它和你在设计稿上看到的数字可以直接对应。第三控件返回的bounds()也是同一套坐标系b.centerX()和b.centerY()拿到的中心点可以直接喂给click()两者不需要任何转换。需要特别注意刘海屏和挖孔屏。这类机型的状态栏高度是不对称的横屏游戏里尤其明显。我的习惯是先用device.width、device.height打印一次实际尺寸再用系统的指针位置开发者选项去核对几个关键点的坐标确认原点和自己的预期一致再开始写业务逻辑。这一步花两分钟能省掉后面半小时的为什么点偏了。2.2 三种适配思路从糙到细思路一只做比例缩放。记录设计稿尺寸运行时按实际屏幕算比例const DESIGN_W 1080; const DESIGN_H 2340; function sx(x) { return Math.round(x * device.width / DESIGN_W); } function sy(y) { return Math.round(y * device.height / DESIGN_H); } // 用法设计稿上量到 (540, 1980) click(sx(540), sy(1980));这个写法简单直接但只适合宽高比接近的机型。1080×2340 是 19.5:9如果换到一台 20:9 的机器上纵向会有额外拉伸越靠近屏幕底部偏差越大。思路二用相对比例代替绝对像素。直接把坐标写成屏幕宽高的百分比// 屏幕水平居中、垂直 85% 的位置 click(device.width * 0.5, device.height * 0.85);这个写法对宽高比变化的容忍度更高因为底部导航栏、顶部状态栏在比例上的占位相对稳定。缺点是定位精度不如绝对坐标适合点击区域比较大的元素比如整块卡片、整条列表项。思路三交给 setScreenMetrics。Auto.Js 提供了一个专门做这件事的接口// 告诉脚本我接下来写的坐标都基于 1080 x 2340 setScreenMetrics(1080, 2340); // 下面这两个调用会自动换算到当前设备的实际分辨率 click(540, 1980); swipe(540, 1600, 540, 600, 500);调用之后所有点击、滑动、按压类的 API 都会自动按实际屏幕尺寸做等比缩放脚本里可以一直写设计稿的绝对值可读性最好。但这里有个必须记住的坑setScreenMetrics只影响点击类 API不影响控件返回的bounds()。也就是说如果你先调了setScreenMetrics然后又把bounds().centerX()直接丢给click()那这个坐标会被再缩放一次结果就是往左上角偏。混用的时候要么全程用控件坐标、不调这个函数要么用sx()/sy()把控件坐标反向换算回设计稿坐标系这一点我当年调了整整一个下午才反应过来。2.3 宽高比差异大的时候怎么补遇到平板、折叠屏展开态这类宽高比差异巨大的设备纯比例缩放会失效。这时候我的做法是放弃全局适配改成按界面区块定位。举个例子一个底部悬浮的确认按钮与其算它的绝对 y 坐标不如先用控件拿到页面容器的bounds()然后用容器的底边减去一个固定的像素内边距去定位按钮。这种相对某个已知元素定位的思路比任何全局换算公式都可靠因为它跟着界面布局本身走而不是跟着屏幕尺寸走。3. 控件定位的核心选择器和节点树控件这一块Auto.Js 的选择器设计其实相当讲究但文档里讲得比较散。我按实际用得到的顺序重新组织一遍。3.1 常用选择器速查和组合打法选择器本质上是一组筛选条件的链式叠加每次调用都在已有条件上继续加约束。它们分成两类一类是文本类text()、textContains()、textStartsWith()、textEndsWith()、textMatches()对应的还有一套desc()系列用于描述信息另一类是属性类id()系列、className()系列、clickable()、enabled()、checked()、scrollable()、editable()、visibleToUser()、boundsInside()等等。选择器匹配方式适用场景text(确定)完全相等文案固定的按钮textContains(确定)包含子串文案前后带空格或序号textMatches(/^确\s*定$/)正则文案有细微变动desc(返回)完全相等图标按钮无文字有描述idEndsWith(btn_ok)后缀匹配包名前缀会变的控件className(android.widget.EditText)类名圈定输入框clickable(true).scrollable(true)属性组合找可滑动的可点击容器实际写的时候条件加得越多越精确但也越脆。我的经验是两到三个条件最合适一个语义锚点文本或描述一个类型约束类名或可点击性必要时再加一个位置约束boundsInside()限定在某个区域内。三层往上基本就是把自己焊死了。还有一个高频技巧是idEndsWith()。很多应用在 Debug 和 Release 包里控件 ID 前面的包名是不一样的但后半段的自定义名字通常保留。用idEndsWith(btn_confirm)就比id(com.xxx.debug:id/btn_confirm)稳得多。3.2 从控件到坐标bounds、parent 和 child拿到 UiObject 之后点击只是最基础的用法。真正让脚本变灵活的是节点之间的导航能力// 找到商品名称这个文本然后点它所在的那一行 const title text(商品名称).findOne(3000); if (title) { // 往上找三层通常是列表项的容器 let row title; for (let i 0; i 3; i) { row row.parent(); if (!row) break; } if (row) { const b row.bounds(); click(b.centerX(), b.centerY()); } }这里bounds()返回的是一个矩形对象left、top、right、bottom是四个边界值centerX()、centerY()是中心点width()、height()是尺寸。除了parent()还有child(i)按索引取子节点、children()取全部子节点、childCount()取数量、depth()取层级深度、indexInParent()取在兄弟节点中的位置。这些接口的价值在于把一个稳定锚点扩展到不稳定区域。比如列表项里的删除按钮它自己可能没有文本也没有 ID但它旁边有个稳定的订单号文本节点你就可以从那个文本出发往上找到行容器再按child()索引或者bounds()的左右关系挑出删除按钮。这比硬编码坐标可靠得多。注意child()的索引在列表滚动后会变别把索引当成永久有效的常量每次操作前重新查一遍。3.3 waitFor、untilFind、exists 到底该用哪个这三个接口经常被混用但它们的行为差别挺大用错了轻则卡脚本重则直接超时报错。exists()是一次性检查立刻返回 true 或 false不等待。适合如果弹窗出现了就关掉没出现就继续这种分支判断。findOne(timeout)会在超时时间内反复查找找到就返回对象超时返回 null。超时值建议永远显式写上不写的话在某些版本里会一直等下去脚本就僵在那里了。waitFor(timeout)返回布尔值只在等某个东西出现的时候用它不返回对象所以通常还要再findOne()一次才行。untilFind()返回一个集合会一直等到有结果为止适合等列表渲染出来这种确定会出现的场景。实战里我常用的封装长这样function tapByText(txt, timeout) { timeout timeout || 5000; const sel text(txt); if (sel.exists()) { const w sel.findOne(1000); if (w) { const b w.bounds(); click(b.centerX(), b.centerY()); return true; } } // 等一会儿再试一次给页面渲染留时间 sleep(400); if (sel.waitFor(timeout)) { const w sel.findOne(1000); if (w) { const b w.bounds(); click(b.centerX(), b.centerY()); return true; } } return false; }先exists()快查一遍是性能优化页面已经渲染好的时候直接命中省掉等待逻辑没命中再进等待分支。这个小改动在批量操作的脚本里能省下相当可观的时间。4. 模拟真人操作把机械动作改成有呼吸的坐标准了、控件找到了脚本能跑通但这只是及格线。接下来才是真正决定脚本能活多久的部分——操作特征。系统和人眼识别自动化的方式很多最粗暴也最有效的一条就是看节拍。4.1 随机化延时、偏移、按压时长固定sleep(1000)是最大的破绽。一台机器精确到毫秒地每隔 1000 毫秒点一次屏幕连续点五十次这个行为特征太干净了干净得不像人。人手的操作间隔是波动的而且波动不是均匀分布是集中在某个值附近的正态分布——偶尔快一点偶尔慢一点但极少极端值。均匀随机已经比固定值强很多// 均匀分布1.2 秒到 2.8 秒之间随机 sleep(random(1200, 2800));但更接近真人的是正态分布// 均值 1800ms标准差 400ms 的正态分布随机 function gaussSleep(mean, sd) { let u 0, v 0; while (u 0) u Math.random(); while (v 0) v Math.random(); const z Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); const ms Math.round(mean z * sd); // 掐掉极端值避免出现负数或者离谱的长等待 sleep(Math.max(300, Math.min(ms, mean 3 * sd))); }这个函数我用得非常多因为它生成的间隔天然带着人的节奏感大部分操作集中在 1.5 到 2 秒偶尔来个 1 秒的快手偶尔来个 3 秒的犹豫。坐标抖动是第二个必做的。手指点击从来不会落在绝对精确的同一个像素上人类点击是围绕目标中心的一个小范围散布。实测下来对于 100 到 200 像素见方的大按钮±8 到 ±15 像素的抖动完全不影响命中但能让点击分布看起来自然很多function humanClick(x, y, jitter) { jitter jitter || 8; const tx x random(-jitter, jitter); const ty y random(-jitter, jitter); press(tx, ty, random(60, 160)); }这里我用press()而不是click()因为press()允许我控制按压时长。真人的轻点大约在 60 到 160 毫秒之间而click()的下压时长是固定的。注意抖动幅度要跟目标元素的大小挂钩。如果按钮只有 40 像素宽±15 的抖动就有一半概率点到外面去了这时候要么把抖动收到 ±4 以内要么干脆不抖改用别的拟人化手段。4.2 滑动轨迹贝塞尔曲线加缓动如果说点击的拟人化是抖动那滑动的拟人化就是轨迹。swipe(x1, y1, x2, y2, duration)这个接口走的是直线而且速度基本匀速。人的手指滑动是带弧度的而且起手快、收尾慢——因为手指贴近屏幕的瞬间速度最高抬起来之前会减速。这两点都很好模拟。先用贝塞尔曲线生成带弧度的路径再用缓动函数调整点的分布密度// 二阶贝塞尔给定控制点求参数 t 处的坐标 function bezier(t, p0, p1, p2) { const mt 1 - t; return mt * mt * p0 2 * mt * t * p1 t * t * p2; } // 缓出t 越大变化越慢模拟收尾减速 function easeOutQuad(t) { return t * (2 - t); } function humanSwipe(x1, y1, x2, y2, duration) { duration duration || random(420, 780); // 控制点放在两点中间横向随机偏移让轨迹轻微弯曲 const cx (x1 x2) / 2 random(-80, 80); const cy (y1 y2) / 2 random(-60, 60); // 采样点数量也随机化16 到 26 个都够用 const steps 16 random(0, 10); const points []; for (let i 0; i steps; i) { const raw i / steps; const t easeOutQuad(raw); const px bezier(t, x1, cx, x2) random(-2, 2); const py bezier(t, y1, cy, y2) random(-2, 2); points.push([Math.round(px), Math.round(py)]); } // 用 gesture 走完整条路径 gesture.apply(null, [duration].concat(points)); }gesture()的第一个参数是整段手势的总时长后面跟一长串坐标点它会按顺序经过每一个点。用apply展开是因为点数不固定。这段代码一次性解决了三个问题轨迹有弧度、速度有变化、每次路径都略有不同。注意滑动距离越短控制点的偏移量要越小。如果只是滑 200 像素横向偏移还设 ±80轨迹会夸张到看起来像画圈反而更假。我一般让偏移量跟滑动距离挂钩取距离的 5% 到 12%。还有一个细节是惯性。真人滑动列表时手指离屏之后列表还会继续滑一段。如果你的脚本需要滚动到列表底部别用固定次数的humanSwipe死循环改成边滑边检查目标元素是否出现出现就停function scrollUntilText(txt, maxTry) { maxTry maxTry || 12; for (let i 0; i maxTry; i) { if (text(txt).exists()) return true; humanSwipe(device.width / 2, device.height * 0.75, device.width / 2, device.height * 0.30); gaussSleep(1200, 300); } return false; }这样既避免了滑过头也避免了滑不到位而且每次滑动的时长和轨迹都不一样。4.3 长按、多指和操作节奏的整体把控长按用press(x, y, duration)就够了时长按场景给普通长按菜单 500 到 800 毫秒拖拽起始 800 到 1200 毫秒。别用longClick()它的时长是系统定的不好控。多指操作要用gestures()它接收一组手势定义每个手势前面可以带一个起始延迟。比如双指捏合缩放就是两个方向相反的手势同时进行。这块在实际项目里用得不多但如果要处理地图缩放、图片查看器这类界面是绕不过去的。写的时候注意两点一是两个手指的路径长度要略有差异完全对称的动作很假二是捏合完成后要留一点停顿再收手别马上接下一个动作。节奏把控是更高一层的东西。单次操作拟人化不等于整段流程拟人化。人做一件事是有节奏起伏的连续操作几步会停一下看看结果遇到需要思考的地方停顿更长快结束的时候动作会变潦草。我在脚本里会刻意插入几种不同量级的停顿操作级 800 到 2000 毫秒步骤级 2500 到 5000 毫秒段落级 8 到 20 秒。而且这些停顿的位置不是固定的用随机数决定哪一步后面插入长停顿。真正让脚本经得起长期运行的是每次运行的整体耗时都不一样。如果每次跑完都是 3 分 12 秒哪怕中间每个动作都随机了整段流程的时长特征依然很扎眼。我的做法是在主流程末尾加一个随机收尾等待让总时长在一个区间内浮动比如 3 分 10 秒到 4 分 40 秒之间。5. 从测量到落地完整实操流程理论讲完了说一遍我实际接一个新页面时的完整动作顺序。5.1 环境和调试窗口先备好第一步是把调试环境搭起来。Auto.Js 里console.show()打开悬浮日志窗toast()做轻量提示log()打详细日志。我的习惯是脚本开头统一做无障碍检查auto.waitFor(); // 没有开无障碍会跳到设置页 console.show(); // 打开日志悬浮窗 log(屏幕尺寸 device.width x device.height); console.setSize(device.width * 0.9, device.height * 0.35);另外系统的开发者选项里有两个功能必须打开指针位置用来看当前触摸点的实时坐标布局边界用来看控件的可视范围。这两个配合 Auto.Js 自带的布局分析工具悬浮窗里能调出来定位效率能提高一倍以上。5.2 量坐标和抓控件信息的具体做法量坐标的正确姿势是先截一张全屏图用系统截图工具或者images.captureScreen()拿到位图然后对照指针位置逐个确认。别靠肉眼估屏幕上有几十像素的误差很常见。量到之后立刻记下来同时标注这个点距离屏幕左边和上边的比例方便后面做适配。抓控件信息用一段遍历脚本把当前页面上有价值的节点全打出来// 打印当前所有带文字的控件 textMatches(/./).find().forEach(function (w) { const b w.bounds(); log([ text w.text(), id w.id(), cls w.className(), clickable w.clickable(), bounds[ b.left , b.top , b.right , b.bottom ] ].join( | )); });跑一遍日志里就能看到这个页面上所有能用来定位的锚点。我一般会把这段输出复制出来人工挑几个最稳定的当锚点把不稳定的比如带序号的文本、带时间的描述排除掉。5.3 一套可复用的工具函数集每次新项目我都会先建一个工具文件把这几样东西放进去。这样业务代码写起来干净调整拟人化参数也只改一个地方。// ---------- 坐标系 ---------- const DESIGN_W 1080, DESIGN_H 2340; setScreenMetrics(DESIGN_W, DESIGN_H); // ---------- 随机工具 ---------- function gauss(mean, sd) { let u 0, v 0; while (u 0) u Math.random(); while (v 0) v Math.random(); const z Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); return mean z * sd; } function gaussSleep(mean, sd) { sleep(Math.max(200, Math.round(gauss(mean, sd)))); } // ---------- 拟人点击 ---------- function humanClick(x, y, jitter) { jitter jitter || 8; press(x random(-jitter, jitter), y random(-jitter, jitter), random(60, 160)); } // 点控件点不到返回 false function humanTapWidget(sel, jitter) { const w sel.findOne(3000); if (!w) return false; const b w.bounds(); if (b.width() 0 || b.height() 0) return false; // 抖动幅度不超过控件尺寸的四分之一 const j Math.min(jitter || 8, Math.floor(Math.min(b.width(), b.height()) / 4)); humanClick(b.centerX(), b.centerY(), j); return true; } // ---------- 拟人滑动 ---------- // 见 4.2 的 humanSwipe 实现这套东西里最值得说的是humanTapWidget里那行抖动幅度限制。这个细节能避免控件找到了但点到了外面这种很隐蔽的 bug——大按钮上抖得爽小图标上就翻车。5.4 主流程组装把上面的零件拼起来一个完整的操作段落大概长这样auto.waitFor(); console.show(); // 第一步进页面 if (!humanTapWidget(text(我的))) { log(找不到入口走坐标兜底); humanClick(device.width * 0.9, device.height * 0.95); } gaussSleep(2000, 500); // 第二步等待目标出现最多等 8 秒 if (text(任务中心).waitFor(8000)) { humanTapWidget(text(任务中心)); gaussSleep(2500, 600); } else { log(页面没加载出来跳过); } // 第三步列表滚动查找 if (scrollUntilText(领取, 10)) { humanTapWidget(text(领取)); gaussSleep(1500, 400); } // 收尾随机停留让总时长浮动 gaussSleep(4000, 1500); log(本轮结束);这段代码里能看到的拟人化处理有四处控件定位优先、坐标兜底、正态分布延时、滑动查找代替固定次数滚动。写业务逻辑的时候把每个动作都按这个模式套一遍就行。6. 常见问题和排查速查表这一节是我这些年攒下来的问题清单基本都是踩过之后才记住的。6.1 控件找不到的六种情形现象可能原因处理方式页面明明有按钮findOne返回 null页面还没渲染完加result.waitFor()或者先sleep(500)有时找得到有时找不到控件在动画中位置在变等动画结束一般 300 到 600 毫秒再查换台手机就找不到ID 带包名前缀或文案有差异改用idEndsWith()/textContains()找到了但点不动点击落在不可点击的子节点上往上找clickable(true)的父节点再点整个页面只有一个 WebView 节点H5 没做无障碍标记退回坐标方案或截屏找图弹窗遮挡导致点击无效前置弹窗没关闭每次操作前先做一次弹窗清理弹窗清理这个习惯值得单独强调。我在每个主要步骤之前都会插一段扫雷检查有没有常见的关闭按钮function closePopups() { const killers [ text(跳过), text(关闭), text(以后再说), desc(关闭), idEndsWith(close_btn) ]; killers.forEach(function (sel) { if (sel.exists()) { const w sel.findOne(500); if (w) { const b w.bounds(); humanClick(b.centerX(), b.centerY()); gaussSleep(800, 200); } } }); }6.2 坐标点击跑偏的排查思路坐标类问题排查有个固定套路先确认是不是坐标系问题再确认是不是适配问题最后才怀疑坐标本身。第一步打印device.width和device.height跟设计稿对一下确认基准没搞错。第二步检查有没有混用setScreenMetrics和控件坐标这是最隐蔽的一类 bug。第三步把要点击的那个位置截图下来人眼看清楚目标到底在不在那个坐标上。很多时候问题根本不是坐标算错了而是页面当时的状态和量坐标的时候不一样——比如多了个顶部横幅整个布局下移了 80 像素。对状态敏感的页面我的处理方式是用控件坐标代替硬编码坐标。哪怕这个控件没有文字没有 ID只要能通过className()和boundsInside()找到它拿到的中心点就是跟着布局走的页面下移它也跟着下移。6.3 权限、保活和长期运行的稳定性脚本能跑起来和能跑一周是两件事。长期运行会遇到的坑主要是这几个。无障碍服务被系统回收。部分定制系统在内存紧张时会关掉无障碍服务脚本就静默失效了。应对办法是脚本里做周期性自检或者用auto()检查状态// 在循环里定期检查 if (!auto()) { log(无障碍服务掉了重新等待); auto.waitFor(); }后台被杀。需要在系统设置里给 Auto.Js 加白名单关闭电池优化。同时脚本本身别开太多定时器长时间空闲时用较长的sleep而不是短sleep空转后者会明显增加耗电和被杀的概率。日志无限制增长。console.show()一直开着日志堆多了会拖慢界面甚至触发内存问题。我的做法是只在调试阶段开日志正式跑的时候关掉或者用console.hide()只在关键节点用toast()做轻量提示。误触导致的流程错位。这是最烦的一类问题某个步骤点空了后面的步骤就在错误的页面上继续操作最后报一堆莫名其妙的错。解法是给每个关键步骤加状态断言——操作完之后确认一下当前页面是不是预期的那一个不是就回退或者重来function assertPage(marker, timeout) { if (!text(marker).waitFor(timeout || 5000)) { log(页面状态异常期望标记 marker); return false; } return true; }这几行代码看着简单但它能把静默跑偏变成明确报错排查成本差着数量级。最后说个我自己踩过的坑。早期写脚本的时候我特别迷信抖动越大越像人把所有点击的抖动幅度都设到 ±20。结果在一个带图标的列表页上脚本稳定点偏因为列表项右侧那个小箭头的可点击区域只有 30 多像素宽抖一下就到隔壁元素上去了。后来改成按元素尺寸动态计算抖动上限问题就没了。拟人化的前提是不影响功能所有随机参数都应该有一个基于元素实际尺寸的安全边界这条经验比任何技巧都值钱。