本来计划五一约上几个朋友去一趟青岛的,吃蛤蜊喝啤酒,去感受下青岛人民的热情。 实在是没想到,最近国内疫情反复,没去成...
最近露营比较火,那就找个人少的地方烧烤吧,五一转了一大圈,开放的河边不让烤,让烤的河边封闭了。
不但没去上青岛,没烤上串,我们还居家办公了...
最近的股票也是这样的...所以我卸载了股票App,基本算是躺平了
最近公司的项目也是非常的忙,因为是0-1的项目,所以很多基建的工作需要做,今天把基建过程中编译打包的一部分总结了一下,给大家分享。
项目结构
我目前在做一个三方的SDK,我们的项目结构主要由2个SDK和一个Demo组成
-
由于提供给三方使用,因此打包上传的链路跟集团的2方SDK链路就完全不同了
-
三方SDK需要把包上传到Maven中心仓库,三方可以访问到的位置。
-
具体的maven上传链路可以看这篇文章juejin.cn/post/707010…
最终都是产出一个包,为什么还需要打包方案?
公司打包的链路
公司打包有一个统一的打包平台,类似外部经常使用的Jenkins,打包链路相对比较清晰,主要有以下几步。
-
1.首先注册我们需要打包的应用。
-
2.配置我们打包的流水线,流水线就好比打包的执行链路
-
3.在流水线添加具体的工作环节,Jenkins通过shell也可以实现,比如首先通过git拉取代码分支,然后执行我们的打包脚本,最后进行产物上传。
-
4.如果有其他自动化诉求可自定义添加,比如各种卡口,包大小检查,单元测试,风险/合规测试等。
-
5.创建一个打包单,填写本次打包的版本,代码分支,选择对应的流水线即可。Jenkins可以通过代码配置文件实现该功能,也可以做定制页面去执行该操作。
我们要做什么?
在以上思路完成后,我大概列了下我们需要做的事
- SDK
- Demo
同样需要动态依赖,本地打包全部依赖SDK源码,打包机打包需要通过下放参数动态引用SDK的Snapshots AAR,Release AAR。
整体打包细节拆解
参数下发/解析
- 首先我先普及下传参思路,打包开始前,需要从打包平台下发当前打包的参数,我们可以通过脚本将所需参数暂存到环境中,编译时通过暂存的KEY获取即可。
//SDK gradle可通过这种方式获取相应参数
def sdkVersion = java.lang.System.getenv("VERSION_NAME")
- 当然为了便捷,也可以将数据生成一个Json文件,存到项目固定目录,打包开始前首先读取对应Json,将相关参数做解析,并动态使用,如下所示。
{
"other": [
"buildType":"Snapshots",
],
"dependencies": [
{
"artifact": "CoreSDK",
"type": "AAR",
"version": "1.0.0",
"group": "com.sdk.core"
},
{
"artifact": "UISDK",
"type": "AAR",
"version": "1.0.0",
"group": "com.sdk.ui"
}
],
"exclude": []
}
- Android工程可通过JsonSlurper进行Json解析
import groovy.json.JsonSlurper
def buildJson = file('../buildJson.json')
def jsonSlurper = new JsonSlurper()
def jsonObject = jsonSlurper.parse(buildJson)
根据参数动态引用
if (jsonObject.other.buildType == 'Snapshots'
||jsonObject.other.buildType == 'Release') {
println("implementation 引用SDK aar")
implementation(group: jsonObject.dependencies[0].group, name: jsonObject.dependencies[0].artifact, version:jsonObject.dependencies[0].version)
implementation(group: jsonObject.dependencies[1].group, name: jsonObject.dependencies[1].artifact, version:jsonObject.dependencies[1].version)
} else {
println("implementation 引用SDK工程")
implementation project(':UISDK')
}
构建产物动态上传
//这个值是从mtl下发的
if (jsonObject.other.buildType == 'Snapshots') {
apply from: './upload_snapshot.gradle'
} else if (jsonObject.other.buildType == 'Release') {
apply from: './upload_release.gradle'
} else {
apply from: './upload_local.gradle'
}
打包
以上普及完成,我们开始打包。
-
1.打包CoreSDK
-
2.打包UISDK,一切都顺利。
-
3.打包引用SDK的Demo,报错了。
Multiple dex files define com.a.ds.a: Multiple dex files define Lcom/a/ds/aa.class
这个问题当时感觉有点奇怪,明明没有重复的包名类名,为什么会编码报错类冲突呢?
后来发现,我们编译用的混淆文件proguard-rules.pro基本都是默认配置,默认配置在混淆过程中,会通过默认的abcdefg这种顺序的字母组合去替换我们代码中自定义的类名,接口名,方法名,以及变量名,以此来达到混淆的目的。
这里插一句,其实经常有人有误区,以为混淆打包就是为了防止被反编译,其实不然
- 混淆只是将我们自定义的各种代码名字进行了重命名,将名称意义抹除,替换成了abcd这种无任何意义的名字,同时也把未引用的代码移除。
- 所以混淆只能提高代码的安全性,增加反编译的后代码的阅读成本,同时也会缩减包体积。
如果混淆都使用默认的配置,那其实是有一定概率产生混淆后的类冲突,比如混淆后都叫com.a.ds.aa.class,此时可以通过自定义混淆字典实现,混淆后的代码即可规避冲突风险。
-obfuscationdictionary filename.txt
- filename.txt
coreu_do
coreu_if
coreu_for
coreu_int
coreu_new
coreu_try
coreu_byte
coreu_case
coreu_char
coreu_else
coreu_goto
提测
一顿操作结束,Demo打包成功,并通过脚本上传到了服务器,通过Demo下载地址产出一个二维码,将二维码提测给QA团队,测试异常打回,测试成功发版。
发版
-
1.发版的打包过程与测试打包环节类似,只是构建产物上传地址差异。
-
2.发版前会强制代码review,如果不review无法打包发布。
-
3.代码review结束会将dev开发分支合并到master分支,进行代码统一管理。
-
4.发布后的代码会通过tag进行打标,记录当前发布的版本。
-
5.版本发布成功,会打一个回归包进行简单回归。
-
6.如果有渠道包诉求可以在这个环节产出。
回归
如果回归测试发现问题,需要按情况分析,如果涉及功能阻塞,需要先将功能开关关闭(每个功能都有开关),其次加发修复包,修复包在技术流程跟发正常版本一样,在管理流程需要抄送高级别领导知晓,并在问题解决后一周内进行复盘。
归档
版本发布结束后,打包单会冻结,并打上已发版的标签,后续版本迭代重新拉取代码分支,重新创建打包单。
打包过程优化
作为三方使用SDK,有3个硬指标
包体积
包体积肯定是越小越好,这里我补充一个包体积优化的3个核心做法
-
1.删减,压缩图片资源
-
2.转换图片资源webp格式
-
3.资源混淆
删减资源
想删减无效资源,首先得知道哪些资源无效,目前通用做法一般有2种。
- 1.编译过程可以通过lintOptions标签配置资源检测,通过./gradlew lint触发检测task,通过产出的lint-results.html进行删减资源
lintOptions {
check "UnusedResources" //只检查无用资源"UnusedResources","UnusedIds"
checkDependencies true //对依赖的资源也执行检查,注意也会对引用aar检查
}
- 2.通过字节的开源框架ByteX可以更深层的检测无用assets/Resource,具体使用可以看这个 github.com/bytedance/B…
resCheck {
enable true // 无用资源检查的开关
// 根据资源所在的路径做模糊匹配(因为第三方库用到的冗余资源没法手动删)
onlyCheck = [
// 只检查主工程里的资源
"app/build",
"business-support/build",
"dynamic/build",
"home/build",
"search/build",
"stepping-stone/build"
]
// 检查白名单。这些资源即便是冗余资源也不会检查出来
keepRes = [
"R.dimen",
"R.color",
"R.animator",
"R.integer",
"R.bool",
"R.style",
"R.styleable",
"R.attr",
"R.xml",
"R.array",
"R.string"
]
}
压缩图片
-
我们很多的资源图片是需要打包带进去的,基本图片资源压缩会是优化体积的重要手段。
-
一般UI或视觉工程师给我们的图都是原图,我们可以通过比较有名的tinyping平台做图片压缩,但是tinyping压缩算法没有开源,也有压缩数量限制,并且对已经在的资源还需要一个个拿出来压缩,无法自动化。
-
McImage是一个比较成熟的压缩插件,可参考github.com/smallSohoSo…,对多个位置的图片资源都可以进行压缩,压缩算法开源。
压缩范围
- Jar包中的图
- AAR中的图
- 子Module中的图
压缩算法
使用方式
McImageConfig {
isCheck true //default true
isCompress true //default true
maxSize 1*1024*1024 //default 1MB
maxSize 60 * 1024 //大图片阈值,default 100KB
enableWhenDebug false //debug下是否可用,default true
isCheckPixels true // 是否检测大像素图片,default true
maxWidth 2000 //default 1000 如果开启图片宽高检查,默认的最大宽度
maxHeight 2000 //default 1000 如果开启图片宽高检查,默认的最大高度
whiteList = [ //默认为空,如果添加,对图片不进行任何处理
"ic_launcher.png"
]
mctoolsDir "$rootDir"
multiThread false //是否开启多线程处理图片,default true
bigImageWhiteList = [ // 对上层业务中加入的大图,增加白名单
"big.png"
]
}
- 编译过程中,pngquant 压缩 png 图片,guetzli 压缩 jpg 图片, 并替换
[Compress][/home/admin/.emas/build/13314914/workspace/stepping-stone/build/intermediates/packaged_res/release/drawable-xxhdpi-v4/share.png][oldInfo: 493][newInfo: 307]
[Compress][/home/admin/.emas/build/13314914/workspace/stepping-stone/build/intermediates/packaged_res/release/drawable-xxhdpi-v4/default.png][oldInfo: 16510][newInfo: 1031]
转换图片资源webp格式
- 在图片压缩的基础上,还可以做进一步的转化处理,将png无损转换为webp,webp有格式优势,具备更优的压缩率.
- 该转换存在内部逻辑,如果转换后体积增大则不使用,转换后体积减少则替换原格式。但是需要注意Android在4.3以上系统才完全支持webp格式
McImageConfig {
optimizeType "ConvertWebp"
isSupportAlphaWebp true
}
[Webp][/home/admin/.gradle/caches/transforms-2/files-2.1/49e45938bdf3a608d0bb31bae299e270/res/drawable-xhdpi-v4/check.png][oldInfo: 423][newInfo: 324]
[Webp][/home/admin/.gradle/caches/transforms-2/files-2.1/801ea15ef7c3dc1dea3117fae865b2bc/res/drawable-mdpi-v4/light.png][oldInfo: 310][newInfo: 292]
[Webp][/home/admin/.gradle/caches/transforms-2/files-2.1/432f7fc4743a824d19b5b2652e87345e/res/drawable-hdpi-v4/location.png][oldInfo: 1204][newInfo: 650]
[Webp][home_arrow.png] do not convert webp because the size become larger!
资源混淆
-
与代码混淆类似,资源同样可以做混淆,目前相对成熟的方案可以使用AndResGuard github.com/shwenzhang/…
-
这个一般用于Apk压缩(AAR需要改动),我们需要提供打包完成的Apk,然后将签名删除,再将Apk解压,然后将资源按abcdefg这种顺利进行重命名,抹除原有名称,同时将arsc索引表与之对应修改,完成之后重新进行打包,整个过程不会牵扯编译过程,而是在Apk编译成功之后增加的一个处理任务,对其资源进行混淆重排,最后可通过7z压缩算法对apk进行整体压缩。
长效机制
- 随着版本的迭代开发,增加编译卡口,每次编译对新增资源做体积检测,发现图片超过阈值,则直接终止编译,报错指向增量较大的资源,提示修改压缩后再进行编译集成。必须有这种卡口机制才是长效的保证