1. 项目缘起:为什么需要AutoJs自动化截图?
在移动端自动化测试、数据采集、日常任务批处理或者无障碍辅助功能开发中,我们经常会遇到一个看似简单却颇为繁琐的需求:定时、定点或按条件对手机屏幕进行截图,并自动保存到相册。你可能会想到手动操作,或者依赖一些录屏软件,但这些方法要么效率低下,要么无法精准控制。比如,你需要监控某个App特定页面的数据变化,每小时截取一次;或者,在运行一个自动化脚本时,需要在关键步骤留下证据;又或者,你只是想做一个自动化的“每日打卡”截图工具。
这时,AutoJs就成为了一个绝佳的选择。它是一个基于JavaScript的Android平台自动化工具,无需Root权限,通过模拟点击、滑动、找图、找色等操作,可以轻松实现复杂的自动化流程。而“截图并保存”正是其最基础也最核心的功能之一。网络上虽然有很多零散的代码片段,但往往缺乏系统性的原理讲解、完整的异常处理以及贴合实际场景的优化建议。今天,我就结合自己多次在数据监控和自动化测试项目中的实战经验,从头到尾拆解如何用AutoJs实现一个稳定、可靠、功能完备的自动化截图工具,并深入探讨那些官方文档不会告诉你的“坑”和技巧。
2. AutoJs环境搭建与核心权限剖析
在开始写代码之前,一个正确且充分授权的环境是成功的一半。很多新手卡在第一步,不是因为代码逻辑问题,而是权限没给对或者环境没配好。
2.1 AutoJs Pro与开源版的选择
目前AutoJs主要有两个分支:AutoJs Pro(闭源收费版)和AutoJs 4.x(开源免费版)。对于学习和个人使用,开源版完全足够。但需要注意的是,由于一些原因,原开源版本在各大应用商店已不易直接下载,通常需要从GitHub等开源平台获取APK文件进行安装。安装后,请务必从软件内部检查更新,确保使用的是较新的稳定版本。
注意:请务必从官方或可信的渠道获取AutoJs安装包,避免使用来历不明的修改版,以防安全风险。
2.2 必须开启的三大系统权限
AutoJs的强大能力建立在系统权限之上,截图功能尤其依赖以下三项,缺一不可:
无障碍服务:这是AutoJs的“灵魂”。它允许脚本模拟用户操作,监听屏幕内容。开启路径:
系统设置 -> 无障碍 -> 已下载的服务 -> 找到AutoJs并开启。开启后,AutoJs的悬浮窗或通知栏通常会有一个“小机器人”图标。悬浮窗权限:允许AutoJs在屏幕上层显示控制按钮、日志信息或调试窗口。这对于脚本调试和实时监控非常有用。开启路径:
系统设置 -> 应用管理 -> AutoJs -> 权限管理 -> 开启“悬浮窗”。存储权限:这是截图保存到相册的关键。AutoJs需要写入权限才能将图片文件保存到设备的公共存储区域(如DCIM、Pictures目录)。开启路径:
系统设置 -> 应用管理 -> AutoJs -> 权限管理 -> 开启“存储”或“文件与媒体”权限。
一个常见的深坑:即使在Android 6.0+的动态权限管理中授予了存储权限,在某些深度定制的系统(如MIUI、EMUI)上,可能还需要额外开启“允许后台弹出界面”或关闭“电池优化”,以防止脚本在后台运行时被系统“杀掉”,导致定时截图任务中断。
2.3 开发环境与调试技巧
建议在电脑上使用VSCode等编辑器编写代码,通过Wi-Fi或USB与手机上的AutoJs连接,实现实时推送脚本和查看日志。AutoJs自带了一个非常好用的“可视化”开发工具,可以录制操作、查看控件布局,对于编写找图找色、控件操作的脚本至关重要。
初次编写截图脚本时,不要急于写完整逻辑。可以先写一个最简单的测试脚本,验证核心函数captureScreen()是否能成功执行,并打印出返回的图片路径。这能帮你快速定位是权限问题、路径问题还是函数调用问题。
3. 核心代码实现:从截图到保存的完整链路
现在,我们进入核心环节。一个健壮的自动化截图脚本,绝不仅仅是一行captureScreen()那么简单。它需要包含截图捕获、图片处理、路径生成、文件保存、相册更新等多个环节,并且每一环都要考虑异常情况。
3.1 基础截图与直接保存
AutoJs提供了最基础的截图函数captureScreen()。它返回一个Image对象,代表当前屏幕的快照。最简单的保存方式如下:
// 示例1:基础截图并保存到指定路径 auto.waitFor(); // 等待无障碍服务稳定 let img = captureScreen(); // 捕获屏幕,返回Image对象 let savePath = "/sdcard/Pictures/Screenshots/auto_capture_" + new Date().getTime() + ".png"; img.saveTo(savePath); // 将Image对象保存为文件 toast("截图已保存: " + savePath);这段代码已经可以实现功能,但它存在几个明显问题:
- 路径固定:所有图片都堆在一个文件夹,难以管理。
- 文件名简单:仅用时间戳,缺乏业务含义。
- 未刷新相册:文件虽然保存在存储区,但系统的“相册”或“图库”App可能不会立即扫描到新文件,导致无法在相册中即时看到。
- 没有错误处理:如果存储空间不足或权限异常,脚本会直接崩溃。
3.2 构建可管理的文件存储体系
良好的文件管理是自动化脚本专业性的体现。我推荐按“日期”创建文件夹,并按“时间+场景”命名文件。
// 示例2:结构化保存方案 function captureAndSave(description) { auto.waitFor(); try { // 1. 捕获屏幕 let screenImg = captureScreen(); if (!screenImg) { throw new Error("截图捕获失败,返回了空对象"); } // 2. 构建存储路径 let date = new Date(); let year = date.getFullYear(); let month = (date.getMonth() + 1).toString().padStart(2, '0'); let day = date.getDate().toString().padStart(2, '0'); let baseDir = `/sdcard/DCIM/AutoJsCaptures/${year}-${month}-${day}/`; // 使用DCIM目录,相册识别优先级高 // 确保目录存在 files.ensureDir(baseDir); // 3. 生成有意义的文件名 let hour = date.getHours().toString().padStart(2, '0'); let minute = date.getMinutes().toString().padStart(2, '0'); let second = date.getSeconds().toString().padStart(2, '0'); let timestamp = `${hour}${minute}${second}`; let descPart = description ? `_${description}` : ''; let fileName = `capture_${timestamp}${descPart}.png`; let fullPath = baseDir + fileName; // 4. 保存图片 let saveResult = screenImg.saveTo(fullPath); if (!saveResult) { throw new Error(`图片保存到路径失败: ${fullPath}`); } log(`截图成功保存: ${fullPath}`); // 5. 通知媒体库刷新(关键步骤!) refreshMediaStore(fullPath); return fullPath; } catch (error) { log(`截图保存过程发生错误: ${error.message}`); console.error(error.stack); toast(`截图失败: ${error.message}`); return null; } } // 调用示例 let path = captureAndSave("主页监控"); if (path) { toast(`截图成功,路径:${path}`); }这个函数做了几件重要的事:使用try...catch包裹核心逻辑,防止脚本因单次失败而完全停止;将图片按日期归档;在文件名中加入可选的描述信息;最重要的是,保存后调用了一个refreshMediaStore函数来通知系统相册更新。
3.3 关键突破:让截图立刻出现在系统相册
这是很多教程忽略的痛点。在Android系统中,单纯向存储设备写入文件,媒体扫描器可能不会立即将其加入数据库。我们需要主动“通知”系统。media.scanFile()是AutoJs提供的API,但其行为在不同Android版本上可能不一致。一个更稳健的方法是使用Android的MediaScannerConnection广播意图,但AutoJs环境执行这个稍微复杂。
这里分享一个经过多机型测试的兼容性方法:
// 示例3:刷新媒体库,使截图立即出现在相册中 function refreshMediaStore(filePath) { if (!filePath || !files.exists(filePath)) { log(`文件不存在,无法刷新媒体库: ${filePath}`); return false; } try { // 方法1:使用AutoJs内置的media.scanFile (API 1.0.2+) if (typeof media !== 'undefined' && media.scanFile) { let result = media.scanFile(filePath); log(`使用media.scanFile刷新结果: ${result}`); if (result) { return true; } // 如果失败,继续尝试方法2 } // 方法2:执行shell命令发送广播(更底层,兼容性更好) // 注意:此命令需要shell权限,但AutoJs通常具备 let cmd = `am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://${filePath}`; let shellResult = shell(cmd, true); if (shellResult.code === 0) { log(`通过Shell广播刷新媒体库成功`); sleep(500); // 给系统一点处理时间 return true; } else { log(`Shell命令执行失败: ${shellResult.code}, ${shellResult.error}`); } // 方法3:备用方案 - 如果上述都失败,尝试移动到更“显眼”的目录 log("尝试备用方案:将文件移动到DCIM/Camera目录"); let cameraDir = "/sdcard/DCIM/Camera/"; files.ensureDir(cameraDir); let newPath = cameraDir + files.getName(filePath); if (files.copy(filePath, newPath)) { log(`文件已复制到Camera目录: ${newPath}`); // 对新路径再尝试一次刷新 refreshMediaStore(newPath); return true; } return false; } catch (e) { log(`刷新媒体库时发生异常: ${e}`); return false; } }这个函数采用了降级策略:优先使用AutoJs的高级API,失败后尝试更底层的Shell命令,最后还有一个移动文件的备用方案。实测中,shell命令的方式在绝大多数机型上最为可靠。
4. 进阶功能与场景化实战
掌握了基础保存后,我们可以根据复杂需求对脚本进行增强。自动化截图很少是孤立的,它通常是一个更大流程的一部分。
4.1 定时循环截图与条件触发
假设你需要每30分钟对某个特定App界面截图一次,持续一整天。
// 示例4:定时循环截图 function startScheduledCapture(intervalMinutes, totalDurationMinutes, targetApp) { let intervalMs = intervalMinutes * 60 * 1000; let totalCycles = Math.floor(totalDurationMinutes / intervalMinutes); let cycleCount = 0; log(`开始定时截图任务,间隔${intervalMinutes}分钟,共${totalCycles}次`); while (cycleCount < totalCycles) { // 1. 确保目标App在前台 if (targetApp) { launchApp(targetApp); sleep(3000); // 等待App完全启动 } // 2. 执行截图 let desc = `Cycle_${cycleCount + 1}_${targetApp || 'Screen'}`; let savedPath = captureAndSave(desc); if (savedPath) { log(`第${cycleCount + 1}次截图完成: ${savedPath}`); } else { log(`第${cycleCount + 1}次截图失败`); } // 3. 等待下一个周期 cycleCount++; if (cycleCount < totalCycles) { log(`等待${intervalMinutes}分钟进行下一次截图...`); sleep(intervalMs); } } log("定时截图任务全部完成"); } // 调用示例:每30分钟对微信截图一次,持续8小时 // startScheduledCapture(30, 8 * 60, "微信");重要提醒:长时间运行脚本务必注意功耗和系统休眠。AutoJs自带“保活”机制,但为了极致稳定,可以在手机设置中为AutoJs开启“自启动”、“后台常驻”并关闭“电池优化”。此外,在循环中增加一些随机延迟sleep(random(500, 2000)),可以避免被某些App的反自动化机制检测到过于规律的操作。
4.2 基于控件状态或图像识别的智能截图
单纯的定时截图可能产生大量无效图片。更智能的做法是在特定条件满足时才截图。例如,当屏幕上出现“支付成功”弹窗时,或者当某个股票App的价格数字超过阈值时。
// 示例5:条件触发截图 - 以检测特定文本为例 function captureOnCondition(conditionChecker, checkIntervalMs = 2000, timeoutMs = 60000) { let startTime = Date.now(); log(`开始条件监控,超时时间:${timeoutMs/1000}秒`); while (Date.now() - startTime < timeoutMs) { // 执行条件检查函数 let conditionMet = conditionChecker(); if (conditionMet) { log("条件满足,触发截图!"); let path = captureAndSave("ConditionTriggered"); toast("条件已触发并截图"); return path; // 返回截图路径 } // 条件未满足,等待一段时间后继续检查 sleep(checkIntervalMs); } log("条件监控超时,未触发截图"); return null; } // 定义一个检查函数:检测屏幕上是否包含“任务完成”文本 function checkForCompletionText() { let found = text("任务完成").findOne(100); // 查找文本,最多等待100毫秒 return found != null; } // 启动监控 // captureOnCondition(checkForCompletionText);这里conditionChecker是一个由你定义的函数,它可以利用AutoJs强大的选择器(text(),id(),className())或图像识别(findImage())能力来判断当前屏幕状态。这种模式将截图从“无脑记录”升级为“有意义的快照”。
4.3 截图后的自动化处理:压缩与上传
保存到相册只是第一步。在实际项目中,截图往往需要后续处理。例如,将截图压缩后通过HTTP API上传到服务器,或者调用百度OCR API识别其中的文字。
// 示例6:截图后压缩并准备上传 function captureCompressAndPrepareUpload(quality = 0.7) { // 1. 截图 let originalPath = captureAndSave("Original"); if (!originalPath) return null; // 2. 压缩图片 let compressedPath = originalPath.replace('.png', '_compressed.jpg'); let img = images.read(originalPath); if (img) { // 将PNG转换为JPG并进行质量压缩 images.save(img, compressedPath, "jpg", quality * 100); img.recycle(); // 重要!释放图像内存 log(`图片压缩完成: ${compressedPath}, 质量: ${quality}`); // 3. 这里可以添加上传逻辑 // uploadToServer(compressedPath); // 可选:删除原始大图以节省空间 // files.remove(originalPath); return compressedPath; } return originalPath; }images.save()方法允许你指定格式和质量。对于网络传输,将PNG转为JPG并设置quality=0.7左右,通常能在清晰度和文件大小间取得良好平衡。务必注意,images.read()读取的图片对象在使用后要调用.recycle()释放内存,否则在批量处理时可能导致内存不足而脚本崩溃。
5. 避坑指南与性能优化
在实际部署中,我踩过不少坑,也总结了一些让脚本更健壮、更高效的经验。
5.1 内存泄漏与图片对象回收
这是AutoJs脚本,尤其是涉及图像处理的脚本,最常见的崩溃原因。captureScreen(),images.read(),images.clip()等函数都会创建图像对象,它们占用的是原生内存,不会像普通JavaScript对象那样被自动垃圾回收。
错误示范:
for (let i = 0; i < 100; i++) { let img = captureScreen(); // 每次循环都创建新图像对象 // ... 处理img // 忘记 img.recycle()! } // 循环几十次后,很可能内存耗尽,脚本被系统终止。正确做法:
for (let i = 0; i < 100; i++) { let img = null; try { img = captureScreen(); // ... 处理img } finally { // 确保无论是否出错,都尝试回收 if (img != null) { img.recycle(); } } sleep(100); // 给系统一点喘息时间 }养成“创建即规划回收”的习惯。对于简单的截图保存,captureScreen()返回的Image对象在调用saveTo()后,其数据已被写入文件,可以立即回收。
5.2 路径权限与存储空间监控
即使授予了存储权限,在Android 10及以上版本中,由于作用域存储的限制,直接写入/sdcard/根目录或某些子目录可能会失败。优先使用AutoJs提供的标准路径或系统媒体目录,如/sdcard/DCIM/、/sdcard/Pictures/,这些目录的访问限制通常更少。
在脚本开始或每次截图前,可以增加一个存储空间检查:
function checkStorageAvailable(minSpaceMB = 50) { let sdcardPath = files.getSdcardPath(); let availableBytes = files.getFreeSpace(sdcardPath); let availableMB = availableBytes / (1024 * 1024); if (availableMB < minSpaceMB) { log(`存储空间不足!可用空间仅${availableMB.toFixed(2)}MB,要求${minSpaceMB}MB`); toast("存储空间不足,请清理后重试"); return false; } return true; }5.3 脚本稳定性与容错设计
自动化脚本需要长时间无人值守运行,稳定性至关重要。
- 异常捕获全局化:使用
try...catch包裹主要功能块,并记录详细的错误日志到文件,方便事后排查。 - 心跳与自恢复:对于长周期任务,可以设计一个“看门狗”机制。主脚本定期向一个文件写入时间戳,另一个监控脚本检查这个时间戳,如果超过预期时间未更新,则判断主脚本可能已崩溃,并尝试重新启动它。
- 避免过度阻塞:在循环中,
sleep的时间不宜过短,避免高频循环耗尽CPU。同时,在等待控件出现时使用带超时的findOne(timeout),而不是无限循环的findOne(),防止脚本卡死。 - 日志分级输出:使用
log(),console.verbose(),console.error()等不同级别的日志函数。在调试阶段开启详细日志,在生产环境可以关闭部分日志以减少性能开销和日志文件体积。
5.4 对抗应用检测与无障碍服务断开
一些应用(如游戏、金融类App)会检测无障碍服务,甚至主动断开。这可能导致你的脚本突然失效。
- 策略一:伪装:在AutoJs Pro中,可以使用“伪装无障碍类名”的功能。在开源版中,可以尝试修改AutoJs的包名(需要一定的技术能力)。
- 策略二:监控与重连:在脚本中定期检查无障碍服务是否开启。
function ensureAccessibility() { if (!auto.service) { toast("无障碍服务已断开,正在尝试打开设置..."); // 尝试跳转到无障碍设置页面 app.startActivity({ action: "android.settings.ACCESSIBILITY_SETTINGS" }); // 这里无法自动开启,需要人工干预。脚本可以在此等待或退出。 sleep(10000); // 再次检查 if (!auto.service) { log("无法恢复无障碍服务,脚本停止"); exit(); } } } // 在主循环中定期调用 ensureAccessibility();6. 完整项目示例:一个监控类App数据变化的自动截图工具
让我们将所有知识点整合,构建一个模拟真实场景的项目:监控某个新闻App的头条新闻标题变化,每当检测到标题更新时,自动截图并保存,同时将新闻标题文本记录到日志文件中。
/* * 项目:新闻头条变化监控与自动截图工具 * 功能:定时打开某新闻App,检查头条标题,如果发现变化则截图并记录。 */ // ====== 配置区域 ====== const TARGET_APP = "网易新闻"; // 目标App名称 const CHECK_INTERVAL = 5 * 60 * 1000; // 检查间隔:5分钟 const HEADLINE_ID = "com.netease.newsreader.activity:id/bdx"; // 头条新闻标题的控件ID(需通过AutoJs控件查看器获取) const STORAGE_DIR = "/sdcard/Documents/NewsMonitor/"; // ===================== let lastHeadlineText = ""; // 上一次记录的头条标题 function main() { // 初始化 auto.waitFor(); files.ensureDir(STORAGE_DIR); logFile = STORAGE_DIR + "monitor_log.txt"; log("新闻头条监控脚本启动"); writeLog("=== 监控开始于 " + new Date().toLocaleString() + " ==="); while (true) { try { monitorCycle(); } catch (e) { console.error("监控周期发生未捕获错误:", e); writeLog(`[ERROR] ${new Date().toLocaleString()} - ${e.message}`); sleep(30000); // 出错后等待30秒再继续 } sleep(CHECK_INTERVAL); } } function monitorCycle() { log("开始一个新的监控周期"); // 1. 启动目标App launchApp(TARGET_APP); sleep(5000); // 等待App加载 // 2. 查找头条标题控件 let headlineWidget = id(HEADLINE_ID).findOne(8000); // 等待8秒 if (!headlineWidget) { log("未找到头条标题控件,可能页面加载失败或ID已变更"); writeLog(`[WARN] ${new Date().toLocaleString()} - 控件查找失败`); return; } let currentText = headlineWidget.text(); log(`当前头条标题: ${currentText}`); // 3. 判断标题是否发生变化 if (lastHeadlineText === "") { // 第一次运行,只记录不截图 log("首次运行,记录初始标题"); lastHeadlineText = currentText; writeLog(`[INIT] ${new Date().toLocaleString()} - 初始标题: ${currentText}`); } else if (currentText !== lastHeadlineText) { // 标题发生变化! log(`检测到头条新闻更新!旧标题: "${lastHeadlineText}" -> 新标题: "${currentText}"`); // 4. 执行截图 let screenshotPath = captureAndSave(`NewsUpdate_${formatTime()}`); if (screenshotPath) { log(`变化已截图: ${screenshotPath}`); // 5. 记录到日志文件 let logEntry = `[UPDATE] ${new Date().toLocaleString()} - 标题更新: "${lastHeadlineText}" -> "${currentText}" | 截图: ${files.getName(screenshotPath)}\n`; writeLog(logEntry); } // 更新记录 lastHeadlineText = currentText; } else { log("头条标题未发生变化"); } // 6. 返回桌面,结束本次监控(可选) home(); sleep(2000); } // 工具函数:写入日志文件 function writeLog(content) { files.append(logFile, content + "\n"); } // 工具函数:格式化时间用于文件名 function formatTime() { let d = new Date(); return `${d.getFullYear()}${(d.getMonth()+1).toString().padStart(2,'0')}${d.getDate().toString().padStart(2,'0')}_${d.getHours().toString().padStart(2,'0')}${d.getMinutes().toString().padStart(2,'0')}${d.getSeconds().toString().padStart(2,'0')}`; } // 启动脚本 main();这个示例展示了如何将截图功能嵌入到一个完整的业务逻辑中:状态判断 -> 条件触发 -> 执行截图 -> 记录日志。它包含了错误处理、日志记录、控件查找等实用技巧,是一个可以直接修改使用的模板。
最后,我想强调的是,自动化脚本的开发和维护是一个持续的过程。App的UI会更新,控件ID会变化,系统权限策略也会调整。一个优秀的自动化脚本,除了代码本身健壮,还需要有完善的文档和配置分离机制。将目标App的控件ID、检查间隔等易变参数放在脚本开头的配置区域,而不是硬编码在逻辑中,这样当需要适配新版本时,你只需要修改几行配置,而不是重构整个脚本。这就是从“能运行”的脚本到“好维护”的工具之间的关键一步。