1. 原因
写这篇文章的原因是因为发现网上对这个库的分析文章并不多,而且高质量的更少。所以就自己来记录一下自己的源码阅读收获。
2. 写在前面
- 本篇文章涉及的AndResGuard源码版本为
1.2.17
- 本篇文章不涉及到
resources.arsc
文件解析的细节问题,如果需要了解可以参考rsources.arsc格式详解(2020年)这篇文章 - AndResGuard是对
resources.arsc
文件进行混淆的,所以说需要对resources.arsc
的格式进行了解,如果不了解可参考rsources.arsc格式详解(2020年) - AndResGuard详细阅读笔记
3. 工程结构
自上至下每个文件夹的功能是:
- AndResGuard-cli: 是命令行的入口,依赖了AndResGuard-core。里面只有一个类 就是CliMain,里面包含入口main方法
- AndResGuard-core: 是 整个项目的核心,AndResGuard-gradle-plugin 依赖了 AndResGuard-core
- AndResGuard-example:不用多说,肯定是存放演示代码的地方。
- AndResGuard-gradle-plugin: 是gradla插件入口,依赖了AndResGuard-core。 只做了Task的创建和配置信息收集两件事
- SeventZip: 存放zip压缩工具,我们不用关系
- doc: 存放使用文档和白名单的地方
- tool_output: 存放的比较杂,和源码分析也没关系,就不说了
上面有提到 AndResGuard-cli只是命令行的入口,核心库是 AndResGuard-core,AndResGuard-gradle-plugin是gradle插件,负责收集和整合基础信息的。因为 AndResGuard-cli 做的事情比较简单,所示我们只对 AndResGuard-gradle-plugin
和 AndResGuard-core
进行分析。
4. AndResGuard-gradle-plugin
我们都知道gradle插件中都会有一个Plugin
用来注册Task
,那么AndResGuard-gradle-plugin
中的Plugin
都做了哪些工作,我们一起来看看。
4.1 AndResGuardPlugin
class AndResGuardPlugin implements Plugin<Project> {
public static final String USE_APK_TASK_NAME = "UseApk"
@Override
void apply(Project project) {
//添加 osdetector 插件 用来查找 7zip 库
project.apply plugin: 'com.google.osdetector'
//添加 andResGuard 扩展
project.extensions.create('andResGuard', AndResGuardExtension)
//添加 sevenzip 扩展
project.extensions.add("sevenzip", new ExecutorExtension("sevenzip"))
project.afterEvaluate {
def android = project.extensions.android
//直接注册 resguardUseApk
createTask(project, USE_APK_TASK_NAME)
....
android.buildTypes.all { buildType ->
def buildTypeName = buildType.name.capitalize()
createTask(project, buildTypeName)
}
....
//查找7zip依赖 并依赖给项目
project.extensions.findByName("sevenzip").loadArtifact(project)
}
}
private static void createTask(Project project, variantName) {
def taskName = "resguard${variantName}"
if (project.tasks.findByPath(taskName) == null) {
def task = project.task(taskName, type: AndResGuardTask)
if (variantName != USE_APK_TASK_NAME) {
//依赖 assemble... Task
task.dependsOn "assemble${variantName}"
}
}
}
}
其实 中的代码是蛮简单的 , 一共做了 3件事。
- 添加
andResGuard
和sevenzip
的扩展,使我们再 .gradle 文件中可以使用
//添加 andResGuard 扩展
project.extensions.create('andResGuard', AndResGuardExtension)
//添加 sevenzip 扩展
project.extensions.add("sevenzip", new ExecutorExtension("sevenzip"))
- 给项目依赖 7zip 压缩库。具体查询和依赖的代码是在
ExecutorExtension.groovy
中的loadArtifact()
方法,只是在AndResGuardPlugin
的apply()
方法中有调用。
void loadArtifact(Project project) {
...
def groupId, artifactId, version
(groupId, artifactId, version) = this.artifact.split(":")
def notation = [group : groupId,
name : artifactId,
version : version,
classifier: project.osdetector.classifier,
ext : 'exe']
project.logger.info("[AndResGuard]Resolving artifact: ${notation}")
//依赖 7zip 库
Dependency dep = project.dependencies.add(config.name, notation)
...
}
- 创建Task,这也是最重要的一步,通过如下代码,最终创建了名字为
resguard...
,类型为AndResGuardTask
的Task,例如resguardDebug,resguardRelease
private static void createTask(Project project, variantName) {
def taskName = "resguard${variantName}"
if (project.tasks.findByPath(taskName) == null) {
def task = project.task(taskName, type: AndResGuardTask)
if (variantName != USE_APK_TASK_NAME) {
//依赖 assemble... Task
task.dependsOn "assemble${variantName}"
}
}
}
4.2 AndResGuardTask
AndResGuardTask
的工作其实挺简单的就是将 .gradle中配置的和 Gradle 能获取到的东西都传递给 AndResGuard-core
,不过有一点需要注意就是如下代码对 whiteList
中配置的资源文件补全了包名。
//对资源文件补全路径
configuration.whiteList.each { res ->
if (res.startsWith("R")) {
whiteListFullName.add(packageName + "." + res)
} else {
whiteListFullName.add(res)
}
}
最后就调用 Main.gradleRun(inputParam)
这段代码开始执行 AndResGuard-core
中的程序了。
5. AndResGuard-core
5.1 Main.run()
进入到 AndResGuard-core
以后最先被调用的方法是 run()
这个方法的 主要工作有三个
- 是将 传入的
InputParam
转换成AndResGuard-core
中使用的Configuration
- 调用
resourceProguard()
方法 - 调用
clean()
释放内存
5.2 Main.resourceProguard()
这个方法有两个工作
- 调用
decodeResource()
其实是调用ApkDecoder.decode()
进行解析,压缩,混淆 资源文件 - 调用
buildApk()
对压缩混淆后的apk进行签名,默认是V1签名。
5.3 ApkDecoder.decode()
这个方式其实算是个核心方法了,资源的压缩,resources.arsc
文件的混淆和重新写入都是在这个方法中生成的。我们一步一步来看一下 都做了哪些事情。
public void decode() {
//apk中是否包含resources.arsc 文件
if (hasResources()) {
//创建后续需要使用的文件及文件夹,以及修改压缩配置
ensureFilePath();
System.out.printf("decoding resources.arsc\n");
//解析arsc文件 将 typeID 和 具体内容 存放在 mExistTypeNames 这个map中
RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"));
//解析arsc文件 并输出 ResPackage 将混淆后的名字和压缩方式 放入mCompressData中。并且将 字符串偏移量和 混淆后的完整路径 保存到 ARSCDecoder.mTableStringsResguard 中
ResPackage[] pkgs = ARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"), this);
//把没有纪录在resources.arsc的资源文件也拷进dest目录
copyOtherResFiles();
//将混淆写入 到 resources_temp (outDir 下) 中
ARSCDecoder.write(apkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
}
}
5.5 ApkDecoder.ensureFilePath()
这个方法中大致就做了两件事
- 创建后面会用到的文件或者文件夹
- 调用
dealWithCompressConfig()
方法 对.gradle
文件中配置的compressFilePattern
中包含的文件类型文件的压缩方式进行更改,后面再重新打apk的时候就会进行压缩,已达到apk瘦身的目的。
5.6 RawARSCDecoder.decode()
该方法的工作很简单,就是解析rsources.arsc
文件然后将 资源的typeId
和资源名字
存放到 mExistTypeNames
这个静态成员变量中备用。
// 用于存放 typeID 和 具体内容 的map 例:{1, [abc_fade_in] } , 1=anim
private static HashMap<Integer, Set<String>> mExistTypeNames;
5.7 ARSCDecoder.decode()
这个方法的作用就是生成混淆后的 路径和文件名称,分为下面几步。
5.7.1 proguardFileName()
调用proguardFileName()
生成混淆后的资源路径 并保存到 mOldFileName
这个map中备用
if (config.mUseKeepMapping) {
....
//遍历文件 如果文件包含在 fileMapping 中,那么直接使用fileMapping中的配置,如果不包含则直接 获取一个混淆名称 保存到 mOldFileName中
for (File resFile : resFiles) {
String raw = "res" + "/" + resFile.getName();
if (fileMapping.containsKey(raw)) {
mOldFileName.put(raw, fileMapping.get(raw));
} else {
mOldFileName.put(raw, resRoot + "/" + mResguardBuilder.getReplaceString());
}
}
} else {
for (int i = 0; i < resFiles.length; i++) {
// 这里也要用linux的分隔符,如果普通的话,就是r。这里替换的是 res 下文件夹的 名称
mOldFileName.put("res" + "/" + resFiles[i].getName(), TypedValue.RES_FILE_PATH + "/" + mResguardBuilder.getReplaceString());
}
}
//保存的是 res下文件全路径 和 混淆后名称的关系
private final Map<String, String> mOldFileName;
5.7.2 readValue()
这个方法的作用有两个
- 将混淆后的完整资源路径 存放到
ApkDecoder.mCompressData
中备用
....
//这里用的是linux的分隔符
HashMap<String, Integer> compressData = mApkDecoder.getCompressData();
if (compressData.containsKey(raw)) {
//这里将 混淆后的名称 和压缩方式 也放入 compressData中
compressData.put(result, compressData.get(raw));
}
....
//保存 文件完整路径(包含混淆路径和非混淆路径)和 压缩方式的 对应关系
private HashMap<String, Integer> mCompressData;
- 将 资源的
资源项目名称index
和 混淆后的完整资源路径 存放到mTableStringsResguard
这个静态变量中备用
if (!resRawFile.exists()) {
Utils.logARSC("can not find res file, you delete it? path: resFile=%s", resRawFile.getAbsolutePath());
} else {
if (!mergeDuplicatedRes && resDestFile.exists()) {
throw new AndrolibException(String.format("res dest file is already found: destFile=%s",
resDestFile.getAbsolutePath()
));
}
if (filterInfo == null) {
//将没有混淆的文件内容 copy 到混淆的文件中
FileOperation.copyFileUsingStream(resRawFile, resDestFile);
Utils.logARSC("resRawFile= %s \n resDestFile= %s",resRawFile,resDestFile);
}
//already copied
mApkDecoder.removeCopiedResFile(resRawFile.toPath());
//放入 mTableStringsResguard 中
mTableStringsResguard.put(data, result);
}
//存放 资源项目名称index((资源项目名称index (下标)) 通过这个再加上 全局资源项目池 就可以拿到对应 文件的全路径 如res/anim/abc_slide_in_bottom.xml 可能为无效值) 和混淆后字符串
public static Map<Integer, String> mTableStringsResguard = new LinkedHashMap<>();
5.7.3 initResGuardBuild()
还记得 5.6
中提到的 mExistTypeNames
吗?它被使用在了 initResGuardBuild()
中,防止 混淆后出现 重复的资源名称
private void initResGuardBuild(int resTypeId) {
// we need remove string from resguard candidate list if it exists in white list
HashSet<Pattern> whiteListPatterns = getWhiteList(mType.getName());
// init resguard builder (防止 mResguardBuilder 中包含白名单内容)
mResguardBuilder.reset(whiteListPatterns);
//避免 混淆后有重复的 String,所以要剔除 重复的名字
mResguardBuilder.removeStrings(RawARSCDecoder.getExistTypeSpecNameStrings(resTypeId));
// 如果是保持mapping的话,需要去掉某部分已经用过的mapping
reduceFromOldMappingFile();
}
5.8 copyOtherResFiles
这一步是 把没有纪录在resources.arsc的资源文件也拷进dest目录,保证项目的完整性
private void copyOtherResFiles() throws Exception {
if (mRawResourceFiles.size() > 0) {//说明还有文件
//获取 原资源文件对象(temp下的res) 的Path
Path resPath = mRawResFile.toPath();
//获取混淆后的res/r文件Path
Path destPath = mGuardResDir.toPath();
for (Path path : mRawResourceFiles) {
Path relativePath = resPath.relativize(path);//使 path 相对于 resPath 相对化
Path dest = destPath.resolve(relativePath);//使 relativePath 相对于 destPath绝对化
System.out.printf("copy res file not in resources.arsc file:%s\n", relativePath.toString());
FileOperation.copyFileUsingStream(path.toFile(), dest.toFile());
}
}
}
5.9 ARSCDecoder.write()
这一步的目的是将 上面生成的混淆后的 路径和名称。写入一个新的 resources.arsc
文件中。也分了很多步。这里只记录里面比较重要的几步,这里因为代码不多长就不粘贴了,有需要可以去 AndResGuard详细阅读笔记 里面查看
5.9.1 StringBlock.writeTableNameStringBlock()
作用是 将混淆好的 文件全路径 写入 全局字符串池中。
5.9.2 StringBlock.writeSpecNameStringBlock()
作用是 将混淆好的路径信息 写入 资源项名称字符串池
5.9.3 StringBlock.reWriteTable()
作用是 将 混淆后的各块大小 写入arsc文件中备用,到这里整个混淆过程就完成了,后面就会调用 5.3
中提到的 buildApk()
生成混淆后的 apk。
6. 总结
6.1 混淆流程调用链
Main.run()--> Mian.resourceProguard()--> Main.decodeResource()--> ApkDecoder.decode()--> ApkDecoder.ensureFilePath()--> RawARSCDecoder.decode()--> RawARSCDecoder.readTable()--> ARSCDecoder.decode--> ARSCDecoder.readTable()--> ARSCDecoder.write()--> ARSCDecoder.writeTable()--> Main.buildApk()--> ResourceApkBuilder.buildApkWithV2sign--> ResourceApkBuilder.addNonSignatureFiles--> ResourceApkBuilder.use7zApk--> ResourceApkBuilder.alignApk--> Main.clean()->
6.2 混淆流程重要步骤 含义
RawARSCDecoder.readTablePackage 中解析 资源项名称字符串池(用于存储 资源名称,比如layout的名字,string的名字)并赋值给 mSpecNames
RawARSCDecoder.readSingleTableTypeSpec 中 解析 TableTypeSpec 并将 type, mResId 和 typeName 和 packageName进行记录
RawARSCDecoder.readEntry 中解析出资源项名称index(specNamesId)然后调用 putTypeSpecNameStrings 通过 type , mSpecNames 和 specNamesId 找到具体的 String 并保存在 mExistTypeNames中 备用
ARSCDecoder.proguardFileName()方法中 对路径进行混淆并保存在 mOldFileName 中。比如 res/anim 混淆为 r/a
ARSCDecoder.initResGuardBuild()方法中过滤掉步骤3生成的 mExistTypeNames存在的名称
ARSCDecoder.dealWithNonWhiteList()方法中 混淆文件名称 并保存到 ResPackage 中的 mSpecNamesReplace map中
ARSCDecoder.readValue()方法中 对 4,5 步生成的 混淆路径和混淆名称进行拼接 并保存到 ApkDecoder的 compressData Map 中。并且将 字符串偏移量和 混淆后的完整路径 保存到 mTableStringsResguard 中 备用
StringBlock.writeTableNameStringBlock()将混淆好的 文件全路径 写入 全局字符串池中
StringBlock.writeSpecNameStringBlock()将混淆好的路径信息 写入 资源项名称字符串池
ARSCDecoder.reWriteTable() 将 混淆后的各块大小 写入arsc文件中备用
7. 阅读中遇到的问题及解答
- 入口在哪里
答: 有两种入口一个是 gradlePlugin一个是命令行。不多真正程序还是执行的入口是 core中的Main - resguardUseApk Task 是干嘛的?
答:直接对apk进行资源压缩的task - resguard...task 是如何保证在打包后运行的(隐士依赖?)
答:不是的是在AndResGuardPlugin的createTask中进行的依赖
private static void createTask(Project project, variantName) {
def taskName = "resguard${variantName}"
if (project.tasks.findByPath(taskName) == null) {
def task = project.task(taskName, type: AndResGuardTask)
if (variantName != USE_APK_TASK_NAME) {
task.dependsOn "assemble${variantName}"
}
}
}
- 怎么压缩图片的?
答:更改图片文件压缩格式为压缩后,使用7zip进行压缩的 - StringBlock 中
int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset;
这段代码 求出的 size是什么?
答:获取字符串池所占总体占用字节数 - ResTypePackage 中 这段代码是干啥的?看起来像是某种特殊情况的判断
// TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e
// which is only in split/new applications.
int splitHeaderSize = (2 + 2 + 4 + 4 + (2 * 128) + (4 * 5)); // short, short, int, int, char[128], int * 4
if (mHeader.headerSize == splitHeaderSize) {
mTypeIdOffset = mIn.readInt();
System.out.printf("mTypeIdOffset= %s\n", mTypeIdOffset);
}
答:如果PackageHeader的大小 等于 splitHeaderSize 这就说明PackageHeader中包含 mTypeIdOffset。 如果包含的话 我们在解析 RES_TABLE_LIBRARY_TYPE 的typeId时要得到真正的 typeId 就要减去 typeIdOffset
RawARSCDecoder.Header.TYPE_LIBRARY(在ResourceTypes.h中的RES_TABLE_LIBRARY_TYPE) 是用来做什么的?虽然在ResourceTypes.h中也看到了定义但是不知道是做什么的。
答:==resources.arsc中会通过 RES_TABLE_LIBRARY_TYPE 中会记录自己的依赖的库==ApkDecoder.decode()中
RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"));
这行代码是在干什么?感觉没有用啊
答:这次解析 产生的 mExistTypeNames 是为了防止在生成混淆字符串时 和 文件名产生重复。ARSCDecoder.initResGuardBuild()中
mResguardBuilder.reset(whiteListPatterns);
这段代码 是在干什么
答: 重置混淆字符串生成器,因为 文件名和路径名可以重复,所以可以重置ARSCDecoder.readValue()方法中的
HashMap<String, Integer> compressData = mApkDecoder.getCompressData();
if (compressData.containsKey(raw)) {
//就是在这里替换了 混淆后的文件名!!!!
compressData.put(result, compressData.get(raw));
} else {
System.err.printf("can not find the compress dataresFile=%s\n", raw);
}
这段代码是不是 替换了 混淆后的文件名?那么是怎么生效的呢?
答:不止的 这里只是将 混淆后的路径名和 压缩方式 也存入compressData中。 混淆生效是在 ARSCDecoder.writeTable()完成的
该项目是如何遭到混淆的?
答: 一次解析 获取 所有文件名和type的对应关系-》二次解析(得到混淆后的名称)-》三次解析及写入新arscResPackage中的mSpecNamesBlock Map 是用来记录什么的?
答:记录 arsc name列的名称和 混淆后文件名 对应关系
8. 注意事项
- 使用glide直接加载资源图片,最终也是去获取 未混淆前的资源名称,所以 如果要使用这种方式需要要将 该图片加入到 白名单中。如下是 glide源码。
ResourceLoader.class
@Override
public DataFetcher<T> getResourceFetcher(Integer model, int width, int height) {
Uri uri = null;
try {
uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+ resources.getResourcePackageName(model) + '/'
+ resources.getResourceTypeName(model) + '/'
+ resources.getResourceEntryName(model));
} catch (Resources.NotFoundException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Received invalid resource id: " + model, e);
}
}
if (uri != null) {
return uriLoader.getResourceFetcher(uri, width, height);
} else {
return null;
}
}
- ShareSDK 中 某些资源也是通过
getIdentifier
方法获取的 具体是在com.mob.tools.utils.ResHelper
类中,所以我们也需要将 ShareSDK涉及到的资源加入白名单,配置如下
whiteList = [
...
//shareSDK
"R.id.ssdk*",
"R.string.mobcommon*",
"R.string.ssdk*",
"R.string.mobdemo*",
"R.drawable.mobcommon*",
"R.drawable.ssdk*",
"R.layout.mob*",
"R.style.mobcommon*",
]
如果你发现 在使用 AndResGuard后 导致 微信等分享调不起来,则你需要添加 如果配置到白名单中