1.前言
初次看这部分内容,感觉功能好强大,尤其是应用启动部分,感觉好方便。我只能说抱歉,这部分内容很多获取的autojs自己的各种信息,而不是当前应用的信息,还有一些函数平时不会使用或者特殊情况使用不方便。我说过,我自己写过一个完整的游戏项目,这游戏是直板机都能玩的游戏,可以在一个手机上挂5个游戏应用,也就是所说的5开。由于我写脚本的游戏应用有两个活跃包名(简单说,应用活跃状态对应两个页面),使用这部分中的启动方式会启动到第一个游戏页面,而我想切换到第二个游戏页面。还一个原因是,官方没有给出切换应用的函数,只有启动应用的函数,通过启动应用的函数去切换应用,速度会很慢还一个就是可能存在内存溢出的风险。为了快速切换应用,我通过root命令的方式进行切换,这个命令代码的优化是非常痛苦的。我开始通过命令行切换应用,速度也很快,但是运行一段时间后,出现了内存溢出导致手机卡死的情况。
我反复排查,一直以为图片处理导致的内存溢出。幸好,因为兼容多个环境的原因,我在脚本开发过程中从来不使用图片识别,不会加载本地的图片,一切图片都是加载内存里面的,当然不是因为图片处理问题而导致的内存溢出。
我通过打印各种信息发现,我使用的root命令切换应用每次会生成一条记录。如果app没有启动,生成一条记录来启动应用是正常的,但是app运行后台的情况下,它也会通过启动的方式来切换应用,这样会导致内存占用越来越多,最后直接卡死手机。如果不是在需要频繁切换5个应用的极端条件下,你一定不会相信,切换应用竟然会导致内存溢出。
对于出现这种问题,其实很好解决。现在一个手机上运行一个游戏的,我们写脚本的时候,只需要启动一次游戏应用就可以了,或者进入游戏后再启动脚本。对于和我一样的特殊情况,一个手机上有多个相同的游戏,需要频繁的切换,我也给出不会导致内存溢出的方式。如果在一个手机上频繁进行不同应用切换,并且想快速切换,也可以使用我给出的这种方式,当然需要在root环境下完成操作。
2.属性与函数
1.概况
app相关的所有属性或者函数都是基于app对象调用的,app对象已经内置在全局变量中,可以直接进行使用,不需要自己创建或者销毁。获取属性时必须通过app对象获取,调用函数可以通过app对象调用,也可直接调用函数,因为全局变量中包含app对象包含的所有函数。
2.versionCode、versionName、autojs.versionCode与autojs.versionName
按照正常,versionCode和versionName对应当前的应用版本号和版本名称,autojs.versionCode和autojs.versionName对应autojs的版本号和版本名称。但是,我们脚本肯定得一直运行在autojs中,导致四个函数都是获取autojs的版本号或版本名称。我们脚本开发过程中,获取当前应用的信息可能有用,获取autojs的信息根本没有任何作用,因此,我说这部分内容没有作用。
arduino
console.log("版本号:" + app.versionCode);
console.log("版本名称:" + app.versionName);
console.log("autojs版本号:" + app.autojs.versionCode);
console.log("autojs版本名称:" + app.autojs.versionName);

3.launchApp、launch与launchPackage
launchApp、launch和launchPackage均是启动应用,均接收字符串类型的参数。launchApp通过应用名称启动应用;launch和launchPackage均通过应用包名启动应用,两者作用一样。需要注意,这三种方式都是启动应用,导致生成一条新记录。
scss
launchApp("按键精灵");
// 相当于
app.launchApp("按键精灵");
app.launch("com.cyjh.mobileanjian");
// 相当于
launch("com.cyjh.mobileanjian");
// 相当于
launchPackage("com.cyjh.mobileanjian");
// 相当于
app.launchPackage("com.cyjh.mobileanjian");
我觉得应用启动是最重要的内容,我会详细讲下。如果你使用官方给出的这种方式,启动应用是很慢的,尤其是在第一次运行这部分代码时,运行速度很慢。有没有快速通用的应用启动方式呢?我给出一个使用无障碍方式快速万能启动应用的方式,而且这种方式是模拟人为点击,一定不会出现内存溢出。先返回主界面,然后通过应用名称进行点击,哪怕应用不在主界面的第一页,也会自动点击的,但是放在"应用分类夹"中的应用无法启动。
scss
// 返回主界面
Home();
// 加两秒延迟
sleep(2000);
// 启动按键精灵,可根据实际情况替换应用名
text("按键精灵").click();
上述方式,在以下情况下无法启动。

