Matrix ApkChecker 中 Unused Resources功能用于扫描 apk 中无用的资源(包括drawable、layout、value类型资源等)。由于工作中使用到了这个功能,且对其实现感兴趣,特此记录~
例子:
先写一个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 文件。
Android Studio 构建完成后,会在 build目录下生成一个 R.txt 文件,这个文件记录 apk 所有的<资源:资源ID>映射,包括 application、library、第三方包中的资源。
拿上面代码中的 “github” 图片为例,它在 R.txt 文件中也有相应的 ID映射,
如下图所示R.txt 文件:
resources.arsc 文件是将 apk 文件解压后的产物之一。其包含了 apk 中所有资源的相关信息。
拿上面 github 图片举例,其在 resources.arsc 如下所示:
可以看到,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
我们将上述的 apk 进行反编译,看看 MainActivity 中是如何获取资源的
反编译后的到的目录如下:
可以看到,反编译后,得到了 smali 代码,我们找到 MainActivity 的 smali 文件:
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源码是怎么实现的吧!
由于 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)文件中引用到的资源
我们逐步看一下
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
该集合用于后面使用
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 文件,并把所有的
0x7f07006d:R.drawable.github
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集合中
其他情况类似~
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 的其中一个功能,其他的功能后续有机会就写!
有什么问题可以留言,欢迎指教!