开源测试工具 ApkChecker_new Android 包大小检测及趋势统计

linpengcheng · 2020年05月07日 · 最后由 linpengcheng 回复于 2020年07月28日 · 4514 次阅读

GitHub 地址 :https://github.com/pengchenglin/ApkChecker_new

根据@xinxiAndroid 包大小检查测试方案,增加了 android bundle 包的检测,和一个 web 的页面展示。
使用到的工具有:

基于FlaskBootstrap 优化了报告展示的 UI 页面。少废话,先看东西。

如何运行

由于使用到了matrix-apk-canary-0.6.5.jarbundletool-all-0.15.0.jar两个 jar 包
首先要确保 java 环境已配置正确

  1. clone 项目到本地
  2. 安装 python3 及相关依赖的库pip3 install -r requirements.txt
  3. 使用 gunicorn 运行
cd /GitHub/ApkChecker_new
gunicorn -w 4 -b 0.0.0.0:9876 server:app --timeout 300
# 300秒延时等待防止上传apk后,后台处理时长过长的保护性等待,没加可能会导致aab的包处理超时导致报告生成失败

App Bundle

什么是 App Bundle

App Bundle 是一种新的安卓编译打包方式,编译工具可以根据 CPU 架构类型、屏幕分辨率、语言等维度将一个传统的 App 打包成一个 App 集合;用户下载 App 时,应用市场会根据终端三种维度的类型提供仅适配此终端的 App 子集,从而在提供相同业务功能的前提下,节省用户的网络流量与终端设备空间。(文件扩展名为 .aab)
App bundle 是经过签名的二进制文件,可将应用的代码和资源整理到模块中,如下图所示。以蓝色标识的目录(例如 drawable/、values/ 和 lib/ 目录)表示 Google Play 用来为每个模块创建配置 APK 的代码和资源。

更多关于 App Bundle 的资料可以从查看 Android App Bundle 简介

使用 bundletool 命令行工具进行测试

App Bundle 的文件扩展名为 .aab,使用adb install是没办法直接将应用安装到手机的。Google 提供了bundletool 命令行工具实现 aab 文件的安装及信息查看。
当 bundletool 根据您的 app bundle 生成 APK 时,它会将这些 APK 纳入到一个名为 “APK set archive” 的容器中,该容器以 .apks 作为文件扩展名。请使用 bundletool build-apks 命令来生成 apks

java -jar bundletool-all-0.15.0.jar build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks    

apks 里面的内容如下,不同的 app 有所不同

由于部分版本 (Android 5.0 以下) 不支持 app Bundle 功能,个别场景我们需要返回全量包,想要完整的 apk,可以在上面的命令行后面加上--mode=universal的参数就行

java -jar bundletool-all-0.15.0.jar build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks --mode=universal

apks 里面就只有一个完整的 universal.apk,解压出来直接adb install universal.apk就好啦

生成的 apks 安装到 android 设备上需要执行下面的命令:

java -jar bundletool-all-0.15.0.jar  install-apks --apks=/MyApp/my_app.apks

估算 apks 文件实际安装到手机上的 apk 大小,可以使用下面的命令:

java -jar bundletool-all-0.15.0.jar get-size total --apks=/MyApp/my_app.apks

更多 bundletool 的使用说明可以查看:https://developer.android.com/studio/command-line/bundletool

Matrix-APKChecker

