探讨apk转aar的可行性
碎碎念
最近不忙了,闲下来就有时间去思考,如何让打包更轻松(写更少的脚本,做更少的事情)。因为工作的原因,日常不是在打包就是在修改打包脚本的路上。这边想采用apk转aar的主要原因是想直接通过AS来出包了,在我的猜想下,打包速度应该会提升,并且不需处理目前使用apktool解包替换资源回编,遇到的各种问题。但是现实往往与想象差距甚远,打包速度并没有明显的提升(哪怕少了解包的这个过程,对比apktool的完成流程并没有占据较大的优势)。而且转aar本身也需要使用到apktool(毕竟有人维护,而且一直以来也是用他 )。
前期查资料阶段,先给出我觉得有用的资料
库配置不正确
如果您的应用依赖于使用旧版 Android SDK Build Tools 构建的第三方库,您的应用可能会在运行时崩溃,且不会显示任何错误或警告。之所以会发生此崩溃,可能是因为在创建库的过程中,将 R.java
字段声明为 final
,从而导致所有资源 ID 都被内嵌在该库的类中。
AAPT2 依赖于在构建应用时能够将 ID 重新分配给库资源。如果该库将这些 ID 视为 final
并将其内嵌在库 dex 中,便会出现运行时不匹配的情况。
如需解决此错误,请与库创建者联系,以使用最新版本的 Android SDK Build Tools 重新构建该库,然后重新发布该库。
从 res/ 读取资源的唯一方法是使用资源 ID
保存在 assets/
目录中的文件没有资源 ID,因此您无法通过 R
类或在 XML 资源中引用它们。您可以改为采用类似普通文件系统的方式查询 assets/
目录中的文件,并利用 AssetManager 读取原始数据。
不过,如果您只需要读取原始数据(例如视频文件或音频文件)的能力,则可将文件保存在 res/raw/
目录中,并利用 openRawResource() 读取字节流。
结论
根据上面分析可知,如果我们通过修改apk,把dex里面视为 'final', 的代码 修改为引用R.java文件,并把分散在不同路径的R文件合并为一个(把不同的引用都指向同一个), 并生成R.txt , 那么Apk文件是可以转化为AAR的
转换为AAR的好处主要就是,可以直接通过AS出包(免去处理拆分dex,65536的烦恼, 替换资源文件,以及理论上应该能提升打包速度==》指二次打包,并且as能帮我们处理合并AndroidMainfests)
诚然,如果cp能直接提供aar是最好的,但是很多时候拿到我们手里的都是apk,为此以往都是采用apk二次打包,运用apktool解包,大部分都是通过python脚本来修改文件。
与 JAR 文件不同,AAR 文件会为 Android 应用提供以下功能:
- AAR 文件可以包含多项 Android 资源和一个清单文件,让您除了能够在 Java 类和方法中进行捆绑以外,还能够在布局和可绘制对象等共享资源中进行捆绑。
- AAR 文件可以包含 C/C++ 库,供应用模块的 C/C++ 代码使用。
库模块开发注意事项
在开发库模块和相关应用时,请注意以下行为和限制。
向 Android 应用模块添加对库模块的引用后,您可以设置它们的相对优先级。在构建时,库会按照优先级由低到高的顺序逐一与应用合并。
-
资源合并冲突
构建工具会将库模块中的资源与相关应用模块的资源合并。如果这两个模块中都定义了给定的资源 ID,系统会使用应用中的资源。
如果多个 AAR 库之间发生冲突,系统会使用依赖项列表中首先列出的库(靠近
dependencies
块顶部)中的资源。为了避免常用的资源 ID 发生资源冲突,请考虑使用对模块具有唯一性(或在所有项目模块之间具有唯一性)的前缀或其他一致的命名方案。
-
在多模块构建中,系统会将 JAR 依赖项视为传递依赖项
在向输出 AAR 的库项目添加 JAR 依赖项时,JAR 会由库模块进行处理,并与其 AAR 打包在一起。
不过,如果您的项目包含库模块,并且此模块已被应用模块使用,应用模块便会将库的本地 JAR 依赖项视为传递依赖项。在这种情况下,本地 JAR 将由使用它的应用模块进行处理,而不是由库模块进行处理。这是为了加快库代码更改导致的增量构建的速度。
由本地 JAR 依赖项导致的所有 Java 资源冲突都必须在使用相应库的应用模块中解决。
-
库模块可以依赖于外部 JAR 库
您可以开发一个依赖于外部库(例如 Google 地图外部库)的库模块。在这种情况下,相关应用必须针对包含此外部库的目标(例如 Google API 插件)进行构建。另外也要注意,库模块和相关应用都必须在其清单文件的
<uses-library>
元素中声明外部库。 -
应用模块的
minSdkVersion
必须等于或大于库定义的版本库是作为相关应用模块的一部分进行编译的,因此,库模块中使用的 API 必须与应用模块支持的平台版本兼容。
-
每个库模块都会创建自己的 R 类
在您构建相关应用模块时,库模块会先编译到 AAR 文件中,然后再添加到应用模块中。因此,每个库都有自己的
R
类,并根据库的软件包名称命名。所需的所有软件包中都会创建从主模块和库模块生成的R
类,包括主模块的软件包和库的软件包。 -
库模块可以包含自己的 ProGuard 配置文件
如果有用于构建和发布 AAR 的库项目,您可以向库的构建配置添加 ProGuard 配置文件,并且 Android Gradle 插件规则适用于您指定的 ProGuard 规则。构建工具会将此文件嵌入到为库模块生成的 AAR 文件中。在您将库添加到应用模块后,库的 ProGuard 文件会附加到应用模块的 ProGuard 配置文件 (
proguard.txt
)。通过将 ProGuard 文件嵌入到库模块中,您可以确保依赖于相应库的应用模块不必手动更新其 ProGuard 文件即可使用此库。当 Android Studio 构建系统构建您的应用时,它会同时使用来自应用模块和库的指令。因此无需按照单独的步骤在库上运行代码缩减器。
如需向库项目添加 ProGuard 规则,您必须使用
consumerProguardFiles
属性(位于库的build.gradle
文件的defaultConfig
块内)指定文件名称。例如,以下代码段会将lib-proguard-rules.txt
设为库的 ProGuard 配置文件:不过,如果库模块是要编译到 APK 中的多模块构建的一部分,并且不会生成 AAR,您应该只在使用相应库的应用模块上运行代码缩减。如需详细了解 ProGuard 规则及其用法,请参阅缩减、混淆处理和优化应用。
-
测试库模块的方法与测试应用的方法相同
主要区别在于,库及其依赖项会作为测试 APK 的依赖项自动包含在内。这意味着测试 APK 不仅包含自己的代码,还包含库的 AAR 及其所有依赖项。由于没有单独的“被测应用”,因此
androidTest
任务只会安装(和卸载)测试 APK。在合并多个清单文件时,Gradle 会遵循默认的优先级顺序,并将库的清单合并到测试 APK 的主清单中。
AAR 文件详解
AAR 文件的文件扩展名为 .aar
,Maven 工件类型应该也是 aar
。此文件本身是一个 zip 文件。唯一的必需条目是 /AndroidManifest.xml
。
此外,AAR 文件可能包含以下一个或多个可选条目:
/classes.jar
/res/
/R.txt
/public.txt
/assets/
/libs/name.jar
-
/jni//abi_name/name.so
(其中abi_name是 Android 支持的 ABI 之一) /proguard.txt
/lint.jar
/api.jar
-
/prefab/
(用于导出原生库)
开始
在查阅了大量资料,并思考后,我得出了apk可以转换成aar的结论.并手动通过apk生成了一个aar然后导入到工程里面,并成功运行后,开始着手编码的过程。
首先,随便搞个apk和aar然后进行对比(同一个工程的同一个model)
大部分文件都认识,除了R.txt ,于是双击
这看起来有点类似public.xml的文件,那就根据public.xml来生成R.txt吧,然后用Sublime来用public.xml生成R.txt,然后悲催的发现行数对不上,对比发现少了styleable (之前修正ApkIdTool填坑也是这个老朋友)
事已至此,不能简单的由public.xml生成R.txt,所以我不由得开始琢磨起这R文件是用来干嘛的了。
官方关于aar的说明
官方文档关于aar上明确说明aar文件本身是一个 zip 文件。唯一的必需条目是 /AndroidManifest.xml。
那就直接删掉aar的R.txt然后直接运行项目,结果就是程序崩毁日志显示找不到对应的R文件,那么这个R文件应该是用来生成R.java的吧
画重点
在查阅了大量资料与实践之后得出R.txt 是决定在aar的包名下生成R文件的内容, 所以有无R.txt将决定是否在aar文件的AndroidMainFest.xml的packageName下的路径下创建R.java
。
那么?!嗯?
可能有的小伙伴已经想到了我要干嘛了。apk文件本身就包含了常量化的资源id,如果我按照as生成的aar的方式生成aar,那我就需要把常量化的资源id改成动态引用包名下的R文件的对应id,并生成R.txt。然后我还需要把用来固定资源id的apk解包后生成的public.xml文件给删除掉。单单就对资源id由final改成动态引用,我就要做这么多操作!
既然apk已经分配了id而且也有了固定资源id的public.xml,那么我能不能直接就用这些常量化的id呢?于是,我又手动合成了一个aar,运行没有任何问题。
其实理论上也是不存在任何问题的,因为最后都是调用aapt2来生成资源id,当项目引用aar的时候,res里面包含public.xml,那也就是提前固定了对应的资源id而已,当不存在public.xml的资源也就不存在固定,也就重新生成资源id。并没有任何问题。(在写游戏sdk方面,一般都不使用R.id的方式来获取资源id。很大一部分原因就是为了规避资源id重新分配,导致的id错乱问题。关于为什么使用动态方式获取id就不会有这个问题,可以参考抖音的分享,文中称为资源文件反射
我更愿意称动态获取资源id
)
有了上述的理论支持,那么由apk2aar可以简化成如下图所示
1.关于AndroiManifest.xml的修改,可以参考合并aar,主要按照as生成 aar的AndroidManifest.xml的形式去修改
2.关于unknown文件,我之所以这么做是基于引入了okhttp后,apk解包后会在unknown下有publicsuffixes.gz,而我这么做也是根据引用okhttp后生成的aar来的(用fat-aar,为我节省了思考的时间,感谢)
3.用dex2jar提取jar,并删除需要替换的类(因为需要更新这部分代码),资源不用做处理,因为如果多个 AAR 库之间发生冲突,系统会使用依赖项列表中首先列出的库(靠近 dependencies 块顶部)中的资源。
所以我又写了代码,整合到了tool里面,使用请参考。因为代码还很粗糙,按照最开始的设想转aar只需运行一次所以并没有过多考虑性能。并且由于使用aar用as出包并没有速度上的优势,已经失去了继续的动力(按照最开始的想法,因为转aar一次性的耗费的时间先忽略,那么打包因为没了apktool的解包回编,理论上只有编译apk这步,速度应该明显提升。但是事与愿违,实测并没有优势),所以,也就没必要优化,而且由于,缺少大量样本,可能存在,我认知外的情况。
希望能起到抛砖引玉的作用,共勉。
ps:
补充说明:AS的aar之所以不用固定id是因为多个aar的话可能有冲突,我目前的使用因为我能确保只有一个aar是用固定id的所以不会出现问题