Transform API — a real world example
Example of using the android TranformAPI for bytecode manipulation - grandcentrix/LogALot-TransformAPI-samplegithub.com
The Transform API is Android’s way to manipulate compiled class files before they are converted to dex files. A powerful tool for a lot of use cases.
The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1. (of the AndroidGradle plugin)
The API was introduced in version 1.5.0-beta1 of the Android Gradle plugin and makes it easy to manipulate class files before they end up in your app.
The API allows you to have multiple of such transformation steps in your builds without worrying about ordering. In fact you even don’t have control over the order of transforms.
There are some frameworks already making extensive use of it — even the infamous InstantRun uses it — but it’s not too complicated to get started and use it for your own needs.
The full implementation of what is discussed here can be found on GitHub.
What we are going to build here
It’s always a bit difficult to find something that is not too complex to serve as an example and not too simple to not leave out the important details. Luckily my co-worker passsy came up with a good idea.
What we basically want to build here is
- have an annotation you can add to fields and methods
- whenever such an annotation is found we want to emit logs for method entering/exit and field access
- it should be configurable for which variants you want to have it
In the end it will look like this
The first snippet shows how it looks in your code. The second snippet shows what the transform will create for you.
First things first
Obviously we need to tell the Android Gradle plugin about our transform implementation.
In Groovy it looks like
Since we are going to use Kotlin it looks a bit different
val android = target.extensions.findByName("android") as BaseExtension
You just pass an instance of your subclass of
Transform and your code will run for every build.
That’s what happens in our plugin’s
apply function. In the example there is additional code to check that the plugin is actually used in an Android build and also our extension is registered.
The only purpose of the extension is to have a way to configure for which variants you want the transform to be performed.
The Transform Implementation
Transform is an abstract class with some abstract methods you need to implement and some non-abstract methods you might want to override.
getNamethis is the unique name of your transform
isIncrementalis a way to tell the Android Gradle plugin if we support incremental work
getInputTypesindicates which type of input you are interested in — basically this can be CLASSES or (Java) RESOURCES or both
getScopeshere we return the scopes we consume — there are a couple of possible scopes, e.g. PROJECT which is the actual code contained in the project, SUB_PROJECTS which are other modules from the project’s dependencies and EXTERNAL_LIBRARIES which are external dependencies
getReferencedScopeswhile it’s not abstract this can be useful: here you can state which scopes you need access to but you don’t want to touch
transformthis is where the heavy lifting happens — the actual transformation of the byte code
There are some more methods you can implement depending on your needs. Just have a look at the API docs.
Modifying Byte Code
Let’s have a look at the transform method now.
Looking at the non-deprecated method version’s signature you can see that you get an instance of TransformInvocation as the only parameter.
The TransformInvocation holds — beside other things — a context. We need it in our example to get the current variant that is building to check if we should apply the transform or not.
⚠️ Important: Even if we don’t want to transform anything we need to copy over all the inputs — otherwise they wouldn’t end up in the final APK.
You get the inputs to your transform via TransformInvocation#getInputs
An input is mostly composed of a list of JarInput and a list of DirectoryInput.
In our example — since we only consume the PROJECT scope — we just expect DirectoryInputs.
To actually modify the classes you can use whatever library you want — or do it completely on your own. In the end you have to provide the classes in a location you request via the OutputProvider which you can get from the TransformInvocation.
It is best practice to write into as many outputs as Jar/Folder Inputs have been received by the transform. Combining all the inputs into a single output prevents downstream transform from processing limited scopes.
In our case I use Javassist to do the byte code manipulation. It’s really high level and easy to use.
Since Javassist needs all referenced classes on it’s “own classpath” (called ClassPool) we have to add all the external libraries and sub-projects to it.
As said before I request those dependencies via referenced scopes since otherwise we would need to copy those dependencies.
One special thing here is that the android.jar is not available via the given inputs. We create the path to it on our own by getting the sdkDirectory and the compileSdkVersion from the Android Gradle plugin.
Transforming the code is really easy thanks to Javassist. Only the heavy usage of ‘$’ in strings you need to pass to it makes it a bit ugly to use from Kotlin.
This is how it looks like in our sample:
What I did is using ‘@’ instead of ‘$’ and having an extension function for
String to replace it before passing the string to Javassist.
Inspect what the transform does
When you checked out the sample you can import it into Android Studio and just launch the “InternalDebug” variant.
Look at logcat after launching the app and see what happens when you tap the button of the sample app. Compare the output to the annotated members in MainActivity.kt
In your IDE navigate to app/build/intermediates/transforms/LogALot/internal/debug/0
That’s where our changed class files are.
Locate the MainActivity and double click.
Now press CMD+A (or CTRL+A depending on your platform) and type Decompile Kotlin to Java and press RETURN.
It might look a bit unfamiliar at first but when looking closer you will recognize an ugly version of MainActivity.kt in Java. The interesting thing here is the code inserted by our transform.
The logging of method entry, exit and eventual thrown exceptions.
doSomething method you can also spot the logging of the the field writes and reads.
Now you learned an simple and easy way to check what you transform actually did to your code. Very useful when developing your own transform.
In order to register the transform I created my own plugin. I implemented it in the buildSrc directory. That’s convenient since the sample app living in the same multi-module build can “just use it”.
That convenience comes with a few minor problems.
- we have a runtime module (doing the actual logging) and an annotation module as separate modules in the same project — but you cannot have a dependency from buildSrc to regular modules in that same project
- we have tests in buildSrc to test the plugin and the transform — the downside is that the Gradle sync in the IDE won’t be successful if you have failing tests (and you have to fix those tests with limited IDE support then)
Maybe converting it to a composite build instead of using buildSrc could help but that’s a different story.
The transform isn’t incremental in our sample since doing it right for this use case wouldn’t be easy (at least not easy enough for something that is meant to be an example): You could add a field annotation and that changed class would get into the transform as a changed file. However since we only can rewrite the field access (which could be in any class file) we would have to touch every class anyway.
⚠️ During development I encountered a problem which really took some time to sort out: Sometimes the plugin didn’t pick up changes in the runtime library and things started to get very wrong if methods were added/removed or method signatures changed.
In the end it turned out that the Gradle daemon was causing this. So just killing the daemon(s) via
gradle --stop or just using the command line with an additional
--no-daemon switch is an easy way to get rid of that.
Being able to tinker with the byte code gives you a lot of power and opportunities — thanks to the Transform API it’s integrated into your builds in an easy and safe way. It’s probably not something every developer should do on a daily basis but it is good to know how and what you can do.
There are a lot more little things to discover in the code — just check out the code and try it out on your own.