Matrix-ApkChecker 是微信针对 android 安装包的分析检测工具,根据一系列设定好的规则检测 apk 是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。Matrix-ApkChecker 以一个 jar 包的形式提供使用,通过命令行执行 java -jar ApkChecker.jar` 即可运行。
简单使用命令如下:

java -jar matrix-apk-canary-0.4.10.jar --apk <your_apk_path>

使用配置文件命令如下:

java -jar matrix-apk-canary-0.4.10.jar -apk <your_apk_path> --config <your_config_path>

具体的使用和操作可以查看:Matrix Android ApkChecker README

APP Bubdle 文件如何结合 Matrix-APKChecker 检查包大小信息

aab 文件通过 bundletool 命令行生成 apks 后 (其实 apks 就是个压缩文件),解压后发现里面就是按照相关的逻辑生成了多个 apk。google 会根据设备的相关配置,安装上对应的 apk 达到缩小 app 大小的目的。
由于我测试的 app,打包的 aab 通过 bundletool 命令行生成 apks 发现,其实里面就是一个 base-master.apk 和另外两个 base-arm64_v8a.apk 和 base-armeabi.apk。后两其实是根据手机是否为 64 位分出来的两个包,包含了不同的资源。只要知道其大小就好了,我只要用 Matrix-APKChecker 检查去 base-master.apk 就可以了。
所以对于 App Bundle 的 aab 文件,我处理的方式是

  1. 先使用 bundletool 生成 apks
  2. 解压出 apks 里的多个 apk
  3. 记录 base-arm64_v8a.apk 和 base-armeabi.apk 的文件大小,用 Matrix-APKChecker 检查去 base-master.apk
  4. 然后将数据整理以报告的形式展示

基于 Flask 的 web 页面

上面讲了 App Bundle 和 Matrix-APKChecker 的内容,下面讲解下 Web 页面是怎么实现的

路由及页面跳转

route templates
/ home.html
/<package> dashboard.html、dashboard_aab.html
/<package>/reports/<report_path> report_template.html、report_template_aab.html

dashboard 页面展示

上传 apk

点击上传 apk 的按钮后,弹出的弹框相关操作使用了 Bootstrap 的模态框(Modal),判断是否为 apk、aab 的文件,上传到工程目录下的 update 文件夹下,执行 run,运行生成报告 html 文件,并弹出 alert 提示后自动跳转到详情报告页面。


server.py

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    '''上传apk、aab文件'''
    if request.method == 'GET':
        return "is upload file ... "
    else:
        path = request.form.get('upload_path')
        file = request.files['upload_file']
        file_name = file.filename
        if os.path.splitext(file_name)[-1][1:] not in ['APK', 'apk', 'aab']:
            return jsonify({"code": 200, "info": "文件:%s 不是apk or aab,请重试" % file_name})
        else:
            base_dir = config.tmp_upload_path
            if not os.path.exists(base_dir):
                mk_dir(base_dir)
            base_dir = config.tmp_upload_path
            file.save(os.path.join(base_dir, path, file_name))
            logger.info('上传%s' % file_name)
            report_info = run(os.path.join(base_dir, path, file_name))
            return jsonify({"code": 200, "info": "%s报告生成成功,即将跳转%s" % (file_name, report_info['report_path']),'report_info': report_info})

home.html、 dashboard.html、 dashboard_aab.html

$('#upload').click(function() {
    var upload_path = $('#upload_path').text();
    var formData = new FormData($('#upload-form')[0]);
    formData.append("upload_path", upload_path);
    window.alert("文件后台上传处理中,请稍后");
    $.post({
        url: '/upload',
        dataType: 'json',
        type: 'POST',
        data: formData,
        async: true,
        cashe: false,
        contentType: false,
        processData: false,
        success: function(returndata) {
            if (returndata['code'] == 200) {
                var info = returndata['info'];
                alert(info);
                var report_path =returndata['report_info']['report_path'];
                window.location.href = report_path;
            }
        },
        error: function(returndata) {
            alert("报告生成失败!")
        }
    })
});
报告统计页面展示

统计页面左侧可以切换不同的 apk 包查看相应的包历史大小信息,由于没有使用到数据库,每次生成的 html 都会保存在 Project_path/Report/<package_name>/reports 的路径下面,并且更新文件夹下的rs.json文件。 每次的页面加载获取数据都是读取Project_path/Report/下的文件目录结构和相应的rs.json文件信息后展现出来的。

  • 默认导航栏显示的名称为Project_path/Report/下各个<package_name>文件夹的名字
  • 想要左侧的导航栏内的不显示 packagename 而是对应的 app 名称,可以去config.pydict internalapp中填写上自己 app 的包名和 APP 名称
  • 同一个包名的,apk 和 aab 会存放在两个文件夹下,aab 的包的会自动生成以_aab结尾的文件夹

由于 apk 和 aab 的数据内容有差别,web 页面展示也有所不同,统计页面分别以dashboard.html、 dashboard_aab.html模板,要是刚开始没有任何报告的时候 (Project_path/Report/都还没有的时候),则以home.html为模板展示首页。


server.py

@app.route('/')
@app.route('/<package>')
def statistics(package=None):
    '''模板生成统计报告'''
    folders = get_report_folders()
    if folders:
        if not package:
            package = list(folders.keys())[0]
        else:
            pass
        if not os.path.exists(os.path.join(config.report_folder, package, 'rs.json')):
            pass
        else:
            reports_info = json.loads(read_file(os.path.join(config.report_folder, package, 'rs.json')))
            reports_info['last']['package'] = package
            report_list = reports_info['total']
            script_list = make_script_str(report_list)

            page, per_page, offset = get_page_args(page_parameter="page", per_page_parameter="per_page")
            pagination_users = get_users(report_list, offset=offset, per_page=per_page)
            pagination = Pagination(
                page=page,
                per_page=per_page,
                total=len(report_list),
                record_name="users",
                css_framework="bootstrap4"
            )

            context = {
                'urls': pagination_users,
                'size_list': script_list[1],
                'time_list': script_list[0],
                'package': package,
                'folders': folders,
                'appname': reports_info['last']['appname']
            }

            if 'aab' in package:
                return render_template("dashboard_aab.html", context=context, per_page=per_page, pagination=pagination)
            else:
                return render_template("dashboard.html", context=context, per_page=per_page, pagination=pagination)
    else:
        return render_template("home.html")

包打大小曲线图绘制

报告上的折线图是从Highcharts 演示 › 可缩放的时间轴修改而来的,在dashboard.html、 dashboard_aab.html内编辑好相应 html 和 js 代码后,传入的数据为 apk 的大小及报告的创建时间,都是在rs.json中已经生成保存好了的。
详情报告页面的,饼图也是用的Highcharts 演示 › 基础饼图

这个网站提供了丰富的报表展示效果,支持在线编辑和调试,提供多种主题的选择。

表格及分页

每个包大小详情的基本数据组成的表格在 web 页面展示,内容传入也都是在 rs.json 的total中已经保存好了的。只需要将数据传入就行


dashboard.html

<div class="table-responsive">
          <table class="table table-striped table-sm table-bordered">
            <thead class="table-dark">
              <tr>
                <th>#</th>
                <th>创建时间</th>
                <th>APP版本</th>
                <th>versioncode</th>
                <th>包体大小</th>
                <th>详情</th>
              </tr>
            </thead>
            <tbody>
              <!--这里写for开头-->
              <!--{% for url in context["urls"] -%}-->
              <tr>
                  <td>{{ loop.index + pagination.skip }}</td>
                  <td class="text-left">{{ url['create_time'] }}</td>
                  <td class="text-left">{{ url['versionName'] }}</td>
                  <td class="text-left">{{ url['versionCode'] }}</td>
                  <td class="text-left">{{ url['apksize'] }}</td>
                  <td><a href="{{ url['report_path'] }}"><span style="color: #0077FF;">查看详情</span></a></td>
              </tr>
              <!--{% endfor -%}-->
              <!--这里写for结束-->
            </tbody>
          </table>
        </div>
        {{ pagination.links }}

表格要是数据很多,在一个页面展示就会显得页面很长,因此需要对表格进行分页切换的处理。就是这个东西

Google 了一下发现 flask 有相应的模块,就直接哪来用就是了。Flask-paginate/


python

from flask_paginate import Pagination, get_page_args
from flask import render_template

def get_users(users,offset=0, per_page=10):
    return users[offset: offset + per_page]

@app.route('/')
def pages():
    page, per_page, offset = get_page_args(page_parameter="page", per_page_parameter="per_page")
    users = [{"create_time": "2020-05-05 19:52:32","size": "39.23"},     
             {"create_time": "2020-05-06 19:52:32","size": "39.23"},
                    ...
                    ...
            ]    
    pagination_users = get_users(users,offset=offset, per_page=per_page)
    pagination = Pagination(
        page=page,
        per_page=per_page,
        total=len(users),
        record_name="users",
        css_framework="bootstrap4"
    )
    return render_template('index.html', users=pagination_users, per_page=per_page, pagination=pagination)
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9876, debug=True)

index.html

{{ pagination.info }}
{{ pagination.links }}
<table class="table table-striped table-sm table-bordered">
  <thead>
    <tr>
      <th>#</th>
      <th>创建时间</th>
      <th>包体大小</th>
    </tr>
  </thead>
  <tbody>
      {% for url in users -%}
      <tr>
          <td>{{ loop.index + pagination.skip }}</td>
          <td class="text-left">{{ url['create_time'] }}</td>
          <td class="text-left">{{ url['size'] }}</td>
      </tr>
      {% endfor -%}

  </tbody>
</table>
{{ pagination.links }}

页面的 UI 优化

页面很多是表格的展示,因为是基于和Bootstrap 的,所以根据Bootstrap-Tables 配置文档对模板 html 进行相关的修改就可以展现较为美观的页面了。
例如想把表格展示位黑的 ,加上class="table-dark" 就好了。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 6 条回复 时间 点赞


请问这个报错什么问题? 上传 apk 包,提示 (文件上传中,请稍后,) 点击确定,就报错

阿三 回复

java not found 电脑 java 的环境没有配置好?

linpengcheng 回复

嗯,已投入项目使用.
再问下微信 Matrix 开源项目,demo 我已经运行到 Android 设备, 但是接下来不懂怎么用,查过资料,没这方面详细,请指定下

阿三 回复

接入 android 项目 然后上报到你需要的地方 进行性能数据的统计分析吧 我这儿没用这个 。只是拿这个 jar 包分析 apk 资源用。

在 windows7 上启动,生成的报告,会乱码

jcgao 回复

好吧 我是在 mac 上运行的 哪里乱码截图看看

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册