前言
2022年,我们APP已经有两年了,其中被各个大佬添加了很多编译时技术。同时包含20多个渠道包,我们中的一个靓仔打包apk的时候,不是so 文件打掉了,就是class打掉了,但是在打非渠道包的时候却没有出问题?我们并没有选择V1 v2 的漏洞做渠道包的打包方案,而是憨憨的使用了全task。问题就是,这么多的渠道包,不可能去守着看编译日志,那就只有笨办法了,打包后,安装到手机里面,点一下,有问题就重新打,安装不起也重新打。
我上线时间少,然后打包没有遇到这个问题(不是自己的问题,解决起来没有多少动力)。直到上次我上线的时候,我发现我竟然也有概率打掉class,emmmmm? 这个调调 会传染? 好吧,那就只能尝试去处理一下。
正文
因为我当时在处理小米隐私合规的问题,然后小米渠道的64位apk 直接没有了so 文件?我看了下build 执行日志,竟然没有警告? 但是后来打32位的时候,出现了一次 错误日志。说的是一个class 为找到(时间有点久了,就忘了截图)。
那么应该怎么切入去处理这个问题呢?我们打渠道包的时候是打很多个,并不是每一个都有问题,所以通过挂一个task 感觉没有切入点。拦截log 通过控制system.out的输出?然后抓取日志吗?关键就在于,我们build的时候,红色警告太多,同时也不确定能不能拿到输出日志。但是apk校验的方式又在知识点盲区,所以只剩下最笨的办法,之前手动安装,那么现在就只能代码安装了对吧。
JAVA 执行 adb 命令
这个是上面一切想法的前提。JAVA 代码可以直接执行 adb 命令,同时拿到输出结果。这个代码很简单,例如: 我们判断是否有adb 连接到手机
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("adb devices -l");
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader in = new BufferedReader(isr);
String line = in.readLine();
Boolean success=false;
while (line!=null){
// 输出 拿到的结果。
System.out.println(line);
line=in.readLine();
}
编写安装校验的task
我们项目是使用了buildSrc 这种插件模式的。通俗的说,buildSrc是一个自动添加到gradle project 中的插件,和导入一个classPath 一样,可以在build.gradle中直接使用里面的代码。 查看buildSrc配置 所以我们可以直接在buildSrc 中创建class,然后再build.gradle 中创建task 然后去调用他,当然也可以直接创建task,我感觉太麻烦了。
我们先梳理一下我们的业务诉求。
- 我们要通过adb 安装apk
- 首先要判断是否有设备连接
- 然后卸载 当前手机中的apk 版本,有可能手机上的比安装的高,然后就安装不起。
- 安装apk,获取到安装日志,判断是否安装成功。
- 我们要启动apk
- 首先要关闭当前apk,主要是批量操作,可能进程没有死。我们就得判断冷启动还是热启动。
- 启动apk 后获取到启动日志,然后判断是否成功。
判断是否有adb 连接
这一步的逻辑很简单,就是判断有没有设备连接,也没有处理连接了多个设备。有设备连接的时候,尝试卸载掉已经安装的APP。 获取已经连接的设备:
adb devices -l
卸载apk
adb uninstall apkPackage
完整代码
public static Boolean devices() throws IOException{
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("adb devices -l");
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader in = new BufferedReader(isr);
String line = in.readLine();
Boolean success=false;
while (line!=null){
if (line.contains("device")){
success=true;
}
System.out.println(line);
line=in.readLine();
}
if (success){
System.out.println("获取完成,尝试卸载APP上已经安装的apk:"+apkPackage);
runtime.exec("adb uninstall "+apkPackage);
System.out.println("卸载完成:"+apkPackage);
}
return success;
}
安装apk
因为流程上,安装成功后,需要启动apk,所以加一个判断。 安装命令:批量操作,加-r 覆盖安装
adb install -r apkPath
完整代码:
public Boolean check() throws IOException {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("adb install -r "+apkPath);
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader in = new BufferedReader(isr);
String line = in.readLine();
Boolean success=false;
while (line!=null){
if (line.equals("Success")){
//表示验证完成。
success=true;
}
System.out.println(line);
line=in.readLine();
}
System.out.println("验证完成"+success);
if (success){
return startPage();
}
return success;
}
启动apk
结合上面的问题,我们要先关闭,然后再启动:-s 是关闭后启动的意思,-w 是等待启动完成。
runtime.exec("adb shell am start -S -W "+apkPackage+"/"+apkStartActivity)
完整代码:
public boolean startPage() throws IOException {
System.out.println("开始启动APP");
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("adb shell am start -S -W "+apkPackage+"/"+apkStartActivity);
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader in = new BufferedReader(isr);
String line = in.readLine();
Boolean success=false;
// 因为上面语句的执行后,会通过-s 去关闭现有的APP,然后再次启动,所以说,这里只需要判断这个调调是不是冷启动就行。
while (line!=null){
if (line.contains("LaunchState")&&line.contains("COLD")){
success=true;
}
System.out.println(line);
line=in.readLine();
}
System.out.println("启动完成校验完成"+success);
return success;
}
将这些业务通过一个class 串联起来。
public class CheckApk {
String apkPath = "C:\Users\Administrator\Desktop\xiaomi";
Map<String, List<File>> allApks = new HashMap<>();
/**
* 解压后,对于apk 内容进行扫描。
*/
public void check() throws IOException {
// 获取到当前目录下的所有apk
CopyApkTools.getChildApk(allApks, new File(apkPath));
if (!ApkCheckImpl.devices()){
System.out.println("请连接上一个可以被adb 识别的手机");
return;
}
List<String> errorFiles=new ArrayList<>();
for (List<File> files : allApks.values()) {
// 如果先删除目录下的所有文件。
for (File file : files) {
System.out.println("开始尝试安装:"+file.getAbsolutePath()+"时间较长,请稍候");
Boolean check = new ApkCheckImpl(file.getAbsolutePath()).check();
if (!check){
errorFiles.add(file.getAbsolutePath());
System.out.println("文件安装校验失败:"+file.getAbsolutePath());
}
}
}
System.out.println("校验完成"+errorFiles.size()+"文件运行安装失败");
if (errorFiles.size()>0){
System.out.println("开始输出失败的文件:");
for (String file: errorFiles){
System.out.println(" "+file);
}
}
}
}
build.gradle 中 创建 task
很单纯。
task checkApk{
doLast{
new CheckApk().check()
}
}
总结
在apk 校验的方案在知识点盲区之外的时候,这也算一种解决思路吧。问题还是有的,比如没有处理多设备连接的情况,比如需要连接一个设备,比如比较耗时等等。总之慢慢学习吧,这个方案总比 手动安装更强是吧,在没有其他方案的情况下[手动狗头];