可能有小伙伴说,我习惯使用包名启动应用的方式,但是我不知道应用包名怎么办?我已经给大家封装了获取应用包名的函数。这个函数除了获取包名外,还可以获取活跃包名,活跃包名会用在root环境下快速切换应用。
scss
getHomePackageAndActivityPackage("按键精灵");
// 获取包名和活跃包名
function getHomePackageAndActivityPackage(appName) {
Home();
sleep(2000);
let currentSelector = className("android.widget.TextView").text(appName).findOne(10000);
if (!currentSelector) {
toastLog("获取包名失败");
return;
}
currentSelector.click();
sleep(2000);
let currentHomePackage = currentPackage();
let currentActivityPackage = currentActivity();
toastLog("包名:" + currentHomePackage);
toastLog("活跃包名:" + currentActivityPackage);
Home();
}

注意:
有些小伙伴使用这个函数的时候可能会出现打印不是当前包名的情况,这个不是代码有问题。有可能是你第一次执行相关代码,autojs第一次启动时候会有个预加载过程,导致执行速度过慢,然后延迟加的不够,会答应主机主界面的包信息。autojs有些代码第一次执行时候就是慢,在我们分块学习过程中很容易出现这个问题,大家知道就好了,在项目中是不会出现的,哪怕真的出现,有很多方式可以预加载。我这是给大家提醒下,出现问题有考虑方向,后面尽量不会再进行这部分提示了。
在root环境下,如果需要快速切换应用的话,可以通过以下方式完成。这个过程非常复杂,首先就是启动和切换要采用不同的方式,需要通过数据来判断使用哪种情况打开应用;其次,切换应用需要根据不同的Android版本有对应的数字;最后,每次启动后需要获取应用的任务id,用于后面快速切换应用。为什么这么麻烦?因为这种情况下,哪怕是一个应用两个活跃页面也能保证切换到最终页面,能够快速切换并且不会导致内存溢出。小伙伴们看到这里应该都头疼了,因此如果不是特别需要,我不推荐这种方式。不过,因为在学习过程中,这种方式我也会信息讲下过程,如果觉得麻烦可以忽略。
第一步,获取任务id。最后需要的就是筛选后的id,只有这个值会在后面进行使用。筛选后的数组,一般先判断数组是否为空。如果为空,代表应用没有启动,方便后面调用启动相关的代码;如果不为空,将数据根据需求全局保存下,每次启动只需要全局保存一次即可,只要能自己能够分清每个应用对应的id即可,具体保存在数组、对象还是什么里面,可以根据自己的喜好进行保存。
ini
// 当前应用包名
let currentPackageName = "com.cyjh.mobileanjian";
// 获取所有的任务id
let taskIdArray = getRecentTaskPackageTaskIds();
// 个别时候有两个任务id(非常特殊的情况),按照最后打开的时间降序排列
taskIdArray.sort(function (a, b) {
return b.lastActiveTime - a.lastActiveTime;
});
// 获取需要的包名id数组,我这里以按键精灵的包名为例,请根据实际情况筛选
let filterTaskIdArray = taskIdArray.filter(function (item) {
return item.packageName.indexOf(currentPackageName) > -1;
});
console.log(filterTaskIdArray);
// 获取最近任务包名和任务id
function getRecentTaskPackageTaskIds() {
let cmd = "dumpsys activity activities";
let result = shell(cmd, true);
let taskArray = result.result.split("Task id #");
let taskIdArray = [];
taskArray.forEach(currentTask => {
let currentTaskArray = currentTask.split(" ");
let packageString = currentTaskArray.find(function (item) {
return item.indexOf("A=") != -1;
});
let timestampString = currentTaskArray.find(function (item) {
return item.indexOf("lastActiveTime=") != -1;
});
if (packageString) {
let currentObj = {
id: currentTaskArray[0].replace(/\n/g, ""),
packageName: packageString.substring(packageString.indexOf("=") + 1),
lastActiveTime: Number(timestampString.substring(15))
}
taskIdArray.push(currentObj);
}
});
return taskIdArray;
}

