引言
当谈到包体积优化时jar中没有主清单属性,网上不乏优秀的方案与文章,如 混淆、资源、ReDex、R8、SO 优化等等。
但聊到 包体积监控 时,总是感觉会缺乏落地性,或者总是会下意识认为这可能比较麻烦,需要其他部门连同配合。通常对于有APM基础的团队而言,这倒不算什么,但往往对于小公司而言,到了这一步,可以说就戛然而止。
但回到问题本身,这并非难事。或者说,其实很简单 :)
计算差异 、通知 、汇总数据 ,三步即可。
按照最朴素的想法,无论多大的团队,也能至少完成前两步,事实上,也的确如此。
故此,本篇将结合实际需求以及背景,使用 Kotlin 去写一个 APK差异化 对比的基础 CLI 工具,并搭配 CI 完成流水线监控。
本篇并不涉及深度源码等,更多是实操,所以文章风格比较轻松,可放心食用
写在开始
关于 CLI(command-line interface) ,每个开发同学应该都非常熟悉,可以说基本就是日常操作,比如我们经常在 命令行 里会去敲几个命令,触发几个操作等,常见的 git、gradle、java 等。
在图形化(GUI)的现在,CLI 往往代表着一种 老派风格 ,有人抵触,觉得繁琐,当然也有同学觉得简单直接。
但总体上的趋势是,越来越多工具趋于图形化。不过两者依然处于一种 互补 ,而非竞争,不同场景也有各自的优势以及差异化。比如在某些场景下,当我们需要去 简化开发流程 时,此时 CLI 就会作为首选项就会映入眼前。
聊聊背景
最近在做 下厨房-懒饭App 的体积优化,优化做完了(后续出文章),那如何做防劣化呢?
因为我们的项目是在 Github 上托管,所以自然而然也有相应的 Action 作为check,所以此时首先最基础想的就是:
先找轮子
思路有了,那关键的工具,diff工具 怎么搞?
作为一个正经的开发仔,所以此时首选肯定是去 Github Action 市场上找现成的(没事就别乱造轮子,你造的又没人家好)。
结果发现,还真有,真不戳!
来自微软的开源,那肯定有保障啊!
集成看看效果:
嗯,看着还不错,不过这个输出怎么改呢,官方只有MD格式,而且看着过糙,作为一个稍微有点审美的同学。
那就考虑先 fork 改一下呢,fork 前看了一下仓库:
我是辣鸡,这下触摸到知识盲区了,压根不知道怎么改,无疑大大增加了后续迭代成本,以及看看上一次的版本时间(此处无声胜有声)。
那既然没有合适的 Action ,那就自己找一个 jar 工具也行啊,于是又去找了一下现有的jar工具,发现只有腾讯的 matrix-apk-canary 可用,但是这也太顶了吧。虽然功能强大,可是不符合我们现在的需要啊,我还得去手动算两次,然后再拿着json结果去对比,想想就复杂。
回到我们现在,我们完全不需要这么复杂,我们只是需要一个 diff工具 而已。
既然没有合适,那就自己造一个,反正diff逻辑也并不复杂。
万事开头难Jar怎么写?
是的,我也没写过这玩意,但本能觉得很简单。
先去 IDE 直接创建个项目,感觉应该选 JVM ,依赖配置上 Gradle 也更接近 Android 开发者的使用习惯,具体如下:
初始化项目
凭着以前用 IDE 学 Kotlin 时的记忆,Jvm 参数应该是在这里进行传递:
Main方法传参
输出也没啥问题,正常打印了出来:
Hello World!
Program arguments: Petterp,123
但这不是我要的样子啊,我的 理想状态 下是这种操作:
java -jar xxx.jar -x xxx
不过就算现在能直接这样使用jar中没有主清单属性,也不能进行快速开发,首先调试就是个麻烦事。
再回到原点,我甚至不知道怎么在命令行传参呢
说说CLIKT
此时就不得不提一个开款库,用 Kotlin 写 CLI 的最强库: CLIKT ,也是无意之间发现的一个框架,可以说是神器不足为过。
简介
Clikt(发音为“clicked”)是一个多平台的 Kotlin 库,可以使编写命令行界面变得简单和直观,它是“Kotlin 的命令行界面”。
该库旨在使编写命令行工具的过程变得轻松,同时支持各种用例,并在需要时允许高级自定义。
Clikt 具有以下特点:
简而言之,Clikt 是一个功能丰富的库,可以帮助开发者快速构建命令行工具,同时具有灵活的自定义和多平台支持。
以上来自官网文档。
依赖方式
因为我们是使用 Gradle 来进行依赖管理,所以直接添加相应的依赖即可:
implementation("com.github.ajalt.clikt:clikt:3.5.2")
同时因为使用的是 Gradle ,所以默认会带有一个 application 插件,因此提供一个 Gradle 任务,来将我们的 jar和脚本 控绑在一起启动(run Main时),从而免除了每次调试都要在命令行 java -jar xxx,非常方便。
示例效果
代码也非常简单,我们定义了两个参数,count 与 name,其中 count 存在默认参数,而 name 没有,故需要我们必须传递,直接运行run方法,然后根据提示键入value即可,就这么简单。
在往常的jar命令里,通常都只存在一次性输入的场景。比如必须直接输入全部kay-value,如果输入错误,或者异常,日志或者输出全凭jar包开发者的自觉程度。可以说大多数jar包并不易用,当然这主要的原因是,传统的cli开发的确比较麻烦,并不是所有开发者都能完善好边界。
使用 CLIKT 之后,上面的问题可以说非常低成本解决,我们可以提前配置提示语句,报错语句等等。它可以做到提示使用者接下来该输入什么,也可以做到对输入进行check,甚至如果输入错误或者不符合要求,直接会进行提示,也可以选择继续让用户输入。
上述的示例只是非常简单的一个常见,CLIKT 官网有更多的用法以及高级示例,如果感兴趣,也可以看看。
常见问题如何打jar包
上面我们实现了 jar包 的编写和本地调试,那该怎么打成 jar包 在命令行运行呢?
因为我们使用了 Gradle 进行依赖配置,那么相应的,也可以使用附带的命令即可,默认有这几个命令可供选择:
这里感谢 虾哥(掘金: 究极逮虾户) 解惑,原本以为 exec 这种方式会导致传参时的部分默认值无法设置问题。
jar包没有主清单属性
上面打完jar包,在命令行运行时,报错如下:
xxx.jar中没有主清单属性
这是什么鬼,不是已经配置过了吗?直接 run main 方法没有什么问题啊?
application {
mainClassName = 'HelloKt'
}
经过一顿查阅,发现配置需要这样改一下,build.gradle 增加以下配置:
jar {
exclude("**/module-info.class")
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
manifest {
attributes 'Main-Class': "HelloKt"
}
}
原理也很简单,你打出来的 jar 包得配置路径啊。我们调试时走的 application 插件里的 run 。而打 jar 包, jar 命令没配置,导致其并不知道你的配置,所以不难理解为啥找不到主清单属性。
再聊实现思路
要对比 Apk 的差异,最简单的思路莫过于直接解压Apk。因为 Apk 其实是一种 Zip 格式,所以我们只需要遍历解压文件,根据文件后缀以及不同的文件夹分类统计大小即可,比较简单粗暴。
当然如果要做的更精细一点,比如要统计 资源文件差异 、代码增长 、aar变化 等,就要借助其他方式,比如 Android 团队就为我们提供了 apkanalyzer,或者可以通过 META-INF/MANIFEST.MF 文件作为基准进行对比。
业内开源的比较好的有腾讯的 matrix-apk-canary,其设计灵巧,功能也更加强大,具体在实现上,我们也可以借鉴其设计思想。
因为本次我们的需求无需上述那么复杂,只需要在意 apk 、资源 、dex 、lib 等差异,所以直接采用手动解压Apk的方式,手动统计,反而更加直接。
核心代码
思路如下:
匹配与模型设计
自定义规则
文件Model
一些小Tips关于分层的想法
一个合格 CLI 设计,基本应该包含下面的流程:
配置 -> 分析 -> 输出
灵活运用语言技巧
value class
Kotlin 内联类 是一个很棒的特性,无论是性能还是可读性方面,如果我们有某个字段,是使用基本类型作为定义,那么此时就可以考虑将其定义为内联类。
比如我们本篇中的 file大小(size字段),通常我们会使用 Long 类型进行代表,但是 Long 类型用于展示而言,可读性并不好,所以此时使用内联类对其进行包装,并搭配 操作符重载 ,使得开发中的体验度会提高不少。
关于CI方面
关于 CI 方面,首选就是 Github Action,具体 Github 也有专门的教程,上手难度也很低,几分钟足以,对于经常写开源库的作者而言,这个应该也算是基本技巧。相应的,既然我们也是产出了一个 CLI 组件,那么每次 release 时都手动上传jar包,或者版本的定义上,如果每次都手动修改,怎么都显得 不优雅 。
故此,我们可以考虑每次 发布新的release版本 之后,就触发一次 Action,然后打一个 jar 包,并将其上传到我们最新的 release 里。相应的,自动化的版本也可以在这里进行匹配,都比较简单。
这里,以自动化发布jar为例:
name: Cli Release
on:
release:
types: [ published ]
permissions: write-all
jobs:
build_assemble:
runs-on: ubuntu-latest
env:
OUTPUT_DIR: build/libs
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- uses: burrunan/gradle-cache-action@v1
name: Cache gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build jar
run: ./gradlew jar
- uses: AButler/upload-release-assets@v2.0
with:
files: build/libs/apk-size-diff-cli.jar
repo-token: ${{ github.token }}
release-tag: ${{ github.event.release.tag_name}}
总体步骤依然非常简单,我们定义这个工作流的触发时机为每次 release 时,然后 拉代码、配置gradle、打jar包、上传到最新release-assets里。
效果如下:
最终效果
最终搭配 Github CI 实现的效果如上,开源地址 apk-size-diff-cli。
使用方式也非常简单,本地使用的话,执行 jar 命令(或者使用 exec 的方式,免除 java -jar) 即可,如下示例所示:
java -jar apk_size_diff_cli.jar -b base.apk -c current.apk -d outpath/result -tss 102400
默认会在指定的输出路径,如 outpath/result 输出一个名为 apk_size_diff.md 的文档。
其中 -tss 指的是默认各类别的阈值大小,比如 apk、dex 等如果某一项本次对比上次超过102400,则输出结果里会有相应提示。
如果大家对这个组件比较感兴趣,也不妨点个Star,整体实现较为干净利落,fork更改也非常简单。
结语
本篇到这里就算结束了,总体也并不算什么高深技巧或者深度文章,更多的是站在一个 技术需求 的背景下,由0到1,完成一个 CLI 组件的全流程开发,希望整个过程以及思考会对大家有所帮助。
参考关于我
我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮助,欢迎 点赞、评论、收藏,你的支持是我持续创作的最大鼓励!
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688