Previously, I was reading an article about automatic garbage removal from Android Projects. This article is very old and of little value now that automatic garbage removal is integrated into Android Studio.

The main content of this article is as follows:

This function in order to realize the function should be like this: 1, read androidunusedresources. Jar exported useless resource list. 2. Clear unnecessary resources, including deleting unnecessary files and modifying files containing unnecessary resources.Copy the code

. The first step in using AndroidUnusedresource jar to export the list of useless, there is a problem, this article is mainly about how to transform AndroidUnusedResources. The jar.

Go to the website to download AndroidUnusedResources. Jar

Will download the AndroidUnusedResources1.6.2. Jar file placed project directory (for example/Document/MyApplication), and then perform the following statement:

Java - jar AndroidUnusedResources. 1.6.2. JarCopy the code

The following error occurs

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

Error: Current directory is not a standard Android project root directory… Take a look at the code for this JAR.

The directory is simple, and the entry class is the Loader class.

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

Take a look at the ResourceScanner class. Most of the logic is in this, including how to find the resource and check if the resource is found.

FindPaths is executed in the run function. In this function, you can find the srcDirectory, resDirectory, manifestFile, genDirectory, and rJavaFile files. SRC, RES, and MANIFEST files were not found.

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++; }}Copy the code

If you look at the logic in the findPaths function, you will find that you are looking for these files using the same old project directory logic that Android used a few years ago when we were using Eclipse. But now that we’re using Android Studio, the project directory structure is as follows:

MyApplication - app (moduleName) - SRC -- -- -- -- -- - the main Java -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- res -- -- -- -- -- -- -- -- AndroidManifest. The XMLCopy the code

The first step is to modify the findPaths function to find the correct file.

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; }Copy the code

Here, the srcDirectory, resDirectory, manifestFile, rJava files are all found. In old Versions of Android, all resources declared in the current project generate an R.java (in the build/generated/source/ directory). In new versions, r.java is no longer found. Alternative/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/R.j ar.

Once the file is found, go back to the run function and execute to this line:

this.mResources.addAll(getResourceList(this.mRJavaFile, mPackageName));
Copy the code
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; }Copy the code

Look at this method in light of the R.java file structure. The logic of this method is to read the original r.java file line by line, then use the re match to find the resource type (attr, color, dimen, String, ID, etc.), then find the resource name, and then use these two parameters to create a Resource object to add to the mResources.

Now the tricky thing is, there is no R.Java, only R.Jar. Let’s see what we found in R.Jar.

In this R.jar, all the resources of the project are entered, Quite so com. Example. Myapplication. R.c lass = com. Google. Android. Material. R.c lass + androidx. * R.c lass + own engineering R.c lass (target).

The generated r.java project contains only the resources declared in our own project, and does not contain the resources of the dependent library. So what I did was I read myApplication.r.class and put it in set. Then I subtracted resources from other libraries to get resources from my project.

How do you do that?

  1. Read the contents of the JAR file, loop through the contents of the JAR package, and fetch each R.class file.
  2. Decompile R.class, the class file corresponding to the contents of the original R.java.
  3. Read the newly decomcompiled r.java line by line, and then continue to follow the original re matching logic in AndroidUnusedResources.

Parse each of these steps separately.

In the first step, through JarFile read R.j ar, get the JarEntry is not the way we see through the jd – GUI, for example, like com. Example. Under the myapplication R.c lass, we can read: Com/Google/android/material/R.c lass, com/Google/android/material/R $attr. Class, com/Google/android/material/R $color. Class, etc. And r.class is empty, only in R$xxx.class. Public static final class XXX {public static final class XXX {R$XXX. Class {R$XXX. Class {R$XXX.

If R$xxx.class is available, how to decompile R$xxx.class Jd-gui is associated with a library called JD-core, which provides an API. If you provide an inputStream of a class file, you can get a String of the contents of the class file.

Third, with regular match after he got the resource, need to distinguish between com/example/myapplication under total resources and the resources of the library, and then use collection subtraction, get our ultimate goal.

Take a look at all the changed code:

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(); }Copy the code

After modifying the code, run our code in a test Android project and get the following 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
Copy the code

You did find two useless resources.

Finally is the package, the code after we change, into a JAR package to use. I am in a demo of the android project, has built a Java library, the original AndroidUnusedResources. The content of the jar after decompiling copied into, and then change in it.

Gradle is a jar task that needs to be configured in the Java library build.gradle.

plugins { id 'java-library' } java { sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = VERSION_1_7} // Configure the contents jar{// configure the jar to wrap the list properties, Meaning to execute Java jar androidUnusedResources. The jar manifest the main class of execution. The attributes ( 'Main - Class' : 'ca. Skennedy. Androidunusedresources. Loader') / / will need to rely on in the project of also in the package, otherwise the runtime, 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'])}Copy the code

Then double-click to execute the JAR task:

A JAR package is generated under build/libs/ of the Java Library. Put the jar package myapplication directory, and then execute the Java – jar androidunusedresources. Jar will get results.

You can go to CSDN on search androidunusedresources jar, I have to upload the generated jar package.