Matrix-ApkChecker 关于 Unused Resources的原理

背景

Matrix ApkChecker 中 Unused Resources功能用于扫描 apk 中无用的资源(包括drawable、layout、value类型资源等)。由于工作中使用到了这个功能,且对其实现感兴趣,特此记录~

前置知识

android 代码中的资源引用

例子:
先写一个Acitvity的 layout文件和 class 文件:


<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <ImageView
        android:id="@+id/iv_test"
        android:layout_width="120dp"
        android:layout_height="120dp"/>

    <ImageView
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:src="@drawable/matrix"
        app:layout_constraintTop_toBottomOf="@id/iv_test"/>

androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val ivText = findViewById<ImageView>(R.id.iv_test)
        ivText.setImageResource(R.drawable.github);
    }
}

很简单的代码。两个Image 一个是通过java 文件中的 setImageResource 来设置“github”图片资源的,一个是通过在 xml 文件中的 android:src=“@drawable/matrix” 来设置图片资源的。
1、对于在 java 文件中设置的资源,可以通过反编译apk 解析smali 文件来识别使用了什么资源。
2、对于在xml 文件中设置的资源,由于xml 文件会被放在 apk 中的 res 目录下,所以可以直接解析在 apk 解压出来的xml 文件。

R.txt 文件

Android Studio 构建完成后,会在 build目录下生成一个 R.txt 文件,这个文件记录 apk 所有的<资源:资源ID>映射,包括 application、library、第三方包中的资源。
拿上面代码中的 “github” 图片为例,它在 R.txt 文件中也有相应的 ID映射,
如下图所示R.txt 文件:
Matrix-ApkChecker 关于 Unused Resources的原理_第1张图片

resources.arsc 文件

resources.arsc 文件是将 apk 文件解压后的产物之一。其包含了 apk 中所有资源的相关信息。
拿上面 github 图片举例,其在 resources.arsc 如下所示:
Matrix-ApkChecker 关于 Unused Resources的原理_第2张图片

可以看到,github 图片的 id 在这里也出现了,同时该文件还保存了 github 图片在 apk 中的路径。

资源混淆

例如像 AndResGuard 这样的资源混淆,它会缩减资源的文件路径和名称,比如上面的 github 文件可能会重命名为 a,并且,apk 中 res 目录下相应的文件名和路径也会重命名, resources.arsc 中相应的资源名称也会重命名。
同时,资源混淆工具通常会生成一个 mapping 文件,记录了混淆前和混淆后的资源名称映射。
mapping 文件的每一行格式类似如下:

com.example.myapplication.debug.R.drawable.github -> com.example.myapplication.debug.R.drawable.a

反编译后的 smali 代码

我们将上述的 apk 进行反编译,看看 MainActivity 中是如何获取资源的
反编译后的到的目录如下:
Matrix-ApkChecker 关于 Unused Resources的原理_第3张图片
可以看到,反编译后,得到了 smali 代码,我们找到 MainActivity 的 smali 文件:
Matrix-ApkChecker 关于 Unused Resources的原理_第4张图片
MainActivity.smali 内容如下:

.class public final Lcom/example/myapplication/MainActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MainActivity.kt"


# annotations
.annotation runtime Lkotlin/Metadata;
    d1 = {
        "\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\u0008\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0014\u00a8\u0006\u0007"
    }
    d2 = {
        "Lcom/example/myapplication/MainActivity;",
        "Landroidx/appcompat/app/AppCompatActivity;",
        "()V",
        "onCreate",
        "",
        "savedInstanceState",
        "Landroid/os/Bundle;",
        "app_debug"
    }
    k = 0x1
    mv = {
        0x1,
        0x5,
        0x1
    }
    xi = 0x30
.end annotation


# direct methods
.method public constructor <init>()V
    .locals 0

    .line 8
    invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V

    return-void
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 2
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .line 10
    invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V

    .line 11
    const v0, 0x7f0b001c

    invoke-virtual {p0, v0}, Lcom/example/myapplication/MainActivity;->setContentView(I)V

    .line 13
    const v0, 0x7f0800c1

    invoke-virtual {p0, v0}, Lcom/example/myapplication/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/ImageView;

    .line 14
    .local v0, "ivText":Landroid/widget/ImageView;
    const v1, 0x7f07006d

    invoke-virtual {v0, v1}, Landroid/widget/ImageView;->setImageResource(I)V

    .line 15
    return-void
.end method

