As the feed

AGP(Android Gradle Plugin) 7.0.0: Transform has expired and will soon be obsolete. Besides, it also briefly introduced the Transform Action as the replacement method. After a period of study and research, I found that only half of the answer was correct. I’m going to introduce you to something new: AsmClassVisitorFactory.

com.android.build.api.instrumentation.AsmClassVisitorFactory

A factory to create class visitor objects to instrument classes.

The implementation of this interface must be an abstract class where the parameters and instrumentationContext are left unimplemented. The class must have an empty constructor which will be used to construct the factory object.

The underlying implementation of this class is based on gradle’s native Transform Action. In fact, this learning process took a bit of a dedeway. At the beginning, we tried to use the Transform Action, but it seemed to be too convoluted and failed. Since the input output of the Transform Action is a single file and the changes are made to a single file, it doesn’t seem like a completely good alternative to the complex ASM operations described in the previous article.

AsmClassVisitorFactory According to the official statement, the compilation speed will be improved by about 18%, which we will introduce in the use phase below.

Learn the waste

Let’s start with the abstract interface AsmClassVisitorFactory.

AsmClassVisitorFactory

@Incubating
interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> : Serializable {

    /** * The parameters that will be instantiated, configured using the given config when registering * the visitor, and injected on instantiation. * * This field must be left unimplemented. */
    @get:Nested
    val parameters: Property<ParametersT>

    /** * Contains parameters to help instantiate the visitor objects. * * This field must be left unimplemented. */
    @get:Nested
    val instrumentationContext: InstrumentationContext

    /** * Creates a class visitor object that will visit a class with the given [classContext]. The * returned class visitor  must delegate its calls to [nextClassVisitor]. * * The given [classContext] contains static information about the classes before starting the * instrumentation process. Any changes in interfaces or superclasses for the class with the * given [classContext] or for any other class in its classpath by a previous visitor will * not be reflected in the [classContext] object. * * [classContext] can also be used to get the data for classes that are in the runtime classpath  * of the class being visited. * * This method must handle asynchronous calls. * *@param classContext contains information about the class that will be instrumented by the
     *                     returned class visitor.
     * @param nextClassVisitor the [ClassVisitor] to which the created [ClassVisitor] must delegate
     *                         method calls.
     */
    fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor

    /** * Whether or not the factory wants to instrument the class with the given [classData]. * * If returned true, [createClassVisitor] will be called and the returned class visitor will * visit the class. * * This method must handle asynchronous calls. */
    fun isInstrumentable(classData: ClassData): Boolean
}
Copy the code

So just to do a quick analysis of this interface, what we’re going to do is we’re going to return a ClassVisitor in this createClassVisitor method, and normally we’re going to create a ClassVisitor instance and we’re going to pass in the next ClassVisitor instance, So we’ll just pass in the nextClassVisitor later on new.

The isInstrumentable method is used to determine whether the current class needs to be scanned. If all classes need to be scanned through the ClassVisitor, it would still be too long. We can filter out many classes that we don’t need to scan.

@Incubating
interface ClassData {
    /** * Fully qualified name of the class. */
    val className: String

    /** * List of the annotations the class has. */
    val classAnnotations: List<String>

    /** * List of all the interfaces that this class or a superclass of this class implements. */
    val interfaces: List<String>

    /** * List of all the super classes that this class or a super class of this class extends. */
    val superClasses: List<String>
}
Copy the code

ClassData is not an ASM API, so it contains relatively little content, but it should be enough. This part we will have a brief look on the line, not to do more introduction.

The new Extension

AGP version after upgrading, should be to distinguish between the new Extension of the old version, so in AppExtension, on the basis of a new AndroidComponentsExtension.

So our transformClassesWith needs to be registered on this. This needs to take into account the variation, which is quite different from the previous Transform, so that we can increase the corresponding adaptation work based on different variations.

        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            variant.transformClassesWith(PrivacyClassVisitorFactory::class.java,
                    InstrumentationScope.ALL) {}
            variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
        }
Copy the code

In actual combat

This time, the bytecode replacement tool was developed based on the previous sensitive permission API replacement.

ClassVisitor

Take a look at how we normally write a simple ClassVisitor.

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor methodFilterCV = new ClassFilterVisitor(classWriter);
ClassReader cr = new ClassReader(srcClass);
cr.accept(methodFilterCV, ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();
Copy the code

First we construct an empty ClassWriter, then we construct a ClassVisitor instance, and pass in the ClassWriter. Then we construct a ClassReader instance, pass in the Byte array, call classReader.Accept, and then we can visit the data one by one in the visitor.

So all of our class information, all of our methods, all of our stuff is read in through the ClassReader, and then it’s accessed by the current ClassVisitor and then it’s handed to us as the last ClassWriter.

The ClassWriter is also a ClassVisitor object that complex reconverts modified classes into byte data. You can see that the ClassVisitor has a very simple list structure that goes down level by level.

If we insert several different classVisitors before the ClassVisitor list, we can make the ASM changes work one by one, and we don’t need any more IO operations. That’s the idea behind the new ASM API, and the bytecode framework. The same is also true of design ideas in Bytex.

Tips ClassNode is a little different from the normal ClassVisitor. You need to call accept(Next) inside the visitEnd method.

Actual code analysis

Let’s move on to the real thing. I’ll apply the previous code to this logic.

The demo address

abstract class PrivacyClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {

    override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
        return PrivacyClassNode(nextClassVisitor)
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }

}
Copy the code

IsInstrumentable returns true, but I can limit the scan rule to a specific package name to speed up the build.

class PrivacyClassNode(private val nextVisitor: ClassVisitor) : ClassNode(Opcodes.ASM5) { override fun visitEnd() { super.visitEnd() PrivacyHelper.whiteList.let { val result = it.firstOrNull { whiteName -> name.contains(whiteName, true) } result }.apply { if (this == null) { // println("filter: $name") } } PrivacyHelper.whiteList.firstOrNull { name.contains(it, true) }? .apply { val iterator: Iterator<MethodNode> = methods.iterator() while (iterator.hasNext()) { val method = iterator.next() method.instructions? .iterator()? .forEach { if (it is MethodInsnNode) { it.isPrivacy()? .apply { println("privacy transform classNodeName: ${name@this}") it.opcode = code it.owner = owner it.name = name it.desc = desc } } } } } accept(nextVisitor) } } private  fun MethodInsnNode.isPrivacy(): PrivacyAsmEntity? { val pair = PrivacyHelper.privacyList.firstOrNull { val first = it.first first.owner == owner && first.code == opcode && first.name == name && first.desc == desc } return pair?.second }Copy the code

This is the easy part. You abstract the logic in the ClassNode class, and then call the Accept (nextVisitor) method I talked about earlier in the visitEnd method.

Another is the registration logic, and I introduced in front of the content is basically the same.

My point of view

AsmClassVisitorFactory is much simpler than Transform. We don’t need to worry about incremental updates and so on. We just need to focus on the ASM API.

The second is because of the reduction of IO operations, so the speed is naturally higher than before. The overall performance is fine because it’s based on the Transform Action, and that part of the increment is easier.

In addition, I have also communicated with my colleague, oh, it is better to use Gradle Task to modify the complexity similar to that introduced in the previous article.

Finally, thanks to 2BAB and Senge, many contents of the article refer to the articles of several leaders.

Finally, I plan to fly myself, the article word count casually, do not always stare at the article length problem.