If you are interested in this article, maybe you will also be interested in my official account, please scan the qr code below or search the official wechat account: MXSZGG

  • preface
    • Service invocation concepts for modular development
    • The solution
  • Transform API
  • javassist
  • In field

preface

You can skip the following section if you have some familiarity with modularized service invocations.

Service invocation concepts in modular development

Modular development is now a familiar term for Android developers. There should be many iterations of application development using modular development. The meaning of modular development is to subdivide App business into N modules, which is conducive to the collaborative development of developers. One of the problems that needs to be solved in modular development is service calls between modules — because modules exist as libraries and do not depend on each other, they do not actually know each other exists, so when module A wants to know something in module B, What happens when you need to call a method in B? For example, the developer is currently developing the main module. The current TextView needs to display movie information, but it is obvious that the movie information belongs to the Movie module instead of the main module. How to solve this problem? Smart Android developers created the basic module Service and let all business modules rely on the service module, service module’s responsibility is very simple, only need to provide interface declaration, the specific implementation of the specific business module to implement their own. For example, the Service module provides a MovieService class:

public interface MovieService {
  String movieName();
}
Copy the code

The MovieServiceImpl class can be created in the Movie module to implement the MovieService interface

public class MovieServiceImpl implements MovieService {
  @Override public String movieName() {
    return "A good play."; }}Copy the code

In the case of the main module, it should have called the movieName() method of the MovieService implementation class, but the main module can’t know what the MovieService implementation class is, so it looks like the problem is stuck again…

The solution

In fact, the problem lies in how to obtain the path of the interface implementation class, such as renxuelong/ComponentDemo, reflection calls all modules of the application of a method, in this method will map the interface and implementation class, the drawback of this method is obvious, Developers need to display fully qualified names to fill in all module applications, which should be avoided as much as possible in development.

A popular solution is to implement ARouter in this way — use APT — build to scan all classes decorated with the @route annotation to determine if the class implements an interface, and if so create the corresponding XXX. App class, you can download the demo of ARouter and go to ARouter after the build. Providers? The app class –

As shown in the figure above, the fully qualified name of the interface is on the left, and the concrete implementation class is on the right. This maps the interface to the implementation class one by one. In contrast to the method mentioned above, the developer does not need to manually fill in the fully qualified name of the class, because the path of the class can be changed during actual development. Writing the fully qualified name of a class should be avoided by the developer and left to the build tool.

In fact, the scheme I want to elaborate in this paper is the same as APT principle, which is to obtain all service interface implementation classes by scanning the classes modified by the specified annotations and maintain them with Map.

Transform API

Transform is a class. The build tool comes with transforms like ProGuardTransform and DexTransform. A series of Transform classes convert all.class files into.dex files, while developers are officially allowed to create custom transforms that operate on all.class files before converting to.dex files. This means that developers can operate on all.class files in the app. Developers can in the plug-in by android. RegisterTransform (theTransform) or android. RegisterTransform (theTransform, Dependencies) to register a Transform.

As mentioned earlier, a Transform is actually a series of operations, so it should be easy for developers to understand that the output of the previous Transform should be the input of the next Transform —

This is the first part of the Transform knowledge needed to understand this paper, and other knowledge points involved will be mentioned in the practical operation later. If you want to learn more about Transform, you can refer to the official documentation for more information about Transform poses.

javassist

Javassist is a bytecode tool that simply allows you to add, delete, or modify code in.class files, since.java files are compiled into.class files at build time.

In field

For each module in modular development, there are two classes. One is an annotation. If the current module has interface services that need to be implemented, this annotation is used to mark the implementation class. The other is Map, which is used to get the implementation classes of other Modules. Of course, in addition to creating the previously mentioned Lib project, you also need to create a plugin for the APP module to use.

Create a new Java module named Hunter and create the HunterRegistry class and Impl annotation as follows:

public final class HunterRegistry { private static Map<Class<? >, Object> services; privateHunterRegistry() {
  }
    
  @SuppressWarnings("unchecked") public static <T> T get(Class<T> key) {
    return(T) services.get(key); }}Copy the code
public @interface Impl { Class<? > service(); }Copy the code

For the main module, if it wants to get movie information from the Movie module, It simply calls HunterRegistry. Get (MovieService.class).moviename () to get a concrete method implementation of the MovieService implementation class. HunterRegistry looks a bit strange, The Services object is not even initialized, so calling the get() method is bound to return an error, It does look that way from existing code, but in fact after getting all the interface-implementation class mappings in Transform, static code will be inserted by Javassist to initialize the Services object and put key-value pairs into the Services object, The resulting.class file looks like this:

