1 package dagger.hilt.android.plugin 2 3 import com.android.build.api.instrumentation.AsmClassVisitorFactory 4 import com.android.build.api.instrumentation.ClassContext 5 import com.android.build.api.instrumentation.ClassData 6 import com.android.build.api.instrumentation.InstrumentationParameters 7 import java.io.File 8 import org.gradle.api.provider.Property 9 import org.gradle.api.tasks.Internal 10 import org.objectweb.asm.ClassReader 11 import org.objectweb.asm.ClassVisitor 12 import org.objectweb.asm.FieldVisitor 13 import org.objectweb.asm.MethodVisitor 14 import org.objectweb.asm.Opcodes 15 16 /** 17 * ASM Adapter that transforms @AndroidEntryPoint-annotated classes to extend the Hilt 18 * generated android class, including the @HiltAndroidApp application class. 19 */ 20 class AndroidEntryPointClassVisitor( 21 private val apiVersion: Int, 22 nextClassVisitor: ClassVisitor, 23 private val additionalClasses: File 24 ) : ClassVisitor(apiVersion, nextClassVisitor) { 25 26 @Suppress("UnstableApiUsage") // ASM Pipeline APIs 27 interface AndroidEntryPointParams : InstrumentationParameters { 28 @get:Internal 29 val additionalClassesDir: Property<File> 30 } 31 32 @Suppress("UnstableApiUsage") // ASM Pipeline APIs 33 abstract class Factory : AsmClassVisitorFactory<AndroidEntryPointParams> { createClassVisitornull34 override fun createClassVisitor( 35 classContext: ClassContext, 36 nextClassVisitor: ClassVisitor 37 ): ClassVisitor { 38 return AndroidEntryPointClassVisitor( 39 apiVersion = instrumentationContext.apiVersion.get(), 40 nextClassVisitor = nextClassVisitor, 41 additionalClasses = parameters.get().additionalClassesDir.get() 42 ) 43 } 44 45 /** 46 * Check if a class should be transformed. 47 * 48 * Only classes that are an Android entry point should be transformed. 49 */ isInstrumentablenull50 override fun isInstrumentable(classData: ClassData) = 51 classData.classAnnotations.any { ANDROID_ENTRY_POINT_ANNOTATIONS.contains(it) } 52 } 53 54 // The name of the Hilt generated superclass in it internal form. 55 // e.g. "my/package/Hilt_MyActivity" 56 lateinit var newSuperclassName: String 57 58 lateinit var oldSuperclassName: String 59 visitnull60 override fun visit( 61 version: Int, 62 access: Int, 63 name: String, 64 signature: String?, 65 superName: String?, 66 interfaces: Array<out String>? 67 ) { 68 val packageName = name.substringBeforeLast('/') 69 val className = name.substringAfterLast('/') 70 newSuperclassName = 71 packageName + "/Hilt_" + className.replace("$", "_") 72 oldSuperclassName = superName ?: error { "Superclass of $name is null!" } 73 val newSignature = signature?.replaceFirst(oldSuperclassName, newSuperclassName) 74 super.visit(version, access, name, newSignature, newSuperclassName, interfaces) 75 } 76 visitMethodnull77 override fun visitMethod( 78 access: Int, 79 name: String, 80 descriptor: String, 81 signature: String?, 82 exceptions: Array<out String>? 83 ): MethodVisitor { 84 val nextMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) 85 val invokeSpecialVisitor = InvokeSpecialAdapter( 86 apiVersion = apiVersion, 87 nextClassVisitor = nextMethodVisitor, 88 isConstructor = name == "<init>" 89 ) 90 if (name == ON_RECEIVE_METHOD_NAME && 91 descriptor == ON_RECEIVE_METHOD_DESCRIPTOR && 92 hasOnReceiveBytecodeInjectionMarker() 93 ) { 94 return OnReceiveAdapter(apiVersion, invokeSpecialVisitor) 95 } 96 return invokeSpecialVisitor 97 } 98 99 /** 100 * Adapter for super calls (e.g. super.onCreate()) that rewrites the owner reference of the 101 * invokespecial instruction to use the new superclass. 102 * 103 * The invokespecial instruction is emitted for code that between other things also invokes a 104 * method of a superclass of the current class. The opcode invokespecial takes two operands, each 105 * of 8 bit, that together represent an address in the constant pool to a method reference. The 106 * method reference is computed at compile-time by looking the direct superclass declaration, but 107 * at runtime the code behaves like invokevirtual, where as the actual method invoked is looked up 108 * based on the class hierarchy. 109 * 110 * However, it has been observed that on APIs 19 to 22 the Android Runtime (ART) jumps over the 111 * direct superclass and into the method reference class, causing unexpected behaviours. 112 * Therefore, this method performs the additional transformation to rewrite direct super call 113 * invocations to use a method reference whose class in the pool is the new superclass. 114 * 115 * @see: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial 116 * @see: https://source.android.com/devices/tech/dalvik/dalvik-bytecode 117 */ 118 inner class InvokeSpecialAdapter( 119 apiVersion: Int, 120 nextClassVisitor: MethodVisitor, 121 private val isConstructor: Boolean 122 ) : MethodVisitor(apiVersion, nextClassVisitor) { 123 124 // Flag to know that we have visited the first invokespecial instruction in a constructor call 125 // which corresponds to the `super()` constructor call required as the first statement of an 126 // overridden constructor body. 127 private var visitedSuperConstructorInvokeSpecial = false 128 visitMethodInsnnull129 override fun visitMethodInsn( 130 opcode: Int, 131 owner: String, 132 name: String, 133 descriptor: String, 134 isInterface: Boolean 135 ) { 136 if (opcode == Opcodes.INVOKESPECIAL && owner == oldSuperclassName) { 137 // Update the owner of INVOKESPECIAL instructions, including those found in constructors. 138 super.visitMethodInsn(opcode, getAdaptedOwner(name) ?: owner, name, descriptor, isInterface) 139 } else { 140 super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) 141 } 142 } 143 144 // Gets the updated owner of an INVOKESPECIAL found in the method being visited. getAdaptedOwnernull145 private fun getAdaptedOwner(methodRefName: String): String? { 146 // If the method reference is a constructor and we are visiting a constructor then only the 147 // first INVOKESPECIAL instruction found should be transformed since that correponds to the 148 // super constructor call. 149 if (methodRefName == "<init>" && isConstructor && !visitedSuperConstructorInvokeSpecial) { 150 visitedSuperConstructorInvokeSpecial = true 151 return newSuperclassName 152 } 153 // If the method reference is not a constructor then the instruction for a super call that 154 // should be transformed. 155 if (methodRefName != "<init>") { 156 return newSuperclassName 157 } 158 return null 159 } 160 } 161 162 /** 163 * Method adapter for a BroadcastReceiver's onReceive method to insert a super call since with 164 * its new superclass, onReceive will no longer be abstract (it is implemented by Hilt generated 165 * receiver). 166 */ 167 inner class OnReceiveAdapter( 168 apiVersion: Int, 169 nextClassVisitor: MethodVisitor 170 ) : MethodVisitor(apiVersion, nextClassVisitor) { visitCodenull171 override fun visitCode() { 172 super.visitCode() 173 super.visitIntInsn(Opcodes.ALOAD, 0) // Load 'this' 174 super.visitIntInsn(Opcodes.ALOAD, 1) // Load method param 1 (Context) 175 super.visitIntInsn(Opcodes.ALOAD, 2) // Load method param 2 (Intent) 176 super.visitMethodInsn( 177 Opcodes.INVOKESPECIAL, 178 newSuperclassName, 179 ON_RECEIVE_METHOD_NAME, 180 ON_RECEIVE_METHOD_DESCRIPTOR, 181 false 182 ) 183 } 184 } 185 186 /** 187 * Check if Hilt generated class is a BroadcastReceiver with the marker field which means 188 * a super.onReceive invocation has to be inserted in the implementation. 189 */ hasOnReceiveBytecodeInjectionMarkernull190 private fun hasOnReceiveBytecodeInjectionMarker() = 191 findAdditionalClassFile(newSuperclassName).inputStream().use { 192 var hasMarker = false 193 ClassReader(it).accept( 194 object : ClassVisitor(apiVersion) { 195 override fun visitField( 196 access: Int, 197 name: String, 198 descriptor: String, 199 signature: String?, 200 value: Any? 201 ): FieldVisitor? { 202 if (name == "onReceiveBytecodeInjectionMarker") { 203 hasMarker = true 204 } 205 return null 206 } 207 }, 208 ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES 209 ) 210 return@use hasMarker 211 } 212 findAdditionalClassFilenull213 private fun findAdditionalClassFile(className: String) = 214 File(additionalClasses, "$className.class") 215 216 companion object { 217 val ANDROID_ENTRY_POINT_ANNOTATIONS = setOf( 218 "dagger.hilt.android.AndroidEntryPoint", 219 "dagger.hilt.android.HiltAndroidApp" 220 ) 221 const val ON_RECEIVE_METHOD_NAME = "onReceive" 222 const val ON_RECEIVE_METHOD_DESCRIPTOR = 223 "(Landroid/content/Context;Landroid/content/Intent;)V" 224 } 225 } 226