写点什么

Android 包体积优化上篇 - 资源混淆优化

作者:百度Geek说
  • 2022 年 3 月 01 日
  • 本文字数:3785 字

    阅读完需:约 12 分钟

Android包体积优化上篇- 资源混淆优化

导读:什么时候进行包体积优化?一般在 app 初创期时,由于业务代码较少,包体积也不大,相应这个时候对包体积的优化收益也较少。当业务逐渐成熟功能,迭代逐渐变多,包体积也会逐渐增加。


增加包体积主要影响如下几个方面:


1: 如果针对 app,会影响到下载转化率,但手百内部直播属于插件,所以不存在转化率的问题,存在插件的下载时长问题,进而在一定程度上影响插件冷启首屏;


2: 渠道商合作的要求,这个要求只在 app 里面,如果是插件不受其影响,app 的体积越大,渠道商的资费会更高,如要求更大的 ROM 空间,更长的安装时间,这些都极大影响手机已经用户体验。


3: 同时更大的包体积,不管是插件还是 app,也意味着更长的安装时间,更长的 oat 时间(Android 系统 9 之前,安装会自动转换为 oat)。


本文章依赖的知识点有 Gradle 插件编译流程、Gradle 插件编写、Groovy、 Android 打包流程、Android AAPT 流程等。


全文 4221 字,预计阅读时间 12 分钟。


一、apk 包的组成

Android 安装包是由很多部分组成的,主要有如下:


  1. 代码相关:class.dex

  2. 动态库(SO)相关:lib

  3. 资源相关 resource.arsc res/, assets

  4. META-INF 存放签名等信息


通过以上包体积的组成部分,不难分析,要想控制 apk 的包体积,需要在 dex、动态库、资源数据上进行控制。动态库(SO)可通过动态下载、按需下载的方式;其次只有 dex 和资源,dex 是我们下一篇里面要讲的东西,本片主要讲资源的优化。

二、资源优化

针对图片资源,使用 Android Size Analyzer 可生成资源的排序列表,通过分析后生成结果,可对分析后的图片采取优化手段,如图片压缩、云控资源、webp、矢量图、移除冗余资源等,以上所有这些都是关于单张图片的处理,本篇主要谈资源的混淆优化,整个资源混淆的实现是基于 AndroidResGuard 做一个二次开发扩展。


为什么要做二次开发?整个直播是基于插件实现,原工具不支持插件资源混淆;大家知道为了解决插件与宿主之间资源 id 冲突的问题,插件都需要固定器 resid 的 package 部分, 如下资源 id 的组成:


[<package_name>.]R.<resource_type>.<resource_name>针对插件//additionalParameters "--package-id", "0x61", "--allow-reserved-package-id"
复制代码


简单介绍下资源映射表协议的原理,资源混淆其原理非常简单:通过如下的映射关系,将复杂的字符串,转换为一个简短的字符串,进而减小资源映射表的字符串常量池的大小。


如下图为混淆前和混淆后对比,一旦加入 packageid,整个资源混淆会出错,经过对资源映射表二进制以及协议的对比发现,是由于 arsc 在进行扩展 packageid 的时候,与协议不符合导致,问题定位以后,即找到合适的解决办法,即扩展 arsc 资源映射表协议。




通过上图可以看出混淆后,资源映射表体积大幅度减少。

三、资源映射表协议

从上图中分析定位一个资源首先通过资源类型,然后通过资源 id,通过资源 id(ID),找到资源名(name),找到后根据当前机器的 mdpi/land/ ldpi,加载不同的文件路径下的资源。


resources.arsc 是以一个个 Chunk 块的形式组织的,Chunk 的头部信息记录了这个 Chunk 的类型、长度等数据。


从整体上来看,其结构为资源索引表头部+字符串资源池+N 个 Package 数据块。



001Editor 下的资源映射表结构如下:


四、实施

方案的实施包含三部分:方案的设计实现、QA 流水线配置、单功能版本灰度。


五、方案设计实现

由于是在原有工程上做二次开发扩展,需要对原有项目的结构有一个清晰的认识,包括到每一个细节,其中包括资源混淆规则、白名单怎么映射,从中理出我们的需求部分,包括资源白名单映射、夜间模式、映射表修改、映射表重写,整体代码结构如下图,黄标处为需要二次开发涉及的模块。



如上图总共涉及到 3 部分的核心工作,包括配置模块、插件模块、与核心的映射表混淆模块;


  • a: 配置模块配置如下参数:


mappingFile = file("./resource_mapping.txt")    use7zip = false    useSign = false    // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字    keepRoot = true     // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小//    fixedResName = "arg"    // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源    mergeDuplicatedRes = true      nightmode_pattern = "_1" // 夜间模式配置     // 基础的sdk都加入白名单, 配合白名单配置文件    whiteList = []
复制代码


