背景
抖音包体积优化提出的“键常量池裁剪”是基于腾讯的AndResGuard资源混淆之后做的进一步处理,也就是对resources.arsc文件的处理。而资源混淆,就是对resources.arsc文件进行修改。那么我们可以尝试基于这个思路,对AndResGuard插件源码进行解析,获悉他对resources.arsc文件的处理详情。
入口
下载AndResGuard源码后,项目结构如上。作为一个gradle插件,打开他的入口AndResGuardPlugin,首先会执行的是apply方法
class AndResGuardPlugin implements Plugin<Project> {
public static final String USE_APK_TASK_NAME = "UseApk"
@Override
void apply(Project project) {
...
project.afterEvaluate {
...
createTask(project, USE_APK_TASK_NAME)
...
}
}
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}"
}
}
}
}
此处创建了AndResGuardTask,并在打包后执行
assemble:打包命令,具体详情可以参考 https://www.jianshu.com/p/db62617cbbff
那么接下来解析AndResGuardTask类,他是一个task,那么直接看run方法
run() {
...
buildConfigs.each { config ->
...
RunGradleTask(config, config.file.getAbsolutePath(), config.minSDKVersion, config.targetSDKVersion)
...
}
}
此处执行了RunGradleTask函数
def RunGradleTask(config, String absPath, int minSDKVersion, int targetSDKVersion) {
...
configuration.whiteList.each { res ->
if (res.startsWith("R")) {
whiteListFullName.add(packageName + "." + res)
} else {
whiteListFullName.add(res)
}
}
InputParam.Builder builder = new InputParam.Builder()
.setMappingFile(configuration.mappingFile)
.setWhiteList(whiteListFullName)
.setUse7zip(configuration.use7zip)
.setMetaName(configuration.metaName)
.setFixedResName(configuration.fixedResName)
.setKeepRoot(configuration.keepRoot)
.setMergeDuplicatedRes(configuration.mergeDuplicatedRes)
.setCompressFilePattern(configuration.compressFilePattern)
.setZipAlign(getZipAlignPath())
.setSevenZipPath(sevenzip.path)
.setOutBuilder(useFolder(config.file))
.setApkPath(absPath)
.setUseSign(configuration.useSign)
.setDigestAlg(configuration.digestalg)
.setMinSDKVersion(minSDKVersion)
.setTargetSDKVersion(targetSDKVersion)
if (configuration.finalApkBackupPath != null && configuration.finalApkBackupPath.length() > 0) {
builder.setFinalApkBackupPath(configuration.finalApkBackupPath)
} else {
builder.setFinalApkBackupPath(absPath)
}
if (configuration.useSign) {
if (signConfig == null) {
throw new GradleException("can't the get signConfig for release build")
}
builder.setSignFile(signConfig.storeFile)
.setKeypass(signConfig.keyPassword)
.setStorealias(signConfig.keyAlias)
.setStorepass(signConfig.storePassword)
if (signConfig.hasProperty('v3SigningEnabled') && signConfig.v3SigningEnabled) {
builder.setSignatureType(InputParam.SignatureType.SchemaV3)
} else if (signConfig.hasProperty('v2SigningEnabled') && signConfig.v2SigningEnabled) {
builder.setSignatureType(InputParam.SignatureType.SchemaV2)
}
}
InputParam inputParam = builder.create()
Main.gradleRun(inputParam)
}
这里主要做了两件事
- 获取构建参数,也就是在使用gradle时,注册的那些参数
- 调用Main.gradleRun,这是一个java函数,也就是说具体的逻辑最终都是用java实现的
实现
进入Main类,解析他的gradleRun函数
public class Main {
...
private void run(InputParam inputParam) {
...
resourceProguard(
new File(inputParam.outFolder),
finalApkFile,
inputParam.apkPath,
inputParam.signatureType,
inputParam.minSDKVersion
);
...
}
protected void resourceProguard(
File outputDir, File outputFile, String apkFilePath, InputParam.SignatureType signatureType, int minSDKVersoin) {
...
try {
ApkDecoder decoder = new ApkDecoder(config, apkFile);
/* 默认使用V1签名 */
decodeResource(outputDir, decoder, apkFile);
buildApk(decoder, apkFile, outputFile, signatureType, minSDKVersoin);
} catch (Exception e) {
e.printStackTrace();
goToError();
}
}
private void decodeResource(File outputFile, ApkDecoder decoder, File apkFile)
throws AndrolibException, IOException, DirectoryException {
if (outputFile == null) {
mOutDir = new File(mRunningLocation, apkFile.getName().substring(0, apkFile.getName().indexOf(".apk")));
} else {
mOutDir = outputFile;
}
decoder.setOutDir(mOutDir.getAbsoluteFile());
decoder.decode();
}
private void buildApk(
ApkDecoder decoder, File apkFile, File outputFile, InputParam.SignatureType signatureType, int minSDKVersion)
throws Exception {
...
}
}
protected void goToError() {
System.exit(ERRNO_USAGE);
}
}
跟着方法读取gradleRun==>run==>resourceProguard==>decodeResource & buildApk
此处做了两件事
- 解析APK资源,并对文件进行修改 - decodeResource
- 重新构建APK - buildApk
解析修改APK资源
读到decodeResource方法,可知他是交给了ApkDecoder#decode方法,代码如下
public void decode() throws AndrolibException, IOException, DirectoryException {
if (hasResources()) {
ensureFilePath();
// read the resources.arsc checking for STORED vs DEFLATE compression
// this will determine whether we compress on rebuild or not.
System.out.printf("decoding resources.arsc\n");
RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"));
ResPackage[] pkgs = ARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"), this);
//把没有纪录在resources.arsc的资源文件也拷进dest目录
copyOtherResFiles();
ARSCDecoder.write(apkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
}
}
此处总共执行了四件事
- 确定文件路径,解压APK - ensureFilePath
- 第一次解析resources.arsc文件,读取资源文件信息并保存 - RawARSCDecoder.decode
- 第二次解析resources.arsc文件,进行混淆 - ARSCDecoder.decode
- 重新生成resources.arsc - ARSCDecoder.write
确定文件信息
跟踪 ensureFilePath 可得
private void ensureFilePath() throws IOException {
Utils.cleanDir(mOutDir);
String unZipDest = new File(mOutDir, TypedValue.UNZIP_FILE_PATH).getAbsolutePath();
System.out.printf("unziping apk to %s\n", unZipDest);
mCompressData = FileOperation.unZipAPk(apkFile.getAbsoluteFile().getAbsolutePath(), unZipDest);
dealWithCompressConfig();
//将res混淆成r
if (!config.mKeepRoot) {
mOutResFile = new File(mOutDir.getAbsolutePath() + File.separator + TypedValue.RES_FILE_PATH);
} else {
mOutResFile = new File(mOutDir.getAbsolutePath() + File.separator + "res");
}
//这个需要混淆各个文件夹
mRawResFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath()
+ File.separator
+ TypedValue.UNZIP_FILE_PATH
+ File.separator
+ "res");
mOutTempDir = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.UNZIP_FILE_PATH);
//这里纪录原始res目录的文件
Files.walkFileTree(mRawResFile.toPath(), new ResourceFilesVisitor());
if (!mRawResFile.exists() || !mRawResFile.isDirectory()) {
throw new IOException("can not found res dir in the apk or it is not a dir");
}
mOutTempARSCFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + "resources_temp.arsc");
mOutARSCFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + "resources.arsc");
String basename = apkFile.getName().substring(0, apkFile.getName().indexOf(".apk"));
mResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath()
+ File.separator
+ TypedValue.RES_MAPPING_FILE
+ basename
+ TypedValue.TXT_FILE);
mMergeDuplicatedResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath()
+ File.separator
+ TypedValue.MERGE_DUPLICATED_RES_MAPPING_FILE
+ basename
+ TypedValue.TXT_FILE);
}
此处总共执行了这几件事
- 解压APK - FileOperation.unZipAPk
- 压缩APK资源 - dealWithCompressConfig
APK中很多资源是以stored方式存储的,这些资源都是没被压缩的,通过修改他的压缩方式达到压缩的目的
在使用AndResGuard时通过compressFilePattern参数配置
未被压缩的资源包含如下
static const char* kNoCompressExt[] = { ".jpg", ".jpeg", ".png", ".gif", ".wav", ".mp2", ".mp3", ".ogg", ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv" }
- 将res混淆成r
- 纪录原始res目录的文件
- 创建新的resources.arsc输出文件和mapping文件
第一次解析文件,存储原资源信息
RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"))
可见此处解析的是apk的resources.arsc文件,进入该方法
public static ResPackage[] decode(InputStream arscStream) throws AndrolibException {
try {
RawARSCDecoder decoder = new RawARSCDecoder(arscStream);
System.out.printf("parse to get the exist names in the resouces.arsc first\n");
return decoder.readTable();
} catch (IOException ex) {
throw new AndrolibException("Could not decode arsc file", ex);
}
}
继续看decoder.readTable()方法
private ResPackage[] readTable() throws IOException, AndrolibException {
nextChunkCheckType(Header.TYPE_TABLE);
int packageCount = mIn.readInt();
StringBlock.read(mIn);
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
packages[i] = readTablePackage();
}
return packages;
}
读到readTablePackage方法
private ResPackage readTablePackage() throws IOException, AndrolibException {
...
while (mHeader.type == Header.TYPE_LIBRARY) {
readLibraryType();
}
while (mHeader.type == Header.TYPE_SPEC_TYPE) {
readTableTypeSpec();
}
...
}
看到此处有readLibraryType和readTableTypeSpec方法,我们先看readLibraryType方法
private void readLibraryType() throws AndrolibException, IOException {
...
while (mHeader.type == Header.TYPE_TYPE) {
readTableTypeSpec();
}
}
此处调用了readTableTypeSpec方法,所以直接读readTableTypeSpec方法即可
private void readTableTypeSpec() throws AndrolibException, IOException {
...
while (mHeader.type == Header.TYPE_TYPE) {
readConfig();
nextChunk();
}
}
继续readConfig方法分析
private void readConfig() throws IOException, AndrolibException {
...
int[] entryOffsets = mIn.readIntArray(entryCount);
for (int i = 0; i < entryOffsets.length; i++) {
if (entryOffsets[i] != -1) {
mResId = (mResId & 0xffff0000) | i;
readEntry();
}
}
}
readEntry
*/
private void readEntry() throws IOException, AndrolibException {
/* size */
mIn.skipBytes(2);
short flags = mIn.readShort();
int specNamesId = mIn.readInt();
putTypeSpecNameStrings(mCurTypeID, mSpecNames.getString(specNamesId));
boolean readDirect = false;
if ((flags & ENTRY_FLAG_COMPLEX) == 0) {
readDirect = true;
readValue(readDirect, specNamesId);
} else {
readDirect = false;
readComplexEntry(readDirect, specNamesId);
}
}
private void putTypeSpecNameStrings(int type, String name) {
Set<String> names = mExistTypeNames.get(type);
if (names == null) {
names = new HashSet<>();
}
names.add(name);
mExistTypeNames.put(type, names);
}
- 将资源类型的名称存在mExistTypeNames里,key为资源类型,value为名称集合 - putTypeSpecNameStrings
- 避免混淆后的名称与混淆前的名称出现相同的情况
- 需要防止由于某些非常恶心的白名单,导致出现重复id
第二次解析,进行混淆处理
ResPackage[] pkgs = ARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"), this);
分析这个方法,进入ARSCDecoder.decode
public static ResPackage[] decode(InputStream arscStream, ApkDecoder apkDecoder) throws AndrolibException {
try {
ARSCDecoder decoder = new ARSCDecoder(arscStream, apkDecoder);
ResPackage[] pkgs = decoder.readTable();
return pkgs;
} catch (IOException ex) {
throw new AndrolibException("Could not decode arsc file", ex);
}
}
private ResPackage[] readTable() throws IOException, AndrolibException {
nextChunkCheckType(Header.TYPE_TABLE);
int packageCount = mIn.readInt();
mTableStrings = StringBlock.read(mIn);
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
packages[i] = readPackage();
}
mMappingWriter.close();
...
mMergeDuplicatedResMappingWriter.close();
...
return packages;
}
- 这里实际上是读取resource.arsc的package部分
- mMappingWriter - mapping文件写入类
- mMergeDuplicatedResMappingWriter - 合并旧mapping文件写入类
主要的逻辑在readPackage
private ResPackage readPackage() throws IOException, AndrolibException {
...
mPkg = new ResPackage(id, name);
// 系统包名不混淆
if (mPkg.getName().equals("android")) {
mPkg.setCanResguard(false);
} else {
mPkg.setCanResguard(true);
}
nextChunk();
while (mHeader.type == Header.TYPE_LIBRARY) {
readLibraryType();
}
while (mHeader.type == Header.TYPE_SPEC_TYPE) {
readTableTypeSpec();
}
return mPkg;
}
- 这里有个处理,就是判断系统包名不混淆
- 然后是核心的readLibraryType和readTableTypeSpec方法,readLibraryType里面调用的也是readTableTypeSpec方法,所以此处直接看readTableTypeSpec方法
private void readTableTypeSpec() throws AndrolibException, IOException {
...
// first meet a type of resource
if (mCurrTypeID != id) {
mCurrTypeID = id;
initResGuardBuild(mCurrTypeID);
}
// 是否混淆文件路径
mShouldResguardForType = isToResguardFile(mTypeNames.getString(id - 1));
// 对,这里是用来描述差异性的!!!
mIn.skipBytes(entryCount * 4);
mResId = (0xff000000 & mResId) | id << 16;
while (nextChunk().type == Header.TYPE_TYPE) {
readConfig();
}
}
- 在白名单里的不需要混淆 - initResGuardBuild
- 某些文件路径不需要混淆,比如string,array - isToResguardFile
- 进行混淆处理 - readConfig
看下 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.reset(whiteListPatterns);
mResguardBuilder.removeStrings(RawARSCDecoder.getExistTypeSpecNameStrings(resTypeId));
// 如果是保持mapping的话,需要去掉某部分已经用过的mapping
reduceFromOldMappingFile();
}
/**
* 如果是保持mapping的话,需要去掉某部分已经用过的mapping
*/
private void reduceFromOldMappingFile() {
if (mPkg.isCanResguard()) {
if (mApkDecoder.getConfig().mUseKeepMapping) {
// 判断是否走keepmapping
HashMap<String, HashMap<String, HashMap<String, String>>> resMapping = mApkDecoder.getConfig().mOldResMapping;
String packName = mPkg.getName();
if (resMapping.containsKey(packName)) {
HashMap<String, HashMap<String, String>> typeMaps = resMapping.get(packName);
String typeName = mType.getName();
if (typeMaps.containsKey(typeName)) {
HashMap<String, String> proguard = typeMaps.get(typeName);
// 去掉所有之前保留的命名,为了简单操作,mapping里面有的都去掉
mResguardBuilder.removeStrings(proguard.values());
}
}
}
}
}
此处主要做了两件事
- 白名单不混淆
- 如果保持之前的mapping的话,需要去掉这些已经用掉的mapping,新的文件用新的mapping
接着看isToResguardFile方法
/**
* 为了加速,不需要处理string,id,array,这几个是肯定不是的
*/
private boolean isToResguardFile(String name) {
return (!name.equals("string") && !name.equals("id") && !name.equals("array"));
}
看注释,基本上已经可以明白他的用途
接着看 readConfig 方法
private void readConfig() throws IOException, AndrolibException {
...
int[] entryOffsets = mIn.readIntArray(entryCount);
for (int i = 0; i < entryOffsets.length; i++) {
mCurEntryID = i;
if (entryOffsets[i] != -1) {
mResId = (mResId & 0xffff0000) | i;
readEntry();
}
}
}
private void readEntry() throws IOException, AndrolibException {
mIn.skipBytes(2);
short flags = mIn.readShort();
int specNamesId = mIn.readInt();
if (mPkg.isCanResguard()) {
// 混淆过或者已经添加到白名单的都不需要再处理了
if (!mResguardBuilder.isReplaced(mCurEntryID) && !mResguardBuilder.isInWhiteList(mCurEntryID)) {
Configuration config = mApkDecoder.getConfig();
boolean isWhiteList = false;
if (config.mUseWhiteList) {
isWhiteList = dealWithWhiteList(specNamesId, config);
}
if (!isWhiteList) {
dealWithNonWhiteList(specNamesId, config);
}
}
}
if ((flags & ENTRY_FLAG_COMPLEX) == 0) {
readValue(true, specNamesId);
} else {
readComplexEntry(false, specNamesId);
}
}
通过以上代码可知
- 已经混淆过或者已经添加到白名单的都不需要再处理了 - dealWithWhiteList
- 具体的混淆操作 - dealWithNonWhiteList
接着看dealWithNonWhiteList方法
private void dealWithNonWhiteList(int specNamesId, Configuration config) throws AndrolibException, IOException {
String replaceString = null;
boolean keepMapping = false;
if (config.mUseKeepMapping) {
String packName = mPkg.getName();
if (config.mOldResMapping.containsKey(packName)) {
HashMap<String, HashMap<String, String>> typeMaps = config.mOldResMapping.get(packName);
String typeName = mType.getName();
if (typeMaps.containsKey(typeName)) {
HashMap<String, String> nameMap = typeMaps.get(typeName);
String specName = mSpecNames.get(specNamesId).toString();
if (nameMap.containsKey(specName)) {
keepMapping = true;
replaceString = nameMap.get(specName);
}
}
}
}
if (!keepMapping) {
replaceString = mResguardBuilder.getReplaceString();
}
mResguardBuilder.setInReplaceList(mCurEntryID);
if (replaceString == null) {
throw new AndrolibException("readEntry replaceString == null");
}
generalResIDMapping(mPkg.getName(), mType.getName(), mSpecNames.get(specNamesId).toString(), replaceString);
mPkg.putSpecNamesReplace(mResId, replaceString);
// arsc name列混淆成固定名字, 减少string pool大小
boolean useFixedName = config.mFixedResName != null && config.mFixedResName.length() > 0;
String fixedName = useFixedName ? config.mFixedResName : replaceString;
mPkg.putSpecNamesblock(fixedName, replaceString);
mType.putSpecResguardName(replaceString);
}
- config.mUseKeepMapping - 如果设置了config.mUseKeepMapping为true,就用老的mapping文件混淆
- 新的或者没设置mUseKeepMapping,就通过mResguardBuilder.getReplaceString()取,它实际上是组装的混淆字符串数组
public String getReplaceString() throws AndrolibException { if (mReplaceStringBuffer.isEmpty()) { throw new AndrolibException(String.format("now can only proguard less than 35594 in a single type\n")); } return mReplaceStringBuffer.remove(0); }
可以看到他取的是mReplaceStringBuffer,取出后移除该元素,防止重复。
mReplaceStringBuffer是一个集合,它的组装如下
private String[] mAToZ = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" }; private String[] mAToAll = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" }; /** * 在window上面有些关键字是不能作为文件名的 * CON, PRN, AUX, CLOCK$, NUL * COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9 * LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9. */ private HashSet<String> mFileNameBlackList; public ResguardStringBuilder() { mFileNameBlackList = new HashSet<>(); mFileNameBlackList.add("con"); mFileNameBlackList.add("prn"); mFileNameBlackList.add("aux"); mFileNameBlackList.add("nul"); mReplaceStringBuffer = new ArrayList<>(); mIsReplaced = new HashSet<>(); mIsWhiteList = new HashSet<>(); } public void reset(HashSet<Pattern> blacklistPatterns) { mReplaceStringBuffer.clear(); mIsReplaced.clear(); mIsWhiteList.clear(); for (int i = 0; i < mAToZ.length; i++) { String str = mAToZ[i]; if (!Utils.match(str, blacklistPatterns)) { mReplaceStringBuffer.add(str); } } for (int i = 0; i < mAToZ.length; i++) { String first = mAToZ[i]; for (int j = 0; j < mAToAll.length; j++) { String str = first + mAToAll[j]; if (!Utils.match(str, blacklistPatterns)) { mReplaceStringBuffer.add(str); } } } for (int i = 0; i < mAToZ.length; i++) { String first = mAToZ[i]; for (int j = 0; j < mAToAll.length; j++) { String second = mAToAll[j]; for (int k = 0; k < mAToAll.length; k++) { String third = mAToAll[k]; String str = first + second + third; if (!mFileNameBlackList.contains(str) && !Utils.match(str, blacklistPatterns)) { mReplaceStringBuffer.add(str); } } } } }
- 替换后的id放到mIsReplaced set中
- 写入mapping文件 - generalResIDMapping
- 设置替换后的名称 - putSpecNamesReplace
继续读之后的readValue方法
/**
* @param flags whether read direct
*/
private void readValue(boolean flags, int specNamesId) throws IOException, AndrolibException {
...
//这里面有几个限制,一对于string ,id, array我们是知道肯定不用改的,第二看要那个type是否对应有文件路径
if (mPkg.isCanResguard()
&& flags
&& type == TypedValue.TYPE_STRING
&& mShouldResguardForType
&& mShouldResguardTypeSet.contains(mType.getName())) {
if (mTableStringsResguard.get(data) == null) {
...
File resRawFile = new File(mApkDecoder.getOutTempDir().getAbsolutePath() + File.separator + compatibaleraw);
File resDestFile = new File(mApkDecoder.getOutDir().getAbsolutePath() + File.separator + compatibaleresult);
MergeDuplicatedResInfo filterInfo = null;
boolean mergeDuplicatedRes = mApkDecoder.getConfig().mMergeDuplicatedRes;
if (mergeDuplicatedRes) {
filterInfo = mergeDuplicated(resRawFile, resDestFile, compatibaleraw, result);
if (filterInfo != null) {
resDestFile = new File(filterInfo.filePath);
result = filterInfo.fileName;
}
}
...
if (!resRawFile.exists()) {
System.err.printf("can not find res file, you delete it? path: resFile=%s\n", resRawFile.getAbsolutePath());
} else {
...
if (filterInfo == null) {
FileOperation.copyFileUsingStream(resRawFile, resDestFile);
}
//already copied
mApkDecoder.removeCopiedResFile(resRawFile.toPath());
mTableStringsResguard.put(data, result);
}
}
}
}
- resRawFile - 原始文件
- resDestFile - 混淆后的文件
- mergeDuplicated - 资源过滤,过滤重复资源,减少apk体积
- copyFileUsingStream - 讲原文件内容复制给混淆后的文件
- mTableStringsResguard - 混淆后的全局经放到mTableStringsResguard字典里
重新生成resources.arsc
ARSCDecoder.write(apkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
接着分析此方法
public static void write(InputStream arscStream, ApkDecoder decoder, ResPackage[] pkgs) throws AndrolibException {
...
for (int i = 0; i < packageCount; i++) {
mCurPackageID = i;
writePackage();
}
// 最后需要把整个的size重写回去
reWriteTable();
}
private void writePackage() throws IOException, AndrolibException {
...
if (mPkgs[mCurPackageID].isCanResguard()) {
int specSizeChange = StringBlock.writeSpecNameStringBlock(mIn,
mOut,
mPkgs[mCurPackageID].getSpecNamesBlock(),
mCurSpecNameToPos
);
mPkgsLenghtChange[mCurPackageID] += specSizeChange;
mTableLenghtChange += specSizeChange;
} else {
StringBlock.writeAll(mIn, mOut);
}
writeNextChunk(0);
while (mHeader.type == Header.TYPE_LIBRARY) {
writeLibraryType();
}
while (mHeader.type == Header.TYPE_SPEC_TYPE) {
writeTableTypeSpec();
}
}
private void writeTableTypeSpec() throws AndrolibException, IOException {
...
while (writeNextChunk(0).type == Header.TYPE_TYPE) {
writeConfig();
}
}
private void writeConfig() throws IOException, AndrolibException {
...
for (int i = 0; i < entryOffsets.length; i++) {
if (entryOffsets[i] != -1) {
mResId = (mResId & 0xffff0000) | i;
writeEntry();
}
}
}
private void writeEntry() throws IOException, AndrolibException {
...
if (pkg.isCanResguard()) {
specNamesId = mCurSpecNameToPos.get(pkg.getSpecRepplace(mResId));
if (specNamesId < 0) {
throw new AndrolibException(String.format("writeEntry new specNamesId < 0 %d", specNamesId));
}
}
mOut.writeInt(specNamesId);
if ((flags & ENTRY_FLAG_COMPLEX) == 0) {
writeValue();
} else {
writeComplexEntry();
}
}
private void writeValue() throws IOException, AndrolibException {
/* size */
mOut.writeCheckShort(mIn.readShort(), (short) 8);
/* zero */
mOut.writeCheckByte(mIn.readByte(), (byte) 0);
byte type = mIn.readByte();
mOut.writeByte(type);
int data = mIn.readInt();
mOut.writeInt(data);
}
- writeTableNameStringBlock - 重写全局字符串池,计算混淆后全局字符串池长度与混淆前的差值。后面 reWriteTable() 方法会用到 mTableLenghtChange
- 重写package
- 最后需要把整个的size重写回去
可以看到此处重写了resources.arsc 文件,如果想按照自己的方式构建此文件,在此处可以尝试添加代码
重新构建APK
private void buildApk(
ApkDecoder decoder, File apkFile, File outputFile, InputParam.SignatureType signatureType, int minSDKVersion)
throws Exception {
ResourceApkBuilder builder = new ResourceApkBuilder(config);
String apkBasename = apkFile.getName();
apkBasename = apkBasename.substring(0, apkBasename.indexOf(".apk"));
builder.setOutDir(mOutDir, apkBasename, outputFile);
System.out.printf("[AndResGuard] buildApk signatureType: %s\n", signatureType);
switch (signatureType) {
case SchemaV1:
builder.buildApkWithV1sign(decoder.getCompressData());
break;
case SchemaV2:
case SchemaV3:
builder.buildApkWithV2V3Sign(decoder.getCompressData(), minSDKVersion, signatureType);
break;
}
}
此处简单分析,根据不同签名方式进行打包操作
总结
整个资源混淆流程如下
- 解压APK,混淆res目录为r
- 第一次解析resources.arsc,保存原来的资源信息,为mapping文件做准备
- 第二次解析resources.arsc,生成混淆信息
- 重新生成resources.arsc
- 重新打包成APK
**从以上流程可知,如果需要对键常量池进行裁剪,可以尝试在第4步进行操作