我们这里只看关注的代码:

.line 14
.local v0, "ivText":Landroid/widget/ImageView;
const v1, 0x7f07006d

invoke-virtual {v0, v1}, Landroid/widget/ImageView;->setImageResource(I)V

可以看到,直接是把 github 图片的 资源 ID 作为常量卸载代码中了,然后赋予 v1 寄存器,并调用 ImageView.setImageResources。

那么想要知道java 代码中使用了什么资源,我们就可以反编译apk,然后解析类似 const v1, 0x7f07006d ,取这个 0x7f07006d,判断其是否出现在 R 文件中,如果是,则说明0x7f07006d 是一个资源ID,并且被我们使用了。
除了 const 这种常量的引用形式外,还有如下几种形式:

1. const

const v6, 0x7f0c0061

2. sget

sget v6, Lcom/tencent/mm/R$string;->chatting_long_click_menu_revoke_msg:I
sget v1, Lcom/tencent/mm/libmmui/R$id;->property_anim:I

3. sput

sput-object v0, Lcom/tencent/mm/plugin_welab_api/R$styleable;->ActionBar:[I   //define resource in R.java

4. array-data

:array_0
.array-data 4
    0x7f0a0022
    0x7f0a0023
.end array-data

了解了上面这些,下面就看看 ApkChecker源码是怎么实现的吧!

ApkChecker源码解析

由于 matrix 项目庞大,这里直接找到无用资源扫描功能的关键 Task 类:UnusedResourcesTask.java。
然后我们直接看它的入口方法 call():

@Override
    public TaskResult call() throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            readMappingTxtFile(); // 解析代码混淆的 mapping文件
            readResourceTxtFile(); // 解析 R.txt 文件
            ResguardUtil.readResMappingTxtFile(resMappingTxt, null, resguardMap);
            unusedResSet.addAll(resourceDefMap.values());
            Log.i(TAG, "find resource declarations %d items.", unusedResSet.size());
            decodeCode(); // 解析java 文件中引用到的资源
            Log.i(TAG, "find resource references in classes: %d items.", resourceRefSet.size());
            decodeResources(); // 解析 xml (如layout)文件中引用到的资源
            Log.i(TAG, "find resource references %d items.", resourceRefSet.size());
            unusedResSet.removeAll(resourceRefSet);
            Log.i(TAG, "find unused references %d items", unusedResSet.size());
            Log.d(TAG, "find unused references %s", unusedResSet.toString());
            JsonArray jsonArray = new JsonArray();
            for (String name : unusedResSet) {
                jsonArray.add(name);
            }
            ((TaskJsonResult) taskResult).add("unused-resources", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
    }

可以看到,一共分为四步:
1、解析资源混淆的 mapping文件
2、解析 R.txt 文件
3、解析java 文件中引用到的资源
4、解析 xml (如layout)文件中引用到的资源

我们逐步看一下

解析代码混淆的 mapping文件

private void readMappingTxtFile() throws IOException {
        // com.tencent.mm.R$string -> com.tencent.mm.R$l:
        //      int fade_in_property_anim -> aRW

        if (mappingTxt != null) {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(mappingTxt));
            String line = bufferedReader.readLine();
            boolean readRField = false;
            String beforeClass = "", afterClass = "";
            try {
                while (line != null) {
                    if (!line.startsWith(" ")) {
                        String[] pair = line.split("->");
                        if (pair.length == 2) {
                            beforeClass = pair[0].trim();
                            afterClass = pair[1].trim();
                            afterClass = afterClass.substring(0, afterClass.length() - 1);
                            if (!Util.isNullOrNil(beforeClass) && !Util.isNullOrNil(afterClass) && ApkUtil.isRClassName(ApkUtil.getPureClassName(beforeClass))) {
                                Log.d(TAG, "before:%s,after:%s", beforeClass, afterClass);
                                readRField = true;
                            } else {
                                readRField = false;
                            }
                        } else {
                            readRField = false;
                        }
                    } else {
                        if (readRField) {
                            String[] entry = line.split("->");
                            if (entry.length == 2) {
                                String key = entry[0].trim();
                                String value = entry[1].trim();
                                if (!Util.isNullOrNil(key) && !Util.isNullOrNil(value)) {
                                    String[] field = key.split(" ");
                                    if (field.length == 2) {
                                        Log.d(TAG, "%s -> %s", afterClass.replace('$', '.') + "." + value, ApkUtil.getPureClassName(beforeClass).replace('$', '.') + "." + field[1]);
                                        rclassProguardMap.put(afterClass.replace('$', '.') + "." + value, ApkUtil.getPureClassName(beforeClass).replace('$', '.') + "." + field[1]);
                                    }
                                }
                            }
                        }
                    }
                    line = bufferedReader.readLine();
                }
            } finally {
                bufferedReader.close();
            }
        }
    }