动态配置参数,这个是针对插件特有的 fixid,生成白名单配置列表混淆资源不能将其混淆,在模块内部数据准备阶段,要将其偷传到 Gradle 插件内


// 生成fix文件白名单def generateWhiteList() {    def fixFile = file("host-res-fix/${rootProject.ext.plugin_fix_file_map[host]}")    def resGuard = file("resguard/res_guard_config.xml")    def resproguard = new XmlParser().parse(resGuard)    def issueNode = new NodeBuilder().issue(id: 'whitelist', isactive: true)    resproguard.issue[0].replaceNode (issueNode)    fixFile.eachLine { line ->// TODO 白名单行生成    }new XmlNodePrinter(new PrintWriter(resGuard)).print(resproguard)return resGuard}
复制代码


  • b:资源映射表读写:


资源映射表读取的目的是,形成基本的 arsc 框架,架构形成后,通过映射逻辑将内存数据重新写回到 arsc, 可如下图结构:



扩展协议部分是支持插件资源混淆的核心,基本规则就是按照扩展协议进行顺序读写即可。


代码片段如下:


private void writeLibraryType() throws AndrolibException, IOException {  checkChunkType(Header.TYPE_LIBRARY);int libraryCount = mIn.readInt();  mOut.writeInt(libraryCount);for (int i = 0; i < libraryCount; i++) {    mOut.writeInt(mIn.readInt());/*packageId*/    mOut.writeBytes(mIn, 256); /*packageName*/  }  writeNextChunk(0);while (mHeader.type == Header.TYPE_TYPE) {    writeTableTypeSpec();  }}private void readLibraryType() throws AndrolibException, IOException {  checkChunkType(Header.TYPE_LIBRARY);int libraryCount = mIn.readInt();int packageId;  String packageName;for (int i = 0; i < libraryCount; i++) {    packageId = mIn.readInt();    packageName = mIn.readNullEndedString(128, true);    System.out.printf("Decoding Shared Library (%s), pkgId: %d\n", packageName, packageId);  }while (nextChunk().type == Header.TYPE_TYPE) {    readTableTypeSpec();  }}
复制代码


<table cellpadding="0" cellspacing="0" width="768" height="NaN"><tbody style="border-radius: 0px;border-width: 0px;border-style: initial;border-color: initial;inset: auto;float: none;height: auto;line-height: 20px;outline: 0px;overflow: visible;vertical-align: baseline;width: auto;min-height: inherit;background-position: 0px center;"><tr style="border-radius: 0px;border-width: 0px;border-style: initial;border-color: initial;inset: auto;float: none;height: auto;line-height: 20px;outline: 0px;overflow: visible;vertical-align: baseline;width: auto;min-height: inherit;background-position: 0px center;"><td width="850" height="NaN" style="padding: 0px 0px 0px 15px;border-width: 0px;border-style: initial;border-color: initial;overflow: visible;border-radius: 0px;inset: auto;float: none;line-height: 20px;outline: 0px;vertical-align: baseline;min-height: inherit;background-position: 0px center;"><br></td></tr></tbody></table>

六、流水线配置

基于已经生成的 apk,做 apk 修改;明确定义输入输出


./gradlew :app:resguardUseApk -PHOST=${assemble_host} -PMODE=debug
复制代码


由于是修改 apk 内部文件结构影响面较大,版本发布采用单灰验证,经过单灰验证优化后的 apk 性能稳定,单功能版本灰度 &收益。


体积优化收益 8%,现在在多插件使用,包括主播端、开播端,下一步会在其他插件推广。

七、注意事项

通过 resouce name 获取资源的方式需要加入到白名单,体现形式如在插件中通过名字取宿主资源, 建议通过 lint 全局扫描 getIdentifier,使用


fixedResId = context.getApplicationContext().getResources()        .getIdentifier(resName, null, null);

uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + resources.getResourcePackageName(model) + '/' + resources.getResourceTypeName(model) + '/' + resources.getResourceEntryName(model));
复制代码

八、相关链接

https://developer.android.com/guide/topics/resources/providing-resources


https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md

九、推荐阅读:

百度APP视频播放中的解码优化


百度爱番番实时CDP建设实践


当技术重构遇上DDD,如何实现业务、技术双赢?


接口文档自动更改?百度程序员开发效率MAX的秘诀


技术揭秘!百度搜索中台低代码的探索与实践


百度智能云实战——静态文件CDN加速


化繁为简--百度智能小程序主数据架构实战总结


---------- END ----------


百度 Geek 说


百度官方技术公众号上线啦!


技术干货 · 行业资讯 · 线上沙龙 · 行业大会


招聘信息 · 内推信息 · 技术书籍 · 百度周边

用户头像

百度Geek说

关注

百度官方技术账号 2021.01.22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
Android包体积优化上篇- 资源混淆优化_百度_百度Geek说_InfoQ写作平台