AndroidUnusedResources.jar改造

673 阅读5分钟

前情提要是我在看一篇文章:自动清理android项目无用资源 这篇文章很老,且也没有什么价值了,因为现在Android Studio中已经集成了自动清理无用资源的功能。

这文章里主要内容如下:

这个功能要实现的功能应该是这样的:
1、读取androidunusedresources.jar导出的无用资源列表。
2、清理无用的资源,包括删除无用文件以及修改包含无用资源的文件。

第一步使用AndroidUnusedresource.jar去导出无用列表时,出现问题,本篇文章主要是讲如何改造AndroidUnusedResources.jar。

先去官网下载AndroidUnusedResources.jar

将下载到的AndroidUnusedResources1.6.2.jar文件放置工程目录下(例如/Document/MyApplication),然后执行以下语句:

java -jar AndroidUnusedResources.1.6.2.jar

出现如下错误

Running in: /Users/xuanxuan/AndroidStudioProjects/MyApplication
The current directory is not a valid Android project root.

错误提示,当前目录不是一个标准的Android工程根目录。。。看下这个jar的代码。

截图1.png

目录很简单,入口类是Loader类。

public class Loader {
  public static void main(String[] args) {
    ResourceScanner resourceScanner = new ResourceScanner();
    resourceScanner.run();
  }
}

看下ResourceScanner类,大部分逻辑都在这个里面,包括如何找到资源,并且检查该资源是否被找到。

截图2.png

从图中可以看到在run函数中,先执行findPaths,在该函数中,找到srcDirectory、resDirectory、manifestFile、genDirectory、rJavaFile这几个文件。但是根据刚刚出错信息,可以看到在检查src、res和manifest三个文件时就已经报错了,因为没找到。

private void findPaths() {
    File[] children = this.mBaseDirectory.listFiles();
    if (children == null)
      return; 
    byte b;
    int i;
    File[] arrayOfFile1;
    for (i = (arrayOfFile1 = children).length, b = 0; b < i; ) {
      File file = arrayOfFile1[b];
      if (file.isDirectory()) {
        if (file.getName().equals("src")) {
          this.mSrcDirectory = file;
        } else if (file.getName().equals("res")) {
          this.mResDirectory = file;
        } else if (file.getName().equals("gen")) {
          this.mGenDirectory = file;
        } 
      } else if (file.getName().equals("AndroidManifest.xml")) {
        this.mManifestFile = file;
      } 
      b++;
    } 
  }

看findPaths函数里的逻辑,发现这里找这几个文件,还是按照Android前几年的旧工程目录逻辑去找,那时候我们还在用eclipse。但是我们现在使用Android Studio,工程目录结构都是如下:

MyApplication
--app(moduleName)
----src
------main
--------java
--------res
--------AndroidManifest.xml

第一步先修改findPaths函数,让其能找到正确的文件。

//改版后的代码
private void findPaths() {
        File src = FileUtilities.getFile(mBaseDirectory, "src");
        if (src == null) {
            return;
        }
        File main = FileUtilities.getFile(src, "main");
        if (main == null) {
            return;
        }
        this.mSrcDirectory = FileUtilities.getFile(main, "java");
        this.mResDirectory = FileUtilities.getFile(main, "res");
        this.mManifestFile = FileUtilities.getFile(main, "AndroidManifest.xml");

        File rJarFile = new File(mBaseDirectory.getAbsolutePath() + "/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/R.jar");
        if (rJarFile != null && rJarFile.exists()) {
            this.mRJavaFile = rJarFile;
        }
    }
    
public static File getFile(File parent, String fileName) {
        File[] children = parent.listFiles();
        if (children == null) {
            return null;
        }
        for (File f : children) {
            if (f.getName().equals(fileName)) {
                return f;
            }
        }
        return null;
    }

到这里,srcDirectory、resDirectory、manifestFile、rJava文件都找到了。 这里需要说明的是,在旧版Android里,当前工程里声明的资源都会生成一个R.java(在build/generated/source/目录下),但新版更新后,发现再也找不到R.java了。替代的是/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/R.jar。

找到文件后,重新回到run函数中,执行到该行:

this.mResources.addAll(getResourceList(this.mRJavaFile, mPackageName));
private static final Pattern sResourceTypePattern = Pattern.compile("^\\s*public static final class (\\w+)\\s*\\{$");
  