代码混淆的 mapping 文件中包含了类似如下内容

com.example.myapplication.R$drawable -> com.example.myapplication.R$drawable
	int github -> github
	.......

解析 R$drawable 等部分内容,获取到对应的资源的包名及其映射,并存入rclassProguardMap集合中,
集合的映射类似如下:
com.example.myapplication.R.drawable.github -> R.drawable.github
该集合用于后面使用

解析 R.txt 文件

private void readResourceTxtFile() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new FileReader(resourceTxt));
        String line = bufferedReader.readLine();
        try {
            while (line != null) {
                String[] columns = line.split(" ");
                if (columns.length >= 4) {
                    final String resourceName = "R." + columns[1] + "." + columns[2];
                    if (!columns[0].endsWith("[]") && columns[3].startsWith("0x")) {
                        if (columns[3].startsWith("0x01")) {
                            Log.d(TAG, "ignore system resource %s", resourceName);
                        } else {
                            final String resId = parseResourceId(columns[3]);
                            if (!Util.isNullOrNil(resId)) {
                                resourceDefMap.put(resId, resourceName);
                            }
                        }
                    } else {
                        Log.d(TAG, "ignore resource %s", resourceName);
                        if (columns[0].endsWith("[]") && columns.length > 5) {
                            Set<String> attrReferences = new HashSet<String>();
                            for (int i = 4; i < columns.length; i++) {
                                if (columns[i].endsWith(",")) {
                                    attrReferences.add(columns[i].substring(0, columns[i].length() - 1));
                                } else {
                                    attrReferences.add(columns[i]);
                                }
                            }
                            styleableMap.put(resourceName, attrReferences);
                        }
                    }
                }
                line = bufferedReader.readLine();
            }
        } finally {
            bufferedReader.close();
        }
    }

代码也很简单,逐行解析 R.txt 文件,并把所有的 映射保存到 resourceDefMap 集合中,集合内容类似如下:
0x7f07006d:R.drawable.github

解析java 文件中引用到的资源

private void decodeCode() throws IOException {
        for (String dexFileName : dexFileNameList) {
            // 获取所有 dex 文件
            MultiDexContainer<? extends DexBackedDexFile> dexFiles = DexFileFactory.loadDexContainer(new File(inputFile, dexFileName), Opcodes.forApi(15));

            for (String dexEntryName : dexFiles.getDexEntryNames()) {
                MultiDexContainer.DexEntry<? extends DexBackedDexFile> dexEntry = dexFiles.getEntry(dexEntryName);
                BaksmaliOptions options = new BaksmaliOptions();
                // 获取每个 dex 文件下所有的 class 
                List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexEntry.getDexFile().getClasses());
                for (ClassDef classDef : classDefs) {
                    String[] lines = ApkUtil.disassembleClass(classDef, options);
                    if (lines != null) {
                        // 逐个解析 class的每一行
                        readSmaliLines(lines);
                    }
                }
            }

        }
    }

这里设计到使用 apktool 来反编译 apk 包,并获取 smali 文件的每一行。
有兴趣可以去看看 apktool 的源码,这里跳过~
我们现在拿到了 smali 文件,接下来看一下解析每一行的方法 readSmaliLines():

