AndResGuard分析

AndResGuard是一款由微信团队成员开源的工具,旨在减小Android应用的资源包大小。通过缩短资源文件名,而不改变资源ID,实现对APK文件的处理。本文详细介绍了其工作原理、使用方法及代码分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

公司6.20版本开始注重Android包大小,所以就搜了一下github上通用的可以减小包大小的方法,AndResGuard是其中的一个,今天就学习下顺便分析下。

一、简介:

AndResGuard的github项目地址:

https://github.com/shwenzhang/AndResGuard

AndResGuard应该是微信团队中的一员开源出来的一个注重减小Res包大小的工具,可以直接对Apk进行处理得到处理后的Apk文件。

二、原理:

大家都知道,我们使用资源文件的时候使用的都是int值,而不是直接使用的resource name。所以这其中肯定会有一张类似于Map的表,来实现这一一对应的关系。

所以我们如果修改掉resource name,使其变短,变成a、b、c、d这样的名字,而对应的id仍不变化的,是不会影响apk中使用资源的。所以我们可以通过减小文件名的长度来减小资源文件的大小。这也就是AndResGuard实现的基本原理。

当然apk打包过程中资源映射并不仅仅只是简单的map关系,这也不是本文讨论的重点,如果有兴趣可以看一下下面这篇文章。

Android逆向之旅---解析编译之后的Resource.arsc文件格式

http://blog.youkuaiyun.com/jiangwei0910410003/article/details/50628894

当然,如果是混淆了之后使用getResource.getIdentifier()的方式去找的话,肯定就找不到了,但是用这种方式去调用资源文件应该还是少数吧。

三、准备工作:

一、clone下工程,本地编译。过程不多叙述。git clone git@github.com:shwenzhang/AndResGuard.git

二、本地调试

从github上拷贝下AndResGuard的工程,本地使用Android Studio打开。

下面说一下我遇到的问题以及解决方式:

1.AndroidResGuard-example工程sync不通过,直接忽略,反正我想看的是代码,最终调试源码就好了。

2.编译版本,工程默认编译版本是1.7,所以使用了很多1.7之后才有的属性,比如HashMap<T,T> map=new HashMap<>();,所以需要在工程里面设置默认编译版本为1.7。设置方式:选择工程->Project Structure->Project Settings->Project->

Project SDK选择1.7之后的,下面的Priject language level选择:7 -Diamonds,ARM...+

3.由于该工程是java工程,所以运行的时候需要选择java的方式。程序入口是AndResGuard-cli工程下的CliMain文件。右键->Run"CliMain.main"运行

4.main函数里面是带参数运行的,需要在入口args传入参数。并且程序中对这个做了检查。所以我暂时直接在main方法里面直接写死给args的赋值。

然后就可以运行了。我选择的自然是debug调试了。

四、代码分析:

执行脚本有两种方式,分别是命令行执行jar文件运行CliMain.main主函数。第二种是运行gradle脚本执行Main.gradleRun方法。下面说的是第一种。

1.由于我们通过的是直接运行main主函数的方式,这里需要给args设置一些必要的参数

参数分为两种方式传入,第一种是命令行(就是直接通过给args设值),第二种通过config.xml文件获取。

有几个是必传参数,由于这一块也不是重点,所以简单的说一下我的配置,方便大家照着做的时候方便运行。(注意顺序很重要)

    public static void main(String[] args) {
        mBeginTime = System.currentTimeMillis();
        CliMain m = new CliMain();
        setRunningLocation(m);
        List<String> list = new ArrayList<>();
        list.add("D:\\develop_workspace\\git_warehouse\\AndResGuard\\tool_output\\test2\\XX_V6.19.0_old.apk");

        list.add("-config");
        list.add("D:\\develop_workspace\\git_warehouse\\AndResGuard\\tool_output\\config.xml");

        list.add("-out");
        list.add("D:\\develop_workspace\\git_warehouse\\AndResGuard\\tool_output\\test2\\out");

        list.add("-signature");
        list.add("D:\\develop_workspace\\git_warehouse\\AndResGuard\\tool_output\\debug.keystore");
        list.add("KeyPass");
        list.add("storePass");
        list.add("storeAlias");

        args = list.toArray(new String[]{});
        m.run(args);
    }