public final class HunterRegistry { private static Map<Class<? >, Object> services = new HashMap(); static { services.put(MovieService.class, new MovieServiceImpl()); } privateHunterRegistry() {
  }

  @SuppressWarnings("unchecked") public static <T> T get(Class<T> key) {
    return(T) services.get(key); }}Copy the code

For the Movie module, it creates a concrete implementation class for MovieService and annotates it with @Impl so that Transform can find its mapping to the interface, for example:

@Impl(service = MovieService.class)
public class MovieServiceImpl implements MovieService {
  @Override public String movieName() {
    return "A good play."; }}Copy the code

Now create the Gradle Plugin:

The basic process of creating a plugin is not mentioned in this article. If the reader is not clear, you can refer to the author’s previous writing for Android developers Gradle series (3) writing plugin.

Create a Plugin class. The plugin is simple:

class HunterPlugin implements Plugin<Project> {
  @Override
  void apply(Project project) {
    project.plugins.withId('com.android.application') {
      project.android.registerTransform(new HunterTransform())
    }
  }
}
Copy the code

So you can see that all the focus is on the HunterTransform

class HunterTransform extends Transform {
  private static final String CLASS_REGISTRY = 'com.joker.hunter.HunterRegistry'
  private static final String CLASS_REGISTRY_PATH = 'com/joker/hunter/HunterRegistry.class'
  private static final String ANNOTATION_IMPL = 'com.joker.hunter.Impl'
  private static final Logger LOG = Logging.getLogger(HunterTransform.class)

  @Override
  String getName() {
    return "hunterService"
  }

  @Override
  Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS
  }

  @Override
  Set<? super QualifiedContent.Scope> getScopes() {
    return Collections.singleton(QualifiedContent.Scope.SUB_PROJECTS)
  }

  @Override
  boolean isIncremental() {
    return false} @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { // 1 transformInvocation.outputProvider.deleteAll() def pool = ClassPool.getDefault() JarInput registryJarInput def impls = [] // 2 transformInvocation.inputs.each { input -> input.jarInputs.each { JarInput jarInput  -> pool.appendClassPath(jarInput.file.absolutePath)if(new JarFile(jarInput.file).getEntry(CLASS_REGISTRY_PATH) ! = null) { registryJarInput = jarInput LOG.info("registryJarInput.file.path is ${registryJarInput.file.absolutePath}")}else {
          def jarFile = new JarFile(jarInput.file)
          jarFile.entries().grep { entry -> entry.name.endsWith(".class") }.each { entry ->
            InputStream stream = jarFile.getInputStream(entry)
            if(stream ! = null) { CtClass ctClass = pool.makeClass(stream)if (ctClass.hasAnnotation(ANNOTATION_IMPL)) {
                impls.add(ctClass)
              }
              ctClass.detach()
            }
          }

          FileUtils.copyFile(jarInput.file,
              transformInvocation.outputProvider.getContentLocation(jarInput.name,
                  jarInput.contentTypes, jarInput.scopes, Format.JAR))
          LOG.info("jarInput.file.path is $jarInput.file.absolutePath")}}}if (registryJarInput == null) {
      return
    }

    // 3
    def stringBuilder = new StringBuilder()
    stringBuilder.append('{\n')
    stringBuilder.append('services = new java.util.HashMap(); ')
    impls.each { CtClass ctClass ->
      ClassFile classFile = ctClass.getClassFile()
      AnnotationsAttribute attr = (AnnotationsAttribute) classFile.getAttribute(
          AnnotationsAttribute.invisibleTag)
      Annotation annotation = attr.getAnnotation(ANNOTATION_IMPL)
      def value = annotation.getMemberValue('service')
      stringBuilder.append('services.put(')
          .append(value)
          .append(', new ')
          .append(ctClass.name)
          .append('()); \n')
    }
    stringBuilder.append('}\n')
    LOG.info(stringBuilder.toString())

    def registryClz = pool.get(CLASS_REGISTRY)
    registryClz.makeClassInitializer().setBody(stringBuilder.toString())

    // 4
    def outDir = transformInvocation.outputProvider.getContentLocation(registryJarInput.name,
        registryJarInput.contentTypes, registryJarInput.scopes, Format.JAR)

    copyJar(registryJarInput.file, outDir, CLASS_REGISTRY_PATH, registryClz.toBytecode())
  }

  private void copyJar(File srcFile, File outDir, String fileName, byte[] bytes) {
    outDir.getParentFile().mkdirs()

    def jarOutputStream = new JarOutputStream(new FileOutputStream(outDir))
    def buffer = new byte[1024]
    int read = 0

    def jarFile = new JarFile(srcFile)
    jarFile.entries().each { JarEntry jarEntry ->
      if (jarEntry.name == fileName) {
        jarOutputStream.putNextEntry(new JarEntry(fileName))
        jarOutputStream.write(bytes)
      } else {
        jarOutputStream.putNextEntry(jarEntry)
        def inputStream = jarFile.getInputStream(jarEntry)
        while ((read= inputStream.read(buffer)) ! = -1) { jarOutputStream.write(buffer, 0,read)
        }
      }
    }
    jarOutputStream.close()
  }
}
Copy the code