private static final Pattern sResourceNamePattern = Pattern.compile("^\\s*public static( final)? int(\\[\\])? (\\w+)\\s*=\\s*(\\{|(0x)?[0-9A-Fa-f]+;)\\s*$");
  
  private static Set<Resource> getResourceList(File rJavaFile) throws IOException {
    InputStream inputStream = new FileInputStream(rJavaFile);
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    boolean done = false;
    Set<Resource> resources = new HashSet<Resource>();
    String type = "";
    while (!done) {
      String line = reader.readLine();
      done = (line == null);
      if (line != null) {
        Matcher typeMatcher = sResourceTypePattern.matcher(line);
        Matcher nameMatcher = sResourceNamePattern.matcher(line);
        if (nameMatcher.find()) {
          resources.add(new Resource(type, nameMatcher.group(3)));
          continue;
        } 
        if (typeMatcher.find())
          type = typeMatcher.group(1); 
      } 
    } 
    reader.close();
    inputStream.close();
    return resources;
  }

截图3.png 结合R.java文件结构来看这个方法。这个方法的逻辑就是按行读取原来的R.java文件,然后用正则匹配,先找到资源类型(attr、color、dimen、string、id等),再找资源名字,然后用这两个参数创建resource对象加入到mResources中。

现在棘手的是,没有R.java,只有R.jar。看下我们找出的R.jar中都有什么。

截图4.png

这个R.jar中,将这个工程所有的资源都打进去了,相当于是com.example.myapplication.R.class = com.google.android.material.R.class + androidx.*.R.class + 自己工程的R.class(目标内容)。

原来工程里生成的R.java,就只有我们自己工程里声明的resource,而不包含依赖的library的resource。所以我的做法就是读取myapplication.R.class里的资源放入set中,然后减去其他library的资源,就能拿到自己工程里的resource。

如何做呢?

  1. 读取jar文件内容,循环遍历jar包内容,拿到每个R.class文件。
  2. 反编译R.class,该class文件对应的原始的R.java的内容。
  3. 逐行读取刚刚反编译后的R.java,然后继续走原来AndroidUnusedResources里正则匹配的逻辑即可。

分别解析上述的每一步。