private void readSmaliLines(String[] lines) {
        if (lines == null) {
            return;
        }
        boolean arrayData = false;
        for (String line : lines) {
            line = line.trim();
            if (!Util.isNullOrNil(line)) {
                if (line.startsWith("const")) {
                    String[] columns = line.split(" ");
                    if (columns.length >= 3) {
                        final String resId = parseResourceId(columns[2].trim());
                        if (!Util.isNullOrNil(resId) && resourceDefMap.containsKey(resId)) {
                            resourceRefSet.add(resourceDefMap.get(resId));
                        }
                    }
                } else if (line.startsWith("sget")) {
                    String[] columns = line.split(" ");
                    if (columns.length >= 3) {
                        final String resourceRef = parseResourceNameFromProguard(columns[2].trim());
                        if (!Util.isNullOrNil(resourceRef)) {
                            Log.d(TAG, "find resource reference %s", resourceRef);
                            if (styleableMap.containsKey(resourceRef)) {
                                //reference of R.styleable.XXX
                                for (String attr : styleableMap.get(resourceRef)) {
                                    resourceRefSet.add(resourceDefMap.get(attr));
                                }
                            } else {
                                resourceRefSet.add(resourceRef);
                            }
                        }
                    }
                } else if (line.startsWith(".array-data 4")) {
                    arrayData = true;
                } else if (line.startsWith(".end array-data")) {
                    arrayData = false;
                } else  {
                    if (arrayData) {
                        String[] columns = line.split(" ");
                        if (columns.length > 0) {
                            final String resId = parseResourceId(columns[0].trim());
                            if (!Util.isNullOrNil(resId) && resourceDefMap.containsKey(resId)) {
                                Log.d(TAG, "array field resource, %s", resId);
                                resourceRefSet.add(resourceDefMap.get(resId));
                            }
                        }
                        if (line.trim().startsWith("0x")) {
                            final String resId = parseResourceId(line.trim());
                            if (!Util.isNullOrNil(resId) && resourceDefMap.containsKey(resId)) {
                                Log.d(TAG, "array field resource, %s", resId);
                                resourceRefSet.add(resourceDefMap.get(resId));
                            }
                        }
                    }
                }
            }
        }
    }

上面讲过了, smali 中有可能是获取资源的代码有四种:

 1. const

 const v6, 0x7f0c0061

 2. sget

 sget v6, Lcom/tencent/mm/R$string;->chatting_long_click_menu_revoke_msg:I
 sget v1, Lcom/tencent/mm/libmmui/R$id;->property_anim:I

 3. sput

 sput-object v0, Lcom/tencent/mm/plugin_welab_api/R$styleable;->ActionBar:[I   //define resource in R.java

 4. array-data

 :array_0
 .array-data 4
     0x7f0a0022
     0x7f0a0023
 .end array-data

readSmaliLines() 方法就是对着四种情况进行枚举,比如 const v6, 0x7f0c0061 ,代表了代码保存了一个值到寄存器中,我们通过resourceDefMap.containsKey(resId) 就可以判断它是不是一种资源。
然后将其保存在resourceRefSet集合中
其他情况类似~

解析 xml (如layout)文件中引用到的资源

private void decodeResources() throws IOException, InterruptedException, AndrolibException, XmlPullParserException {
        File manifestFile = new File(inputFile, ApkConstants.MANIFEST_FILE_NAME);
        // resources.arsc 文件
        File arscFile = new File(inputFile, ApkConstants.ARSC_FILE_NAME);
        // apk 中的 res 目录
        File resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_NAME);
        if (!resDir.exists()) {
            resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_PROGUARD_NAME);
        }

        Map<String, Set<String>> fileResMap = new HashMap<>(); // 保存了 非value 文件(比如layout、drawable)中,引用了那些资源
        Set<String> valuesReferences = new HashSet<>(); // 保存了 value 文件(比如 dimen)中,引用了那些资源
        //进行解析
        ApkResourceDecoder.decodeResourcesRef(manifestFile, arscFile, resDir, fileResMap, valuesReferences);

        for (String resource : fileResMap.keySet()) {
            Set<String> result = new HashSet<>();
            for (String resName : fileResMap.get(resource)) {
               if (resguardMap.containsKey(resName)) {
                   result.add(resguardMap.get(resName));
               } else {
                   result.add(resName);
               }
            }
            if (resguardMap.containsKey(resource)) {
                nonValueReferences.put(resguardMap.get(resource), result);
            } else {
                nonValueReferences.put(resource, result);
            }
        }

        for (String resource : valuesReferences) {
            if (resguardMap.containsKey(resource)) {
                resourceRefSet.add(resguardMap.get(resource));
            } else {
                resourceRefSet.add(resource);
            }
        }

        for (String resource : unusedResSet) {
            if (ignoreResource(resource)) {
                resourceRefSet.add(resource);
            }
        }

        for (String resource : resourceRefSet) {
            readChildReference(resource);
        }
    }

这里解析了两种文件,一种是 非value类型的,比如layout、drawable等,另一种是 value 类型的文件,比如 dimen 文件
这里我们看一下 非value 类型的解析就行了,进入ApkResourceDecoder.decodeResourcesRef() 方法:

public static void decodeResourcesRef(File manifestFile, File arscFile, File resDir, Map<String, Set<String>> nonValueReferences, Set<String> valueReferences) throws IOException, AndrolibException, XmlPullParserException {
        if (!FileUtil.isLegalFile(manifestFile)) {
            Log.w(TAG, "File %s is illegal!", ApkConstants.MANIFEST_FILE_NAME);
            return;
        }
        if (!FileUtil.isLegalFile(arscFile)) {
            Log.w(TAG, "File %s is illegal!", ApkConstants.ARSC_FILE_NAME);
            return;
        }
        if (resDir != null && resDir.exists() && resDir.isDirectory()) {
            //decode arsc file
            ResTable resTable = new ResTable();
            decodeArscFile(arscFile, resTable);

            AXmlResourceParser aXmlResourceParser = createAXmlParser(arscFile);
            XmlPullParser xmlPullParser = XmlPullParserFactory.newInstance().newPullParser();
            ExtMXSerializer serializer = createXmlSerializer();
            for (ResPackage pkg : resTable.listMainPackages()) {
                aXmlResourceParser.getAttrDecoder().setCurrentPackage(pkg);
                for (ResResource resSource : pkg.listFiles()) { // 获取所有 非value 类型的文件进行解析
                    decodeResResource(resSource, resDir, aXmlResourceParser, nonValueReferences);
                }

                for (ResValuesFile valuesFile : pkg.listValuesFiles()) {
                    decodeResValues(valuesFile, xmlPullParser, serializer, valueReferences);
                }
            }

            //decode manifest file
            // 解析 manifest 文件, 获取其引用到的资源
            XmlPullResourceRefDecoder xmlDecoder = new XmlPullResourceRefDecoder(aXmlResourceParser);
            InputStream inputStream = new FileInputStream(manifestFile);
            xmlDecoder.decode(inputStream, null);
            valueReferences.addAll(xmlDecoder.getResourceRefSet());
        } else {
            Log.w(TAG, "Res dir is illegal!");
        }
    }

我们看关键的解析方法 decodeResResource() :

private static void decodeResResource(ResResource res, File inDir, AXmlResourceParser xmlParser, Map<String, Set<String>> nonValueReferences) throws AndrolibException, IOException {
        ResFileValue fileValue = (ResFileValue) res.getValue();
        String inFileName = fileValue.getStrippedPath();
        String typeName = res.getResSpec().getType().getName();

        try {
            File inFile = new File(inDir, inFileName);
            if (!FileUtil.isLegalFile(inFile)) {
//                Log.d(TAG, "Can not find %s", inFile.getAbsolutePath());
                return;
            }

            if (!inFileName.endsWith(".xml")) {
//                Log.d(TAG, "Not xml file %s, type %s", inFileName, typeName);
                return;
            }

            FileInputStream inputStream = new FileInputStream(inFile);
            XmlPullResourceRefDecoder xmlDecoder = new XmlPullResourceRefDecoder(xmlParser);
            xmlDecoder.decode(inputStream, null);
            String resource = ApkConstants.R_PREFIX + typeName + "." + inFile.getName().substring(0, inFile.getName().lastIndexOf('.'));
            if (!nonValueReferences.containsKey(resource)) {
                // getResourceRefSet 用于获取当前文件所引用到的所有资源
                nonValueReferences.put(resource, xmlDecoder.getResourceRefSet());
            } else {
                nonValueReferences.get(resource).addAll(xmlDecoder.getResourceRefSet());
            }
        } catch (AndrolibException ex) {
            Log.e(TAG, ex.getMessage());
        }
    }

我们要获取当前 非value 类型的文件所引用到的资源,其实就是一个解析 xml 文件的过程,代码中的 xmlDecoder.getResourceRefSet() 就是获取所有引用到的资源集合的代码,我们进入 XmlPullResourceRefDecoder 类中,看看这个方法返回的是什么?

private void handleContent() {
        String text = mParser.getText();
        if (!Util.isNullOrNil(text)) {
            if (text.startsWith("@")) {
                int index = text.indexOf('/');
                if (index > 1) {
                    String type = text.substring(1, index);
                    resourceRefSet.add(ApkConstants.R_PREFIX + type + "." + text.substring(index + 1).replace('.', '_'));
                }
            } else if (text.startsWith("?")) {
                int index = text.indexOf('/');
                if (index > 1) {
                    resourceRefSet.add(ApkConstants.R_ATTR_PREFIX + "." + text.substring(index + 1).replace('.', '_'));
                }
            }
        }
    }
    
    public Set<String> getResourceRefSet() {
        return resourceRefSet;
    }