第二步,如果筛选后的数组为空,可以调用下面的启动应用代码。包名和活跃包名上面能够获取,这个对于一个应用来说一般是不会改变的,获取一次后,全局赋值即可。对于shell命令对象,我推荐个全局创建一个,以后可以直接通过这个对象调用命令。但是,这个命令有个缺陷就是权限不如直接通过shell调用的权限高,脚本开发过程中,我们可以优先使用这个全局对象执行shell命令,如果权限不足导致无法执行,可以再考虑直接通过shell命令调用。root命令更高,有概率出现问题,请严格按照过程执行。需要切换应用的调用切换应用代码,需要启动应用的调用启动应用代码。
csharp
// 当前应用包名
let currentPackageName = "com.cyjh.mobileanjian";
// 当前活跃包名
let currentActivePackageName = "com.cyjh.mobileanjian.vip.activity.MainActivity";
// 创建shell命令,全局可以只创建一次
let currentShell = new Shell(true);
currentShell.exec("am start -n " + currentPackageName + "/" + currentActivePackageName + " -f 0x20000000 --ez REORDER true");
第三步,如果筛选后的数组不为空,可以调用下面的切换应用代码。这部分重要的是不同版本的切换值,比如Andorid 7的环境下的切换值为24,这也是用的比较多的脚本开发环境。其他环境下的值需要自己找,但是我尝试了下Andoid 9环境的雷电模拟器,也没找到这个值,也可能在Android 9中不支持这种切换方式了。
ini
// 创建shell命令,全局可以只创建一次
let currentShell = new Shell(true);
let taskId = "5";
let cmd = "service call activity 24 i32 " + taskId + " i32 0";
currentShell.exec(cmd);
当然,为了方便大家找这个值,我给大家封装了个函数,快速找这个值。但是,我推荐大家在Android 7的环境下使用此种方式,我感觉别的环境下大概率不支持。下面这个函数的参数第一个为第一步获取的任务id,第二参数为开始查找的值,第三个参数为结束查找的值。我一般推荐值为40以内,这是执行shell命令,对系统影响很大,40以上的值容易导致崩溃。还有就是需要将代码中的TARGET_PKG填写获取的任务id对应包名,我推荐是函数第二和第三个参数不用填写,使用默认值即可。只需要将第一步中的包名和包名对应的任务id,修改TARGET_PKG值或者通过参数传递,就可以完成下面功能。这个函数主要是让大家参考下,我如何找到的这个值。当然,如果有小伙伴找到了其他环境下的这个值,可以评论区留言提醒。
javascript
// 查找操作id
function getSwitchAppCallCode(taskId, startTaskId, endTaskId) {
if (!startTaskId) {
startTaskId = 1;
}
if (!endTaskId) {
endTaskId = 40;
}
// 要切换到的目标包名和 TaskId
let TARGET_PKG = "com.cyjh.mobileanjian";
let TASK_ID = taskId;
let SAFE_CODES = [24, 25, 26, 32, 33, 35];
// 成功后会保存在这里
let foundCode = null;
for (let code of SAFE_CODES) {
let cmd = "service call activity " + code + " i32 " + TASK_ID + " i32 0";
log("尝试 code=" + code + ": " + cmd);
let result = shell(cmd, true);
console.log(result);
sleep(500); // 增加等待时间确保切换完成
if (currentPackage() === TARGET_PKG) {
foundCode = code;
break;
}
}
if (!foundCode) {
console.log("关键值未找到");
// 尝试 code
// 43有问题,不要尝试
for (let code = startTaskId; code <= endTaskId; code++) {
// 拼接命令字符串
let cmd = "service call activity " + code + " i32 " + TASK_ID + " i32 0";
log("尝试 code=" + code + ": " + cmd);
// 执行命令(true 表示以 root 权限执行)
let result = shell(cmd, true);
console.log(result);
// 等500ms 让切换发生
sleep(500);
// 检查当前前台包名
let cur = currentPackage();
log("当前包: " + cur);
if (cur === TARGET_PKG) {
toast("找到有效 code = " + code);
foundCode = code;
break;
}
}
}
// 如果找到了,就用该 code 再切一次,并退出
if (foundCode !== null) {
var finalCmd = "service call activity " + foundCode + " i32 " + TASK_ID + " i32 0";
log("最终切换使用 code=" + foundCode + ": " + finalCmd);
shell(finalCmd, true);
} else {
toast("未在范围内找到有效 code,请调整范围");
}
}
4.getAppName
通过应用包名获取应用名,需要传递应用包名一个参数,参数类型为字符串。一般情况下,都是想通过应用名来获取应用包名,通过应用包名大概率可以猜出所属的应用。在无法确定的情况下,这个函数更多的是用于应用包名和应用名之前的验证。
arduino
console.log(getAppName("com.cyjh.mobileanjian"));

