* This article has been authorized to wechat public account guolin_blog (Guo Lin) exclusive publishing
Abstract:
At compile time, all classes to be packaged into the APK are scanned, all component classes are collected, and registration code is generated by modifying bytecode into the component management class to realize automatic registration at compile time, without having to care about which component classes are in the project. Features: No annotations, no new classes added; High performance, no reflection required, and direct calls to the component’s constructor at runtime; Can scan to all classes, no omission; Supports the implementation of hierarchical on demand loading function.
preface
Recently, I built the Android componentitization development framework in the company, using the component bus for communication: provide a basic library, and all components (the implementation class of IComponent interface) are registered with the component management class (component bus: ComponentManager) between components in the same app, through the ComponentManager forward call requests to achieve communication (communication between different apps is not the subject of this article, temporarily omitted). But there was a problem with the implementation:
How do I automatically register component classes in different Modules to ComponentManager?
A common solution in the market today is to use annotationProcessor: dynamically generating component mapping table code through compile-time annotations. The compile-time annotations feature only works when the source code is compiled. The annotations in the AAR package (project dependencies and Maven dependencies are not valid) cannot be scanned. This means that each module must generate its own code at compile time. Then you want to find a way to find these classes scattered across the AAR species for centralized registration.
ARouter’s solution:
- Each module to generate your own Java classes, these classes of package names are ‘com. Alibaba. Android. Arouter. Routes’
- The mapping table is then registered by reflection at run time by reading all classes under this package in each dex file, as shown in the classutils.java source code
The runtime iterates through each entry to find all the class names in the specified package by reading all the dex files, and then reflects to get the class object. That doesn’t seem very efficient.
The ActivityRouter solution (demo has two components named ‘app’ and ‘SDK ‘) :
- There is one in the main App Module
@Modules({"app", "sdk"})
Generate a RouterInit class based on an annotation that marks how many components are in the current app - Generate calls to RouterMapping_app.map in the same package in the Init method of the RouterInit class
- Each module of the generated classes (RouterMapping_app. Java and RouterMapping_sdk. Java) in the com. Making. Mzule. Activityrouter. The router package (in different aar, but the same package name)
- The map() method of the RouterMapping_sdk class generates a call to Routers. The map() method of the RouterMapping_sdk class generates calls to Routers. Method to register the code for the route
- The RouterInit.init() method is triggered on all apis of these Routers to register all routes in the mapping table
This method uses a single RouterInit class to combine all module routing mapping classes, which is more efficient at runtime than scanning all dex files, but requires maintaining a list of component names in the main project code: @modules ({“app”, “SDK “})
Is there a way to manage this list more efficiently?
I wrote a Gradle plug-in that automatically generates registered components, thinking that I used the ASM framework to automatically generate code for AndAop to automatically insert code into any method of any class. The general idea is: at compile time, scan all classes, collect qualified classes, and generate registration code by modifying bytecode to the specified management class, so as to realize compile time automatic registration function, do not care about which component classes in the project. No new classes are added, no reflection is required, and the runtime calls the component’s constructor directly.
Performance: Due to the use of more efficient ASM framework for bytecode analysis and modification, and filter out all classes in Android/Support package (also support to set custom scanning scope), the company’s project measured, before the code confusion of all dex files total about 12MB. The total scanning and code insertion ** takes between 2s-3s **, which is negligible compared to the approximately 3 minutes it takes to pack the entire APK (running environment: MacBookPro 15-inch high Mid 2015).
After the development, considering the universality of this function, we upgraded the component scan registration plug-in to the universal AutoRegister plug-in, which supports the configuration of multiple types of scan registration, see the README document on Github for details. This plug-in is now used in the componentized development framework: CC
After the upgrade, the complete function description of AutoRegister plug-in is:
All classes to be packaged into apK are scanned at compile time and the implementation class (or subclass of the specified class) of the specified interface is automatically registered with the corresponding management class by bytecode operation. It is especially suitable for mapping table generation in command mode or policy mode.
In a componentized development framework, it is helpful to realize the function of hierarchical on demand loading:
- Generate the code for automatic registration of components in the component management class
- This registry is loaded when the component framework is first called
- If there are many functions in a component that are available for external invocation, these functions can be packaged into multiple processors and automatically registered with the component for management
- These processors are loaded when the component is first called
# Implementation process
Step 1: Preparation
- How to develop Gradle plugins using Android Studio
- Understanding TransformAPI: The Transform API is from Gradle Provided after version 1.5.0, it allows third parties to modify the Java bytecode during compilation before packaging the Dex file (custom plug-in registered transform is executed before ProguardTransform and DexTransform, so auto-registered classes don’t need to consider confusion). Reference articles are:
- Android hotfix: Use Gradle Plugin1.5 to transform Nuwa plugins.
- Bytecode modification framework (harder to get started than Javassist’s ASM framework, but higher performance, but the difficulty of learning shouldn’t stop us from pursuing performance) :
- ASM English Document
- ASM API documentation
- Tinker(7) Pile-in implementation (see the introduction of ASM and the combination with transformAPI)
Step 2: Build the plug-in project
- Create a plugin project and publish it to a local Maven repository (I put it in a folder under the root of the project) so that we can quickly debug it locally
Part of the build.gradle file is as follows:
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
dependencies {
compile 'com. Android. Tools. Build: gradle: 2.2.0'} // Load the local maven server configuration (local.properties file in the project root directory) properties.load(project.rootProject.file('local.properties').newDataInputStream())
def artifactory_user = properties.getProperty("artifactory_user")
def artifactory_password = properties.getProperty("artifactory_password")
def artifactory_contextUrl = properties.getProperty("artifactory_contextUrl")
def artifactory_snapshot_repoKey = properties.getProperty("artifactory_snapshot_repoKey")
def artifactory_release_repoKey = properties.getProperty("artifactory_release_repoKey")
def maven_type_snapshot = true// The version number referenced by the project, such as compile'com. Yanzhenjie: andserver: 1.0.1'This is where 1.0.1 is configured. def artifact_version='1.0.1'// Unique package names, such as compile'com. Yanzhenjie: andserver: 1.0.1'Com. Yanzhenjie is configured here. def artifact_group ='com.billy.android'
def artifact_id = 'autoregister'
def debug_flag = true //true: Publish to local Maven repository,falseTask sourcesJar(Maven private server)type: Jar) {
from project.file('src/main/groovy')
classifier = 'sources'} artifacts {archives sourcesJar} uploadArchives {repositories {mavenDeployer {// Deploy to maven repositoryif (debug_flag) {
repository(url: uri('.. /repo-local') //deploy to the local repository}else{def repoKey = maven_type_snapshot? artifactory_snapshot_repoKey : artifactory_release_repoKey repository(url:"${artifactory_contextUrl}/${repoKey}") {
authentication(userName: artifactory_user, password: artifactory_password)
}
}
pom.groupId = artifact_group
pom.artifactId = artifact_id
pom.version = artifact_version + (maven_type_snapshot ? '-SNAPSHOT' : ' ')
pom.project {
licenses {
license {
name The Apache Software License, Version 2.0
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
Copy the code
Add the address of the local repository and dependencies to the build.gradle file in the root directory
buildscript {
repositories {
maven{ url rootProject.file("repo-local") }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
google()
jcenter()
}
dependencies {
classpath 'com. Android. Tools. Build: gradle: 3.0.0 - beta6'
classpath 'com. Making. Dcendents: android - maven - gradle - plugin: 1.4.1'
classpath 'com. Billy. Android: autoregister: 1.0.1'}}Copy the code
2. Add the code related to class scanning in the Transform method of the Transform class
Each {TransformInput INPUT -> // Iterate over jar input.jarInputs. Each {JarInput JarInput -> String destName = JarInput. Name / / rename the output file, because may be the same name, will cover def hexName = DigestUtils md5Hex (jarInput. File. AbsolutePath)if (destName.endsWith(".jar")) { destName = destName.substring(0, Destname.length () -4)} // Obtain the input File File SRC = jarinput. File // Obtain the output File File File dest = outputProvider.getContentLocation(destName +"_"+ hexName, jarinput.contentTypes, jarinput.scopes, format.jar) // Iterate through the JAR's bytecode class files to find the component that needs to be automatically registeredif (CodeScanProcessor.shouldProcessPreDexJar(src.absolutePath)) {
CodeScanProcessor.scanJar(src, dest)
}
FileUtils.copyFile(src, dest)
project.logger.info "Copying\t${src.absolutePath} \nto\t\t${dest.absolutePath}"} / / directory traversal input. DirectoryInputs. Each {DirectoryInput DirectoryInput - > / / get the product catalog File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) String root = directoryInput.file.absolutePathif(! Root root. EndsWith (File. The separator)) + = File. The separator / / directory traversal of each File directoryInput File. EachFileRecurse {File File - > def path = file.absolutePath.replace(root,' ')
if(file.isFile()){
CodeScanProcessor.checkInitClass(path, new File(dest.absolutePath + File.separator + path))
if (CodeScanProcessor.shouldProcessClass(path)) {
CodeScanProcessor.scanClass(file)
}
}
}
project.logger.info "Copying\t${directoryInput.file.absolutePath} \nto\t\t${dest.absolutePath}"Fileutils. copyDirectory(directoryInput.file, dest)}}Copy the code
CodeScanProcessor is a utility class, The CodeScanProcessor. ScanJar (SRC, Dest) and CodeScanProcessor scanClass (file) are respectively used to scan the jar packages and class files of the principle of scanning is to utilize the ASM ClassVisitor to view each class of the interfaces being implemented by the class name and the name of the parent class, comparing with configuration information, If our filter criteria are met, record that the classes’ parameterless constructors will be called for registration after all scans are completed
static void scanClass(InputStream inputStream) {
ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
inputStream.close()
}
static class ScanClassVisitor extends ClassVisitor {
ScanClassVisitor(int api, ClassVisitor cv) {
super(api, cv)
}
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
RegisterTransform.infoList.each { ext ->
if (shouldProcessThisClassForRegister(ext, name)) {
if(superName ! ='java/lang/Object' && !ext.superClassNames.isEmpty()) {
for (int i = 0; i < ext.superClassNames.size(); i++) {
if (ext.superClassNames.get(i) == superName) {
ext.classList.add(name)
return}}}if(ext.interfaceName && interfaces ! = null) { interfaces.each { itName ->if (itName == ext.interfaceName) {
ext.classList.add(name)
}
}
}
}
}
}
}
Copy the code
3. Record the file in which the target class is located, because we will then modify its bytecode to insert the registration code into it
static void checkInitClass(String entryName, File file) {
if(entryName == null || ! entryName.endsWith(".class"))
return
entryName = entryName.substring(0, entryName.lastIndexOf('. '))
RegisterTransform.infoList.each { ext ->
if (ext.initClassName == entryName)
ext.fileContainsInitClass = file
}
}
Copy the code
4. After the scan is complete, start modifying the bytecode of the target class (using ASM’s MethodVisitor to modify the target class specified method, which defaults to static blocks if not specified, i.e.
method). The generated code calls the no-argument constructor of the scanned class directly, not through reflection
- Class file: Modify the bytecode file directly (in fact, regenerate a class file and replace the original file)
- Jar file: Copy this JAR file, find the JarEntry corresponding to the target class in the JAR package, modify its bytecode, and replace the original JAR file
import org.apache.commons.io.IOUtils
import org.objectweb.asm.*
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
/**
*
* @author billy.qi
* @since 17/3/20 11:48
*/
class CodeInsertProcessor {
RegisterInfo extension
private CodeInsertProcessor(RegisterInfo extension) {
this.extension = extension
}
static void insertInitCodeTo(RegisterInfo extension) {
if(extension ! = null && ! extension.classList.isEmpty()) { CodeInsertProcessor processor = new CodeInsertProcessor(extension) File file = extension.fileContainsInitClassif (file.getName().endsWith('.jar'))
processor.insertInitCodeIntoJarFile(file)
elseProcessor. InsertInitCodeIntoClassFile (file)}} / / processing jar in the class code into private file insertInitCodeIntoJarFile (file jarFile) {if (jarFile) {
def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
if (optJar.exists())
optJar.delete()
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = file.getInputStream(jarEntry)
jarOutputStream.putNextEntry(zipEntry)
if (isInitClass(entryName)) {
println('codeInsertToClassName:' + entryName)
def bytes = referHackWhenInit(inputStream)
jarOutputStream.write(bytes)
} else {
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
inputStream.close()
jarOutputStream.closeEntry()
}
jarOutputStream.close()
file.close()
if (jarFile.exists()) {
jarFile.delete()
}
optJar.renameTo(jarFile)
}
return jarFile
}
boolean isInitClass(String entryName) {
if(entryName == null || ! entryName.endsWith(".class"))
return false
if (extension.initClassName) {
entryName = entryName.substring(0, entryName.lastIndexOf('. '))
return extension.initClassName == entryName
}
return false} /** * handle class injection * @param file * @returnThe modified bytecode File content * / private byte [] insertInitCodeIntoClassFile (File File) {def optClass = new File (File. The getParent (), file.name +".opt") FileInputStream inputStream = new FileInputStream(file) FileOutputStream outputStream = new FileOutputStream(optClass) def bytes = referHackWhenInit(inputStream) outputStream.write(bytes) inputStream.close() outputStream.close()if (file.exists()) {
file.delete()
}
optClass.renameTo(file)
returnbytes } //refer hack class when object init private byte[] referHackWhenInit(InputStream inputStream) { ClassReader cr = new ClassReader(inputStream) ClassWriter cw = new ClassWriter(cr, 0) ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw) cr.accept(cv, ClassReader.EXPAND_FRAMES)return cw.toByteArray()
}
class MyClassVisitor extends ClassVisitor {
MyClassVisitor(int api, ClassVisitor cv) {
super(api, cv)
}
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
if(name == extension.initmethodName) {// Inject code into the specified method. Boolean _static = (access & opcodes.acc_static) > 0 mv = new MyMethodVisitor(Opcodes.ASM5, mv, _static) }return mv
}
}
class MyMethodVisitor extends MethodVisitor {
boolean _static;
MyMethodVisitor(int api, MethodVisitor mv, boolean _static) {
super(api, mv)
this._static = _static;
}
@Override
void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
extension.classList.each { name ->
if(! _static) {// load this mv.visitVarinsn (opcodes.aload, 0)} // Create a component instance with the no-parameter constructor mv.visitTypeInsn(opcodes.new, name) mv.visitInsn(Opcodes.DUP) mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name,"<init>"."()V".false) // Call the registration method to register the component instance with the component libraryif (_static) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC
, extension.registerClassName
, extension.registerMethodName
, "(L${extension.interfaceName};) V"
, false)}else {
mv.visitMethodInsn(Opcodes.INVOKESPECIAL
, extension.registerClassName
, extension.registerMethodName
, "(L${extension.interfaceName};) V"
, false)
}
}
}
super.visitInsn(opcode)
}
@Override
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}
}
Copy the code
5. Receive the extended parameters to obtain the characteristics of the class to be scanned and the code to be inserted
After searching for a long time, we failed to find a way for gradle plugin to receive the extension parameters of a custom array of objects, so we took a step back and used the List
Import org.gradle.api.Project /** * aop configuration information * @author Billy. Qi * @since 17/3/28 11:48 */ class AutoRegisterConfig { public ArrayList<Map<String, Object>> registerInfo = [] ArrayList<RegisterInfo> list = new ArrayList<>() Project projectAutoRegisterConfig(){}
void convertConfig() {
registerInfo.each { map ->
RegisterInfo info = new RegisterInfo()
info.interfaceName = map.get('scanInterface')
def superClasses = map.get('scanSuperClasses')
if(! superClasses) { superClasses = new ArrayList<String>() }else if (superClasses instanceof String) {
ArrayList<String> superList = new ArrayList<>()
superList.add(superClasses)
superClasses = superList
}
info.superClassNames = superClasses
info.initClassName = map.get('codeInsertToClassName'Info.initmethodname = map.get()'codeInsertToMethodName'Info.registermethodname = map.get(default: static block)'registerMethodName') // The generated code calls the method info.registerClassName = map.get('registerClassName'Info.include = map.get()'include')
info.exclude = map.get('exclude')
info.init()
if (info.validate())
list.add(info)
else {
project.logger.error('auto register config error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n' + info.toString())
}
}
}
}
Copy the code
Import java.util. Reggex.pattern /** * AOP configuration information * @author Billy. Qi * @since 17/3/28 11:48 */ class RegisterInfo {static final DEFAULT_EXCLUDE = ['.*/R(\\$[^/]*)? '
, '.*/BuildConfig$'] // The following is a configurable parameter: String interfaceName =' '
ArrayList<String> superClassNames = []
String initClassName = ' '
String initMethodName = ' '
String registerClassName = ' '
String registerMethodName = ' 'ArrayList<String> include = [] ArrayList<String> exclude = [] ArrayList<Pattern> excludePatterns = [] File fileContainsInitClass //initClassName class File or jar File containing initClassName class ArrayList<String> classList = new ArrayList<>()RegisterInfo(){}
boolean validate() {
returnInterfaceName && registerClassName && registerMethodName} // Used to output logs in the console @override StringtoString() {
StringBuilder sb = new StringBuilder('{')
sb.append('\n\t').append('scanInterface').append('\t\t\t=\t').append(interfaceName)
sb.append('\n\t').append('scanSuperClasses').append('\t\t=\t[')
for (int i = 0; i < superClassNames.size(); i++) {
if (i > 0) sb.append(', ')
sb.append('\'').append(superClassNames.get(i)).append('\' ')
}
sb.append(' ]')
sb.append('\n\t').append('codeInsertToClassName').append('\t=\t').append(initClassName)
sb.append('\n\t').append('codeInsertToMethodName').append('\t=\t').append(initMethodName)
sb.append('\n\t').append('registerMethodName').append('\t\t=\tpublic static void ')
.append(registerClassName).append('. ').append(registerMethodName)
sb.append('\n\t').append('include').append('= [')
include.each { i ->
sb.append('\n\t\t\'').append(i).append('\' ')
}
sb.append('\n\t]')
sb.append('\n\t').append('exclude').append('= [')
exclude.each { i ->
sb.append('\n\t\t\'').append(i).append('\' ')
}
sb.append('\n\t]\n}')
return sb.toString()
}
void init() {
if (include == null) include = new ArrayList<>()
if (include.empty) include.add(". *") // Default to include all if not setif (exclude == null) exclude = new ArrayList<>()
if(! RegisterClassName) registerClassName = initClassName // will interfaceName in'. 'convert'/'
if(interfaceName) interfaceName = convertDotToSlash(interfaceName'. 'convert'/'
if (superClassNames == null) superClassNames = new ArrayList<>()
for (int i = 0; i < superClassNames.size(); i++) {
def superClass = convertDotToSlash(superClassNames.get(i))
superClassNames.set(i, superClass)
if(! Exclude. Contains (superClass)) exclude. Add (superClass)} //interfaceName to excludeif(! Contains (interfaceName)) exclude.add(interfaceName) // The class in which the method is registered and initialized defaults to the same class initClassName = ConvertDotToSlash (initClassName) // Inserts into static blocks by defaultif(! initMethodName) initMethodName ="<clinit>"RegisterClassName = convertDotToSlash(registerClassName) // Add DEFAULT_EXCLUDE. Each {e ->if(! exclude.contains(e)) exclude.add(e) } initPattern(include, includePatterns) initPattern(exclude, excludePatterns) } private static String convertDotToSlash(String str) {return str ? str.replaceAll('\ \..'/').intern() : str
}
private static void initPattern(ArrayList<String> list, ArrayList<Pattern> patterns) {
list.each { s ->
patterns.add(Pattern.compile(s))
}
}
}
Copy the code
Step 3: In Application, configure the relevant extension parameters required to automatically register the plug-in
Add extension parameters to the build.gradle file of the main App Module as shown in the following example:
// Auto Register extension // Scan all classes that will be typed into the APK package at compile time // subclasses that implement scanInterface or scanSuperClasses // and in the codeInsertToClassName class CodeInsertToMethodName method generated in the following code: / / codeInsertToClassName registerMethodName (scanInterface) / / key points: // 1. CodeInsertToMethodName, if not specified, // 2. CodeInsertToMethodName and registerMethodName need to be static or nonstatic. / * in com. Billy. App_lib_interface. CategoryManager. Class files in the static {register (new CategoryA ()); Register (new CategoryB()); //scanSuperClass subclass} */ apply plugin:'auto-register'
autoregister {
registerInfo = [
[
'scanInterface' : 'com.billy.app_lib_interface.ICategory'// scanSuperClasses will be automatically added to exclude. The following exclude is for demonstration only.'scanSuperClasses' : ['com.billy.android.autoregister.demo.BaseCategory'].'codeInsertToClassName' : 'com.billy.app_lib_interface.CategoryManager'CodeInsertToMethodName is not specified and is inserted into a static block by default, so register must be static here.'registerMethodName' : 'register' //
, 'exclude': [// Excluded classes that support regular expressions (package delimiters need to be expressed with /, but cannot be.)'com.billy.android.autoregister.demo.BaseCategory'.replaceAll('\ \..'/') // Exclude this base class [], ['scanInterface' : 'com.billy.app_lib.IOther'
, 'codeInsertToClassName' : 'com.billy.app_lib.OtherManager'
, 'codeInsertToMethodName' : 'init'// Non-static methods,'registerMethodName' : 'registerOther'// Non-static methods]]}Copy the code
conclusion
This article introduces the function of AutoRegister plug-in and its application in the componentized development framework. The principle of this plug-in is mainly explained, and the implementation process of this plug-in is mainly introduced, which involves technical points such as TransformAPI, ASM, Groovy related syntax and Gradle mechanism.
All the code and usage demo of this plug-in have been open source to Github, welcome to fork, start
Next, use this plugin to automatically manage the registry for us!