title | layout |
---|---|
Patching Java bytecode |
default |
One of the challenges with Android Studio is that it isn't completely open-source. You can build most of the Android dependencies by following these steps, but there are internal libraries that are private to Google. You can recompile IntelliJ Community and the plugin, but you can also use this approach to patch existing JAR files. We used this way to verify quickly that a memory leak could be fixed!
You’ll want to use grep to find declarations of a class. Let’s for instance say we want to find the ProjectImportAction
class that gets used in Android Studio to sync the repository:
pushd "$HOME/Applications/MDX/Electric Eel/Android Studio Preview.app/Contents/plugins/gradle/lib"
grep ProjectImportAction *.jar
You may see:
Binary file gradle-tooling-extension-api.jar matches
If you want to be sure, install the zipgrep
tool (brew install zipgrep
)
zipgrep ProjectImportAction gradle-tooling-extension-api.jar
This result will show:
org/jetbrains/plugins/gradle/model/ProjectImportAction$1.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$2.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$3.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$4.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$5.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$6.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$7$1.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$7.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$8$1.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$8$2.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$8.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$AllModels.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$DefaultBuild$DefaultProjectModel.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$DefaultBuild.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$GradleBuildConsumer.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$ModelConverter.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$MyBuildController.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction$NoopConverter.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportAction.class:Binary file (standard input) matches
org/jetbrains/plugins/gradle/model/ProjectImportActionWithCustomSerializer$1.c
You can also install a GUI viewer to view the JAR file too:
brew install jadx
At the terminal, run jadx-gui gradle-tooling-extension-api.jar
to confirm the classes are located there.
Download Recaf. Make sure to download the JAR file that is labeled -with-dependencies.jar
.
Then run Recaf:
java -jar ~/Downloads/recaf-2.21.13-J8-jar-with-dependencies.jar
Before loading any file, we are going to need to make a few tweaks in the Config
.
Click on the Decompile
option and choose the Procyon
disassembler:
Next, uncheck the Generate missing classes
option. You will want to add libraries manually, especially for projects that depend on many other JAR files. With this option turns on, Recaf seems unable to resolve these missing classes.
Exit the Config menu by clicking on the Red icon:
Use the File
→ Load
to load the JAR file (e.g. gradle-tooling-extension-api.jar
).
Recaf depends on having all the classes necessary to recompile the JAR file. If you edit a class and hit Cmd-S
to save it, you will likely see errors at the bottom. If there are Cannot find symbol
errors and red lines on the import statements for the class file, it means that you may will need to use the Add library
feature:
To find any class references, you will likely need to use a combination of grep
and using zipgrep
/ jadx-gui
on individual files to confirm these are the right ones.
Once you add a library, you will see the top-left corner turns into a drop-down. Only the JAR labeled Primary
can be modified. For instance, here is an example of the dependent libraries for the gradle-tooling-extension-api.jar
that were needed to update the ProjectImportAction
class. There could be other JAR files depending on which class file you edit.
You can use the Export workspace
option to save this configuration. The contents will be saved as JSON and can be reloaded again by Recaf.
Once you’ve identified the changes you want to make, you may need to deal with casting issues by the decompiler:
Most of the issues can be solved by deleting the casting issues in question. The decompiler is not perfect, so using Recaf’s assembler mostly involves removing these errors. For instance, this line can be changed from:
addFetchedModelActions.addAll((Collection<? extends List<Runnable>>)controller.run((Collection<? extends BuildAction<?>>)buildActions));
To:
addFetchedModelActions.addAll(controller.run(buildActions));
The one exception is the ‘cannot find symbol class AllModels
class, which needs to be referenced as ProjectImportAction.AllModels
because it’s actually compiled in a different file.
Recaf doesn’t have a way to add Java classes, but you can do it pretty easily by creating the Java class and compiling it. We can simply create SimpleThreadFactory.java in a local editor. Note that we set the package to org.jetbrains.plugins.gradle.model
so we will need to take care of this case later.
package org.jetbrains.plugins.gradle.model;
import java.util.concurrent.ThreadFactory;
public final class SimpleThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
return new Thread(r, "idea-tooling-model-converter");
}
}
Then you can create the .class
file.
javac SimpleThreadFactory.java
The next step is to add it to the JAR we want to patch. Let’s make a backup just in case:
cp gradle-tooling-extension-api.jar gradle-tooling-extension-api.jar.orig
You can then add this SimpleThreadFactory.class
by relocating it in the right directory:
mkdir -p org/jetbrains/plugins/gradle/model
mv SimpleThreadFactory.class org/jetbrains/plugins/gradle/model
zip gradle-tooling-extension-api.jar org/jetbrains/plugins/gradle/model/SimpleThreadFactory.class
Then go ahead and do your patching work with this new added class!