AndResGuard分析

    xiaoxiao2025-05-02  8

    公司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.csdn.net/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文件了,而我所要实现的就是解决这个问题。

    转载请注明原文地址: https://ju.6miu.com/read-1298689.html
    最新回复(0)