上面中的config.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<resproguard>
    <!--defaut property to set  -->
    <issue id="property">
        <!--whether use 7zip to repackage the signed apk, you must install the 7z command line version in window -->
        <!--sudo apt-get install p7zip-full in linux -->
        <!--and you must write the sign data fist, and i found that if we use linux, we can get a better result -->
        <seventzip value="false"/>
        <!--the sign data file name in your apk, default must be META-INF-->
        <!--generally, you do not need to change it if you dont change the meta file name in your apk-->
        <metaname value="META-INF"/>
        <!--if keep root, res/drawable will be kept, it won't be changed to such as r/s-->
        <keeproot value="false"/>
    </issue>

    <!--whitelist, some resource id you can not proguard, such as getIdentifier-->
    <!--isactive, whether to use whitelist, you can set false to close it simply-->
    <issue id="whitelist" isactive="true">
        <!--you must write the full package name, such as com.tencent.mm.R -->
        <!--for some reason, we should keep our icon better-->
        <!--and it support *, ?, such as com.tencent.mm.R.drawable.emoji_*, com.tencent.mm.R.drawable.emoji_?-->
        <!--<path value="<your_package_name>.R.drawable.icon"/>-->
        <!--<path value="<your_package_name>.R.string.com.crashlytics.*"/>-->
        <!--<path value="<your_package_name>.R.string.umeng*"/>-->
        <!--<path value="<your_package_name>.R.layout.umeng*"/>-->
        <!--<path value="<your_package_name>.R.drawable.umeng*"/>-->
        <!--<path value="<your_package_name>.R.anim.umeng*"/>-->
        <!--<path value="<your_package_name>.R.color.umeng*"/>-->
        <!--<path value="<your_package_name>.R.style.*UM*"/>-->
        <!--<path value="<your_package_name>.R.style.umeng*"/>-->
        <!--<path value="<your_package_name>.R.id.umeng*"/>-->
        <!--<path value="<your_package_name>.R.string.UM*"/>-->
        <!--<path value="<your_package_name>.R.string.tb_*"/>-->
        <!--<path value="<your_package_name>.R.layout.tb_*"/>-->
        <!--<path value="<your_package_name>.R.drawable.tb_*"/>-->
        <!--<path value="<your_package_name>.R.color.tb_*"/>-->
    </issue>

    <!--keepmapping, sometimes if we need to support incremental upgrade, we should keep the old mapping-->
    <!--isactive, whether to use keepmapping, you can set false to close it simply-->
    <!--if you use -mapping to set keepmapping property in cammand line, these setting will be overlayed-->
    <issue id="keepmapping" isactive="false">
        <!--the old mapping path, in window use \, in linux use /, and the default path is the running location-->
        <path value="{your_mapping_path}"/>
    </issue>

    <!--compress, if you want to compress the file, the name is relative path, such as resources.arsc, res/drawable-hdpi/welcome.png-->
    <!--what can you compress? generally, if your resources.arsc less than 1m, you can compress it. and i think compress .png, .jpg is ok-->
    <!--isactive, whether to use compress, you can set false to close it simply-->
    <issue id="compress" isactive="false">
        <!--you must use / separation, and it support *, ?, such as *.png, *.jpg, res/drawable-hdpi/welcome_?.png-->
        <path value="*.png"/>
        <path value="*.jpg"/>
        <path value="*.jpeg"/>
        <path value="*.gif"/>
        <path value="resources.arsc"/>
    </issue>

</resproguard>
2.main(args)->m.run(args)->resourceProguard(outputFile,apkFileName)->decodeResource方法

核心ApkDecoder.decode方法中的两个方法,别为是

RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"));
ResPackage[] pkgs = ARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"), this);

第一行的方法的作用我暂时没有看的太懂,猜测的意思应该是读取这个文件加入到内存当中。

OK,首先介绍第一行的方法。




第二行的作用则是整个工程的核心之一,即混淆资源文件名了。

首先,进入到ARSCDecoder.decode方法中后,首先看到的是RawARSCDecoder的构造方法

ARSCDecoder decoder = new ARSCDecoder(arscStream, apkDecoder);

ARSCDecoder的构造函数中,执行方法proguardFileName();