可以看到 getResourceRefSet 直接返回了一个成员变量,这个变量的赋值在 handleElement() 中,handleElement中的代码比较直观,就是我们平常在 xml 文件中经常写的代码:
@drawable/matrix
?android:attr/xxx

decodeResources() 方法解析了 非value 和 value 类型文件的引用后,还做了一些操作:

private void decodeResources() throws IOException, InterruptedException, AndrolibException, XmlPullParserException {
        File manifestFile = new File(inputFile, ApkConstants.MANIFEST_FILE_NAME);
        // resources.arsc 文件
        File arscFile = new File(inputFile, ApkConstants.ARSC_FILE_NAME);
        // apk 中的 res 目录
        File resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_NAME);
        if (!resDir.exists()) {
            resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_PROGUARD_NAME);
        }

        Map<String, Set<String>> fileResMap = new HashMap<>(); // 保存了 非value 文件(比如layout、drawable、xml)中,引用了那些资源
        Set<String> valuesReferences = new HashSet<>(); // 保存了 value 文件(比如 dimen)中,引用了那些资源
        //进行解析
        ApkResourceDecoder.decodeResourcesRef(manifestFile, arscFile, resDir, fileResMap, valuesReferences);

        // 遍历 非value 类型的文件
        for (String resource : fileResMap.keySet()) {
            Set<String> result = new HashSet<>();
            // 遍历文件中引用到的资源名
            for (String resName : fileResMap.get(resource)) {
                // 将文件转化成混淆前的名称
               if (resguardMap.containsKey(resName)) {
                   result.add(resguardMap.get(resName));
               } else {
                   result.add(resName);
               }
            }
            // 再将每个文件的结果放入 nonValueReferences 集合中
            if (resguardMap.containsKey(resource)) {
                nonValueReferences.put(resguardMap.get(resource), result);
            } else {
                nonValueReferences.put(resource, result);
            }
        }
        // value类型文件所引用到的资源添加到 resourceRefSet 集合
        for (String resource : valuesReferences) {
            if (resguardMap.containsKey(resource)) {
                resourceRefSet.add(resguardMap.get(resource));
            } else {
                resourceRefSet.add(resource);
            }
        }

        // 注意此时 unusedResSet 包含了所有 定义了的资源
        // 对于配置了 ignore 的资源,添加到 resourceRefSet 集合,意味着它们是用户要保留的
        for (String resource : unusedResSet) {
            if (ignoreResource(resource)) {
                resourceRefSet.add(resource);
            }
        }
        // 对于 resourceRefSet 集合中的每个引用到的资源,进行递归遍历
        for (String resource : resourceRefSet) {
            readChildReference(resource);
        }
    }

这样,整个流程就到了末尾了,
回到 call 方法,

@Override
    public TaskResult call() throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            readMappingTxtFile(); // 解析资源混淆的 mapping文件
            readResourceTxtFile(); // 解析 R.txt 文件
            ResguardUtil.readResMappingTxtFile(resMappingTxt, null, resguardMap);
            unusedResSet.addAll(resourceDefMap.values());
            Log.i(TAG, "find resource declarations %d items.", unusedResSet.size());
            decodeCode(); // 解析java 文件中引用到的资源
            Log.i(TAG, "find resource references in classes: %d items.", resourceRefSet.size());
            decodeResources(); // 解析 xml (如layout)文件中引用到的资源
            Log.i(TAG, "find resource references %d items.", resourceRefSet.size());
            // 将 unusedResSet 去掉 resourceRefSet部分, 得到未使用的资源!
            unusedResSet.removeAll(resourceRefSet);
            Log.i(TAG, "find unused references %d items", unusedResSet.size());
            Log.d(TAG, "find unused references %s", unusedResSet.toString());
            JsonArray jsonArray = new JsonArray();
            for (String name : unusedResSet) {
                jsonArray.add(name);
            }
            ((TaskJsonResult) taskResult).add("unused-resources", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
    }

此时 resourceRefSet 集合保存了所有被引用到的资源
而 unusedResSet 保存了所有的资源
此时将 unusedResSet 去掉 resourceRefSet部分,就得到了未使用的资源了!

结尾

这只是 ApkChecker 的其中一个功能,其他的功能后续有机会就写!
有什么问题可以留言,欢迎指教!

你可能感兴趣的:(android,java,Matrix,Apkchecker,Andorid无用资源扫描)