在第一步中,通过JarFile读取R.jar,拿到的JarEntry其实并不是我们通过jd-gui中看到的那个样子,举个例子,像com.example.myapplication下的R.class,我们能读取到的是:com/google/android/material/R.classcom/google/android/material/R$attr.classcom/google/android/material/R$color.class等。而且R.class内容为空,只有R$xxx.class中才有内容。而且读取R$xxx.class时,其读到的类是public final class xxx{,而不是我们正则匹配里的public static final class xxx{,所以这里我们需要修改我们的正则匹配规则。

第二步,拿到R$xxx.class后,如何反编译,我们只知道有jd-gui可视化工具去反编译,那在代码里如何反编译呢?jd-gui相关的有一个库jd-core,里面提供了api,只要提供一个class文件的inputStream,就可以得到这个class文件内容的String。

第三部,是用正则匹配后拿到的resource后,需要区分出com/example/myapplication下的总资源和library的资源,然后用集合减法,就能拿到我们最终目标。

看下全部改动代码:

private static Set<Resource> getResourceList(File rJavaFile, String packageName) throws IOException {
        Set<Resource> myAppResource = new HashSet<>();
        Set<Resource> otherLibraryResource = new HashSet<>() ;
        try {
            JarFile jarFile = new JarFile(rJavaFile);
            String newPackageName = packageName.replace('.', '/');
            Enumeration entries = jarFile.entries();
            JarEntry entry;
            String entryName;

            while (entries.hasMoreElements()) {
                entry = (JarEntry) entries.nextElement();
                entryName = entry.getName();
                System.out.println(entryName);
                if (entryName.endsWith("R.class")) {
                    continue;
                }
                if (entryName.startsWith(newPackageName)) {
                    myAppResource.addAll(RJavaUtil.getResourceList(jarFile, entry));
                } else {
                    otherLibraryResource.addAll(RJavaUtil.getResourceList(jarFile, entry));
                }
            }
        } catch (Exception e) {
            System.out.println(e.getStackTrace());
        }
        myAppResource.removeAll(otherLibraryResource);
        return myAppResource;
    }
    
    public static Set<Resource> getResourceList(JarFile jarFile, JarEntry entry) {
        Set<Resource> resources = new HashSet<>();
        try {
            InputStream inputStream = jarFile.getInputStream(entry);
            String[] lines = decompile(inputStream).split(NEWLINE);
            String type = "";
            for (String line : lines) {
                if (line != null) {
                    Matcher typeMatcher = sResourceTypePattern.matcher(line);
                    Matcher nameMatcher = sResourceNamePattern.matcher(line);
                    if (nameMatcher.find() && !type.equals("id")) {
                        resources.add(new Resource(type, nameMatcher.group(3)));
                        continue;
                    }
                    if (typeMatcher.find())
                        type = typeMatcher.group(1);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return resources;
    }

    public static String decompile(final InputStream is){
        ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();
        Loader loader = new Loader() {
            @Override
            public byte[] load(String internalName) throws LoaderException {
                try {
                    if (is == null) {
                        return null;
                    } else {
                        try (InputStream in = is; ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                            byte[] buffer = new byte[1024];
                            int read = in.read(buffer);

                            while (read > 0) {
                                out.write(buffer, 0, read);
                                read = in.read(buffer);
                            }

                            byte[] result = out.toByteArray();
                            out.close();
                            in.close();
                            return result;
                        } catch (IOException e) {
                            throw new LoaderException(e);
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            public boolean canLoad(String internalName) {
                return true;
            }
        };

        Printer printer = new Printer() {

            protected int indentationCount = 0;
            protected StringBuilder sb = new StringBuilder();

            @Override public String toString() { return sb.toString(); }

            @Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {}
            @Override public void end() {}

            @Override public void printText(String text) { sb.append(text); }
            @Override public void printNumericConstant(String constant) { sb.append(constant); }
            @Override public void printStringConstant(String constant, String ownerInternalName) { sb.append(constant); }
            @Override public void printKeyword(String keyword) { sb.append(keyword); }
            @Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { sb.append(name); }
            @Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { sb.append(name); }

            @Override public void indent() { this.indentationCount++; }
            @Override public void unindent() { this.indentationCount--; }

            @Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) sb.append(TAB); }
            @Override public void endLine() { sb.append(NEWLINE); }
            @Override public void extraLine(int count) { while (count-- > 0) sb.append(NEWLINE); }

            @Override public void startMarker(int type) {}
            @Override public void endMarker(int type) {}
        };

        try {
            decompiler.decompile(loader, printer, "");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return printer.toString();
    }

改完代码后,在一个测试android工程中运行下我们的代码,得到input如下:

Running in: /Users/xuanxuan3/AndroidStudioProjects/MyApplication/app
18 resources found

Not generating usage matrices. If you would like them, create a directory named 'resource-matrices' in the base of your project.

2 unused resources were found:
layout    : layout_unused
    /Users/xuanxuan3/AndroidStudioProjects/MyApplication/app/src/main/res/layout/layout_unused.xml
string    : unused_str
    /Users/xuanxuan3/AndroidStudioProjects/MyApplication/app/src/main/res/values/strings.xml

的确是找到了两个无用资源。

最后就是打包了,把我们改动后的代码,打成一个jar包使用。我是在一个demo的android工程里,新建了一个java library,将原来AndroidUnusedResources.jar内容反编译后拷贝进去,然后在里面改。

改完之后,通过gradle中的一个jar task来打包,首先需要在这个java library的build.gradle中进行配置。

plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

//主要是配置这个内容
jar{
    //配置这个jar包住清单属性,也就是执行java -jar androidUnusedResources.jar执行的main类
    manifest.attributes(
            'Main-Class':'ca.skennedy.androidunusedresources.Loader'
    )
    //需要将工程里依赖的包也打进去,要不然运行时,会抱找不到jd-core下类的错误
    from(project.zipTree('libs/jd-core-1.1.3.jar'))
    from('build/classes/java/main/')
}

dependencies {
//    compile files('libs/jd-core-1.1.3.jar')
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

然后双击执行jar任务:

截图6.png

就会在java library的build/libs/下生成一个jar包。将这个jar包放在myapplication目录下,然后执行java -jar androidunusedresources.jar就会得到结果。

大家可以去csdn上搜索androidunusedresources.jar,我生成的jar包已经上传。