3

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

 2 years ago
source link: https://juejin.cn/post/7070013197054394399
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2022年03月01日 06:23 ·  阅读 351
架构师 @ 百度

图片

导读:什么时候进行包体积优化?一般在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();  }}
复制代码

六、流水线配置

基于已经生成的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));
复制代码

八、相关链接

developer.android.com/guide/topic…

github.com/shwenzhang/…

九、推荐阅读:

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

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

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

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

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

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

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

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

百度 Geek 说

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK