记一次APP打包后的adb校验APK思路

341 阅读5分钟

前言

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 校验的方案在知识点盲区之外的时候,这也算一种解决思路吧。问题还是有的,比如没有处理多设备连接的情况,比如需要连接一个设备,比如比较耗时等等。总之慢慢学习吧,这个方案总比 手动安装更强是吧,在没有其他方案的情况下[手动狗头];