private void proguardFileName() throws IOException, AndrolibException {
    mMappingWriter = new BufferedWriter(new FileWriter(mApkDecoder.getResMappingFile(), false));

    mProguardBuilder = new ProguardStringBuilder();
    mProguardBuilder.reset();

    final Configuration config = mApkDecoder.getConfig();

    File rawResFile = mApkDecoder.getRawResFile();

    File[] resFiles = rawResFile.listFiles();

    //需要看看哪些类型是要混淆文件路径的
    for (File resFile : resFiles) {
        String raw = resFile.getName();
        if (raw.contains("-")) {
            raw = raw.substring(0, raw.indexOf("-"));
        }
        mShouldProguardTypeSet.add(raw);
    }

    if (!config.mKeepRoot) {
        //需要保持之前的命名方式
        if (config.mUseKeepMapping) {
            HashMap<String, String> fileMapping = config.mOldFileMapping;
            List<String> keepFileNames = new ArrayList<String>();
            //这里面为了兼容以前,也需要用以前的文件名前缀,即res混淆成什么
            String resRoot = TypedValue.RES_FILE_PATH;
            for (String name : fileMapping.values()) {
                int dot = name.indexOf("/");
                if (dot == -1) {
                    throw new IOException(
                        String.format("the old mapping res file path should be like r/a, yours %s\n", name)
                    );
                }
                resRoot = name.substring(0, dot);
                keepFileNames.add(name.substring(dot + 1));
            }
            //去掉所有之前保留的命名,为了简单操作,mapping里面有的都去掉
            mProguardBuilder.removeStrings(keepFileNames);

            for (File resFile : resFiles) {
                String raw = "res" + "/" + resFile.getName();
                if (fileMapping.containsKey(raw)) {
                    mOldFileName.put(raw, fileMapping.get(raw));
                } else {
                    System.out.printf("can not find the file mapping %s\n", raw);
                    mOldFileName.put(raw, resRoot + "/" + mProguardBuilder.getReplaceString());
                }
            }
        } else {
            for (int i = 0; i < resFiles.length; i++) {
                //这里也要用linux的分隔符,如果普通的话,就是r
                mOldFileName.put("res" + "/" + resFiles[i].getName(), TypedValue.RES_FILE_PATH + "/" + mProguardBuilder.getReplaceString());
            }
        }
        generalFileResMapping();
    }

    Utils.cleanDir(mApkDecoder.getOutResFile());
}

这段代码意思是如果没有设置keep包名的话,则读取res下所有的资源包目录,对应的生成一个一一对应的LinkHashMap:mOldFileName



其中value的值的设置,是从提前设置好的一个List里面拿的。拿出来一个,则从原有的List删掉。这样确保不会重复。

然后清空outPath下r下的所有文件,为后面的生成做准备。


其次,执行readTable方法

ResPackage[] pkgs = decoder.readTable();

讲到这里,我们必须稍微普及一下resource.arsc的概念了。resource.arsc可以想象成以二进制存储的数据模型。我们读的时候,需要按字节所在的位置读取我们所需要的内容。

下面借用大神的神图,进行下一步的解释:


OK,接下来详细的看readTable()方法,大家可以完全的和上面的图对照起来。

private ResPackage[] readTable() throws IOException, AndrolibException {
    nextChunkCheckType(Header.TYPE_TABLE);//这个方法里面作者先读取short类型2字节,然后跳过2字节长度的头大小,再读取int类型4字节文件大小构造Header对象
    int packageCount = mIn.readInt();//在读取int类型4字节的package数
    StringBlock.read(mIn);//这个方法的主要作用就是读取上图中Global String Pool的部分。如果
    ResPackage[] packages = new ResPackage[packageCount];
    nextChunk();
    for (int i = 0; i < packageCount; i++) {
        packages[i] = readPackage();
    }
    return packages;
}




//第一天暂时到这,后续继续编辑



3.buildApk();方法




五、后续优化:

搞懂了其实现的原理,就可以在其基础上进行进一步的改造,比如现在AndResGuard仅支持主apk压缩,那么如果我的APK采用的是多apk技术,子apk都放在asset文件夹下动态加载的话,自然的就不可能去混淆子apk的res文件了,而我所要实现的就是解决这个问题。





评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失落夏天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值