Here is simple the way to the first three methods, the first is getInputTypes (), which represents the input what to Transform the file type is, from QualifiedContent. ContentType implementation class can see there are many kinds of input file type, or there is no use, however, Transformmanager.content_class; transformManager.content_class; transformManager.content_class; transformManager.content_class; transformManager.content_class; transformmanager.content_class; transformmanager.content_class; transformmanager.content_class; Followed by getScopes () method, it said developers need to be obtained from where the input file, and QualifiedContent. Scope. SUB_PROJECTS is on behalf of each module, Because we only need to get the.class files of each module; Finally, there is the isIncremental() method, which indicates whether the current Transform supports incremental compilation. To make this article easier, I chose return False to indicate that the current Transform does not support incremental compilation. You can refer to the official documentation later to optimize this Transform to support incremental compilation. Next comes the core transform() method — the transform() method is divided into four parts in order to explain the code. First, in part 1, to avoid the impact of the previous build on this build, Need to call transformInvocation. OutputProvider. DeleteAll () to delete the last product of building, and some of the initialization of operation; The second part is the operation on the Transform input products, that is, all.class files, input, dirInputs, jarInputs, But for the input range for QualifiedContent. Scope. SUB_PROJECTS Transform input type only jarInputs, where jarInputs. The file is, in fact, all the module in the current project:

In this step, we need to distinguish between two kinds of JARS, one is the jar package containing HunterRegistry. Class, through new JarFile(jarinput.file).getentry (CLASS_REGISTRY_PATH)! = null to determine whether the current jar package contains HunterRegistry. Groovy’s API is used to filter out all.class files in the Module jar package, and javassist’s API is used to determine if the current.class is decorated with @impl annotations. If so, add it to impls. The output of the previous Transform will be the input to the next Transform. So I need through transformInvocation. OutputProvider. GetContentLocation (jarInput. Name, jarInput contentTypes, jarInput. Scopes, Format.jar) gets the path to which the JAR package should be moved, since it will also be used as input to the next Transform; The third step is to use ImplS to get the implementation class, use the JavAssist API to get the return value of the service method in the @IMPl annotation, the interface class, and concatenate them into a string. Finally through registryClz. MakeClassInitializer (.) setBody (stringBuilder. ToString ()) can be a string into HunterRegistry. Class files; Step 4 is to replace the bytecode of the new HunterRegistry. Class file obtained in the previous step with the original bytecode and finally enter the specified path.

Use the jadx tool to open debug.apk and find the HunterRegistry. Class file with the following bytecode:

You can see that MovieService and its implementation class MovieServiceImpl are put into services. Run debug.apk to jump to HomeActivity in the main module and see the output on the screen:

The tail language

Both APT and Transform schemes solve the core idea of service invocation in modular development, which is to find the mapping relationship between interface and implementation class. As long as the mapping relationship is solved, the problem will be easily solved. For those readers who are not familiar with Transform, the author believes that after understanding the knowledge of this paper, they can go further to understand Transform, such as optimizing HunterTransform to support incremental compilation. For example, if you try to change the input range, what will happen to the input file?

When the input range is the QualifiedContent. Scope. When the PROJECT will have the directoryInput input file type, the folder path is actually.. / app/build/intermediates/classes/debug, actually it is all the app module. The class files:

And when the input range is zero
QualifiedContent.Scope.EXTERNAL_LIBRARIESJar packages are all third-party libraries:

So if upload plugin maven, in the form of a third party into the project, since the input range, you can’t just above QualifiedContent. Scope. SUB_PROJECTS, because of the plug-in jar package will find.

The last is the address of the project: jokermonn/transformSample