forked from square/dagger
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate HiltGradlePlugin to use AGP's new ASM pipeline for bytecode t…
…ransformation. Updates the HiltGradlePlugin to use AGP's new ASM pipeline if the developer is on AGP 4.2.0+, otherwise the plugin will use the older transform APIs. Using the new APIs allows for the plugin to not need a custom task to transform local tests as this is now supported with the new pipeline. If the developer is using AGP 4.2.0 then having 'enableTransformForLocalTests' is no longer needed and a warning will be shown with the hopes of migrating user away and once 4.2 is stable completely removing and deprecating the option. AGP's new transform pipeline is available in AGP 4.2.0 which is currently in beta. Fixes: #2118 RELNOTES=Use AGP's new transform pipeline when developer is on AGP 4.2.0+. PiperOrigin-RevId: 348818076
- Loading branch information
1 parent
bf18e2f
commit d69b00f
Showing
19 changed files
with
417 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
...ndroid/plugin/src/main/kotlin/dagger/hilt/android/plugin/AndroidEntryPointClassVisitor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
package dagger.hilt.android.plugin | ||
|
||
import com.android.build.api.instrumentation.AsmClassVisitorFactory | ||
import com.android.build.api.instrumentation.ClassContext | ||
import com.android.build.api.instrumentation.ClassData | ||
import com.android.build.api.instrumentation.InstrumentationParameters | ||
import java.io.File | ||
import org.gradle.api.provider.Property | ||
import org.gradle.api.tasks.Input | ||
import org.objectweb.asm.ClassReader | ||
import org.objectweb.asm.ClassVisitor | ||
import org.objectweb.asm.FieldVisitor | ||
import org.objectweb.asm.MethodVisitor | ||
import org.objectweb.asm.Opcodes | ||
|
||
/** | ||
* ASM Adapter that transforms @AndroidEntryPoint-annotated classes to extend the Hilt | ||
* generated android class, including the @HiltAndroidApp application class. | ||
*/ | ||
@Suppress("UnstableApiUsage") | ||
class AndroidEntryPointClassVisitor( | ||
private val apiVersion: Int, | ||
nextClassVisitor: ClassVisitor, | ||
private val additionalClasses: File | ||
) : ClassVisitor(Opcodes.ASM8, nextClassVisitor) { | ||
|
||
interface AndroidEntryPointParams : InstrumentationParameters { | ||
@get:Input | ||
val additionalClassesDir: Property<File> | ||
} | ||
|
||
abstract class Factory : AsmClassVisitorFactory<AndroidEntryPointParams> { | ||
override fun createClassVisitor( | ||
classContext: ClassContext, | ||
nextClassVisitor: ClassVisitor | ||
): ClassVisitor { | ||
return AndroidEntryPointClassVisitor( | ||
apiVersion = instrumentationContext.apiVersion.get(), | ||
nextClassVisitor = nextClassVisitor, | ||
additionalClasses = parameters.get().additionalClassesDir.get() | ||
) | ||
} | ||
|
||
/** | ||
* Check if a class should be transformed. | ||
* | ||
* Only classes that are an Android entry point should be transformed. | ||
*/ | ||
override fun isInstrumentable(classData: ClassData) = | ||
classData.classAnnotations.any { ANDROID_ENTRY_POINT_ANNOTATIONS.contains(it) } | ||
} | ||
|
||
// The name of the Hilt generated superclass in it internal form. | ||
// e.g. "my/package/Hilt_MyActivity" | ||
lateinit var newSuperclassName: String | ||
|
||
lateinit var oldSuperclassName: String | ||
|
||
override fun visit( | ||
version: Int, | ||
access: Int, | ||
name: String, | ||
signature: String?, | ||
superName: String?, | ||
interfaces: Array<out String>? | ||
) { | ||
val packageName = name.substringBeforeLast('/') | ||
val className = name.substringAfterLast('/') | ||
newSuperclassName = | ||
packageName + "/Hilt_" + className.replace("$", "_") | ||
oldSuperclassName = superName ?: error { "Superclass of $name is null!" } | ||
super.visit(version, access, name, signature, newSuperclassName, interfaces) | ||
} | ||
|
||
override fun visitMethod( | ||
access: Int, | ||
name: String, | ||
descriptor: String, | ||
signature: String?, | ||
exceptions: Array<out String>? | ||
): MethodVisitor { | ||
val nextMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) | ||
val invokeSpecialVisitor = InvokeSpecialAdapter(apiVersion, nextMethodVisitor) | ||
if (name == ON_RECEIVE_METHOD_NAME && | ||
descriptor == ON_RECEIVE_METHOD_DESCRIPTOR && | ||
hasOnReceiveBytecodeInjectionMarker() | ||
) { | ||
return OnReceiveAdapter(apiVersion, invokeSpecialVisitor) | ||
} | ||
return invokeSpecialVisitor | ||
} | ||
|
||
/** | ||
* Adapter for super calls (e.g. super.onCreate()) that rewrites the owner reference of the | ||
* invokespecial instruction to use the new superclass. | ||
* | ||
* The invokespecial instruction is emitted for code that between other things also invokes a | ||
* method of a superclass of the current class. The opcode invokespecial takes two operands, each | ||
* of 8 bit, that together represent an address in the constant pool to a method reference. The | ||
* method reference is computed at compile-time by looking the direct superclass declaration, but | ||
* at runtime the code behaves like invokevirtual, where as the actual method invoked is looked up | ||
* based on the class hierarchy. | ||
* | ||
* However, it has been observed that on APIs 19 to 22 the Android Runtime (ART) jumps over the | ||
* direct superclass and into the method reference class, causing unexpected behaviours. | ||
* Therefore, this method performs the additional transformation to rewrite direct super call | ||
* invocations to use a method reference whose class in the pool is the new superclass. Note that | ||
* this is not necessary for constructor calls since the Javassist library takes care of those. | ||
* | ||
* @see: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial | ||
* @see: https://source.android.com/devices/tech/dalvik/dalvik-bytecode | ||
*/ | ||
inner class InvokeSpecialAdapter( | ||
apiVersion: Int, | ||
nextClassVisitor: MethodVisitor | ||
) : MethodVisitor(apiVersion, nextClassVisitor) { | ||
override fun visitMethodInsn( | ||
opcode: Int, | ||
owner: String, | ||
name: String, | ||
descriptor: String, | ||
isInterface: Boolean | ||
) { | ||
if (opcode == Opcodes.INVOKESPECIAL && owner == oldSuperclassName) { | ||
// Update the owner of all INVOKESPECIAL instructions, including those found in | ||
// constructors. | ||
super.visitMethodInsn(opcode, newSuperclassName, name, descriptor, isInterface) | ||
} else { | ||
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Method adapter for a BroadcastReceiver's onReceive method to insert a super call since with | ||
* its new superclass, onReceive will no longer be abstract (it is implemented by Hilt generated | ||
* receiver). | ||
*/ | ||
inner class OnReceiveAdapter( | ||
apiVersion: Int, | ||
nextClassVisitor: MethodVisitor | ||
) : MethodVisitor(apiVersion, nextClassVisitor) { | ||
override fun visitCode() { | ||
super.visitCode() | ||
super.visitIntInsn(Opcodes.ALOAD, 0) // Load 'this' | ||
super.visitIntInsn(Opcodes.ALOAD, 1) // Load method param 1 (Context) | ||
super.visitIntInsn(Opcodes.ALOAD, 2) // Load method param 2 (Intent) | ||
super.visitMethodInsn( | ||
Opcodes.INVOKESPECIAL, | ||
newSuperclassName, | ||
ON_RECEIVE_METHOD_NAME, | ||
ON_RECEIVE_METHOD_DESCRIPTOR, | ||
false | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Check if Hilt generated class is a BroadcastReceiver with the marker field which means | ||
* a super.onReceive invocation has to be inserted in the implementation. | ||
*/ | ||
private fun hasOnReceiveBytecodeInjectionMarker() = | ||
findAdditionalClassFile(newSuperclassName).inputStream().use { | ||
var hasMarker = false | ||
ClassReader(it).accept( | ||
object : ClassVisitor(apiVersion) { | ||
override fun visitField( | ||
access: Int, | ||
name: String, | ||
descriptor: String, | ||
signature: String?, | ||
value: Any? | ||
): FieldVisitor? { | ||
if (name == "onReceiveBytecodeInjectionMarker") { | ||
hasMarker = true | ||
} | ||
return null | ||
} | ||
}, | ||
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES | ||
) | ||
return@use hasMarker | ||
} | ||
|
||
private fun findAdditionalClassFile(className: String) = | ||
File(additionalClasses, "$className.class") | ||
|
||
companion object { | ||
val ANDROID_ENTRY_POINT_ANNOTATIONS = setOf( | ||
"dagger.hilt.android.AndroidEntryPoint", | ||
"dagger.hilt.android.HiltAndroidApp" | ||
) | ||
const val ON_RECEIVE_METHOD_NAME = "onReceive" | ||
const val ON_RECEIVE_METHOD_DESCRIPTOR = | ||
"(Landroid/content/Context;Landroid/content/Intent;)V" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.