5.openAppSetting
打开app的设置页面,需要传应用包名一个参数,参数类型为字符串。返回是否操作成功,返回类型为boolean,一般会返回true,如果打开失败或者未找到应用返回false。
arduino
console.log(openAppSetting("com.cyjh.mobileanjian"));


6.viewFile与editFile
viewFile和editFile函数分别用于在其他应用中查看或者编辑某个文件,均需要传递文件路径一个参数,参数类型均为字符串。如果没有找到打开能够打开文件的应用,抛出ActivityNotException异常。如果这个类型的文件已经在系统中设置了默认打开方式会直接打开,如果没有设置默认打开方式会弹出应用选择框。下面就以viewFile函数为例介绍下这个功能,editFile函数类似。最好通过捕获异常的方式来调用这个函数,真正有异常可以进行特殊处理,而不是导致脚本停止。对于Android环境下的文件路径,我在文件部分会进行介绍,现在知道是一个完整的文件路径就好了。我想打开个特殊文件类型的文件或者不存在的文件来触发报错报错,但是这两种方式都无法触发,都会弹出类似的应用选择框。这就说明,要出发这个报错是非常困难的,文件管理器一般都是允许打开任何文件的,没出现的文件类型顶多触发个文件选择。
vbnet
try {
app.viewFile("/sdcard/com.py.test/test.txt");
} catch (error) {
console.log(error);
}

注意:
viewFile和editFile函数只能通过app对象调用,没有在全局变量里面。我没有特殊提醒的函数,代表app对象调用和直接调用函数两种方式均可以,但是这两个函数只支持app对象调用方式。按照正常理解,同属于app部分的函数,调用方式应该一致,但是就出现了不同情况。autojs会出现很多这种类似的问题,如果后面有特殊情况,我会提醒的。
7.uninstall
卸载应用,需要传应用包名一个参数,参数类型为字符串。只是弹出卸载应用的确认框,最后卸载还需要自己确认。只能通过app对象调用,无法直接调用。但是这个函数在Android 7的环境下可以生效,但是在安卓9的环境下不生效。
8.openUrl
通过浏览器打开链接,需要传链接一个参数,参数类型为字符串。如果不以"http://"或者"https://"开头,会自动拼接"http://"。如果没有浏览器,会抛出ActivityNotException异常。只能通过app对象调用,无法直接调用。
arduino
app.openUrl("www.baidu.com");
// 相当于
// app.openUrl("http://www.baidu.com");
9.sendEmail至getInstalledApps
后面这几个函数我很少使用,我就不做过多介绍,这些函数大概率都是通过app对象调用,无法直接调用。小伙伴如果感兴趣可以研究下这部分内容,尤其是intent和startActivity函数配合使用。这两个函数配合使用功能很强大,前面一些函数完全可以通过这两个函数配合使用来完成。这两个函数也能完成应用启动功能,需要谨慎这两个函数配合使用导致的内存溢出风险。
3.总结
特别注意,只有通过个人主页博客或者个人介绍中方式,才能获取源码