Arouter’s questions?
1. What if we annotate the same path? If one SecondActivity uses a/ A /b path and the other ThirdActivity uses a/ a/b path, will the compilation pass? If so, which Activity is retrieved through path?
2. What if there are two activities in the same group under different Modules? Module1 has a SecondActivity path that uses/A /c, and module2 has a ThirdActivity path that also uses /a/d.
ARouter can also be used to retrieve services. If the service is discovered through an interface, what will happen if there is more than one implementation of the interface?
4. ARouter service, why can’t we inherit the IProvider from an abstract class and implement the abstract class instead of inheriting the IProvider from an interface and implementing the interface?
5. Is it the same object or a different object each time you get the same service path from ARouter?
6. What is the purpose of the arouter-gradle-plugin? ARouter added apK and the first load will take time.
ARouter’s source code is divided into several parts:
Let’s analyze them one by one:
Arouter -compiler: Generates class files at compile time.
In the ARouter source code, the ARouter – Compiler module is used to handle annotations.
ARouter generates the.java file we want by extracting the annotations at compile time, just before going from.java to.class.
The main code is in the Arouter-Compiler module. We used annotationProcessor ‘com.alibaba: arouter-Compiler: latestVersion ‘to introduce ARouter, AnnotationProcessor processes annotations at compile time and does not package them into APK.
In this Module, we mainly analyze the code of RouteProcessor, and the principles of other xxxProcessor are similar.
RouteProcessor indirectly inherits AbstractProcessor, init method gets all kinds of tools, process method is the most important.
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
try {
this.parseRoutes(routeElements); //1
} catch (Exception e) {
}
return true;
}
return false;
}
Copy the code
Do some preparatory work, particularly in the parseRoutes method in comment 1.
The parseRoutes method, which is quite long, is only partially captured:
. If (types.issameType (tm, iProvider)) { // Its implements iProvider interface himself. // This interface extend the IProvider, so it can be used for mark provider loadIntoMethodOfProviderBuilder.addStatement( "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", (routeMeta.getRawType()).toString(), routeMetaCn, routeTypeCn, className, routeMeta.getPath(), routeMeta.getGroup()); } else if (types.isSubtype(tm, iProvider)) { // This interface extend the IProvider, so it can be used for mark provider loadIntoMethodOfProviderBuilder.addStatement( "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", tm.toString(), // So stupid, will duplicate only save class name. routeMetaCn, routeTypeCn, className, routeMeta.getPath(), routeMeta.getGroup()); }...Copy the code
In the sentence types. IsSameType (tm, iProvider), types is a Type object that is retrieved from the environment in RouteProcessor’s init method, We can use it to compare whether two interfaces are the same or whether one is a subclass of the other.
By calling the isSameType method of Types, we can compare tm and iProvider to whether they are the same interface. The tm variable is the interface implemented by the class file, which is the annotated class file, and the iProvider is the interface com\ Alibaba \ Android \arouter\facade\template\ iprovider.java.
If types is an IProvider interface, that is, the class implements IProvider directly, then ARouter uses Javapoet to generate the corresponding code. Types. IsSubtype (tm, iProvider) means that if types is a subclass of iProvider, then ARouter also generates the corresponding code using Javapoet.
What is Javapoet? JavaPoet is an open source Java code generation framework from Square that provides Java Api generation. ARouter uses Javapoet to generate various routing files. In this way, you can scan the path where the routing file resides, obtain the class name of the routing file, and initialize the routing table by calling related methods.
If there is a class HelloService that inherits from IProvider, and HelloService has a path annotation, for example:
@Route(path = "/module1/hello")
public class HelloService implements IProvider {
@Override
public void init(Context context) {
}
}
Copy the code
Module1 is the name of the module. You can see that a Map passed in has a key-value pair with HelloService as the key and RouteMeta as value. RouteMeta can be understood as the meta information of a jump. You can obtain the path, group and corresponding target Class of a jump through RouteMeta:
public class ARouter$$Providers$$module1 implements IProviderGroup { @Override public void loadInto(Map<String, RouteMeta> providers) { providers.put("com.example.module1.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloService.class, "/module1/hello", "module1", null, -1, -2147483648)); }}Copy the code
However, ARouter provides an alternative, which is to use a class such as IUserService to inherit the IProvider interface and write a class called UserServiceImpl to implement the IUserService. UserServiceImpl uses the path annotation. As you can see, the key in the Map is no longer the name of the implemented UserServiceImpl class, but the interface it implements, IUserService. This is the rule ARouter uses to discover the service through the interface to achieve decoupling:
public class ARouter$$Providers$$module1 implements IProviderGroup { @Override public void loadInto(Map<String, RouteMeta> providers) { providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648)); }}Copy the code
Now consider questions 3 and 4 mentioned at the beginning of this article. The third question is what happens if the interface has more than one implementation. Using the UserServiceImpl example above, if there is another class such as UserServiceImpl2 that also integrates with IUserService, and UserServiceImpl2 also has a Path annotation, ARouter will generate such a class file.
public class ARouter$$Providers$$module1 implements IProviderGroup { @Override public void loadInto(Map<String, RouteMeta> providers) { providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648)); providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl2.class, "/u/2", "u", null, -1, -2147483648)); }}Copy the code
Obviously, implementing the same IUserService will make them have the same key, and the element that follows it will override the element that was put before it. From the above inference, it can be concluded that if there is more than one implementation of the interface, the interface with the alphabetic order will override the interface with the alphabetic order
Fourth question: Why you can’t inherit the IProvider from an abstract class and implement the abstract class instead of inheriting the IProvider from an interface and implementing the interface is because ARouter only deals with the interface in RouteProcessor’s parseRoutes method.
So what about question 1 and question 2?
Question 1: What if we annotate the same path? That is, one SecondActivity uses the path of /a/b, and the other ThirdActivity uses the path of /a/b.
We know that APT framework is handled by modules, so we also divide problems into the same module and different modules:
If SecondActivity and ThirdActivity are in the same Module:
RouteProcessor has a member variable, groupMap. GroupMap is important for generating the ARouter groupgroupgroup_name file. private Map
groupMap = new HashMap<>();
The key of the groupMap is string, that is, the name of the group, and the value is a Set. We all know that a Set cannot be added to an existing element in a Set, and exceptions will not be thrown. From this we can guess that if we annotate the same path in the same module, the elements after the alphabet will be invalid. That is, if one SecondActivity uses the path of /a/b, and another ThirdActivity uses the path of /a/b. The group class file generated by ARouter would look like this, and ThirdActivity would not appear in the regenerated file because it was not added to the Set:
public class ARouter$$Group$$a implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/a/b", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/a/b", "a", null, -1, -2147483648));
}
}
Copy the code
If SecondActivity and ThirdActivity are under different Modules:
Since apt framework is compiled by modules and each module generates ARouterRoot$module\_name,ARouterGroup group_name, etc., it is no doubt that two identical files will be generated. Using the same SecondActivity and ThirdActivity example above, both generate ARoutergroup\_name and so on, and you will no doubt generate two identical files. Using the same SecondActivity and ThirdActivity example above, they both generate ARoutergroup_name and so on, and you will no doubt generate two identical files. Using the same SecondActivity and ThirdActivity examples above, both generate ARouterGroup$$A files, which will definitely fail when merged into the dex, as it does.
To sum up:
For the first question: What if we annotate the same path? That is, one SecondActivity uses the path of /a/b, and the other ThirdActivity uses the path of /a/b.
- If SecondActivity and ThirdActivity are in the same Module, the elements after the alphabet are invalidated because they cannot be added to the Set. That is, only the routes associated with SecondActivity are generated.
- ARouterRoot$module_name,ARouterGroup$$group_name,ARouterGroup$$group_name, etc. Two routing files with the same group will be generated, which will never compile.
This also explains the second question, so I won’t go into it again.
For question 5: Is it the same object or a different object each time a service with the same path is fetched through ARouter?
You need to find the answer in the arouter-API module.
Arouter – API: Implementation module for jump
The classes that ARouter needs to run at runtime, such as ARouter. Java, LogisticsCenter. Java, etc., are all in the module ARouter – API.
For question 5: Is it the same object or a different object each time a service with the same path is fetched through ARouter?
Let’s first consider the case of getting a service through an interface:
When using ARouter, we typically get the service through the interface like this:
val service2 = ARouter.getInstance().navigation(IUserService::class.java)
Copy the code
ARouter. GetInstance () gets the ARouter singleton from DCL and calls ARouter’s navigation method.
public <T> T navigation(Class<? extends T> service) {
return _ARouter.getInstance().navigation(service);
}
Copy the code
_ARouter is a class that actually implements logic, and its navigation is as follows:
protected <T> T navigation(Class<? extends T> service) { try { Postcard postcard = LogisticsCenter.buildProvider(service.getName()); / / 1... LogisticsCenter.completion(postcard); return (T) postcard.getProvider(); } catch (NoRouteFoundException ex) { return null; }}Copy the code
Note 1, let’s go in and see what we did:
public static Postcard buildProvider(String serviceName) { RouteMeta meta = Warehouse.providersIndex.get(serviceName); //1 if (null == meta) { return null; } else { return new Postcard(meta.getPath(), meta.getGroup()); }}Copy the code
Note 1: The RouteMeta for serviceName is fetched from Warehouse’s providersIndex. ProvidersIndex is a Map obtained when ARouter is initialized with ClassName as key. RouteMeta acts as a Map for value.
Warehouse can be an instant data Warehouse and is a class that contains many static maps. ARouter loads the routing table into memory at initialization, which is to load the routing table into Warehouse’s many maps.
A RouteMeta can be thought of as a bean that stores the path of the jump and the className of the final jump target.
Since it is clear that ARouter has been initialized before getting the service through the interface, the meta here is not null. After retrieving the corresponding RouteMeta from the interface’s class name, it constitutes a Postcard.
After returning to _ARouter navigation method, call the LogisticsCenter.com pletion (it); . Take a look at the completion method:
public synchronized static void completion(Postcard postcard) { ...... switch (routeMeta.getType()) { case PROVIDER: // if the route is provider, should find its instance // Its provider, so it must implement IProvider Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination(); IProvider instance = Warehouse.providers.get(providerMeta); //1 if (null == instance) { // There's no instance of this provider IProvider provider; try { provider = providerMeta.getConstructor().newInstance(); provider.init(mContext); Warehouse.providers.put(providerMeta, provider); instance = provider; } catch (Exception e) { throw new HandlerException("Init provider failed! " + e.getMessage()); } } postcard.setProvider(instance); postcard.greenChannel(); // Provider should skip all of interceptors break; . }}}Copy the code
The logic of the omitted code is to go to the routes Map of the Warehouse to find out if there is a corresponding RouteMeta. If there is no RouteMeta, go to the groupsIndex of the Warehouse to load the group. And then the routes map must have a corresponding RouteMeta.
Then look at comment 1 and get the corresponding class objects from the Warehouse providers map. If there are any, use them directly. If not, create an instance via reflection and add it to the Warehouse providers map. So every time the service is discovered through ARouter’s interface, it gets the same object every time.
What if it’s through path? Since you also end up calling the Completion method through path, the conclusion is the same as finding the service through the interface.
So far, the conclusion of problem 5 is that every time a service is discovered through ARouter’s interface, it gets the same object every time.
One more question, what does the arouter-gradle-plugin do? ARouter added apK and the first load will take time.
Arouter -gradle-plugin: applies to class to dex file period, using ASM framework to insert bytecode
The arouter-gradle-plugin module uses ASM to insert bytecode into the loadRouterMap method of LogisticsCenter:
LogisticsCener: private static void loadRouterMap() { registerByPlugin = false; / / ASM framework to insert the bytecode / / such as below: the register (" com. Alibaba. Android. Arouter. Routes. Arouter $$$$arouterapi Root "); / / such as: register (" com. Alibaba. Android. Arouter. Routes. Arouter $$$$arouterapi will ")}Copy the code
When is the loadRouterMap method called? In the init method of LogisticsCenter.
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
loadRouterMap(); //1
if (registerByPlugin) { //2
} else {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); //3
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// Load interceptorMeta
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// Load providerIndex
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
}
} catch (Exception e) {
...
}
}
Copy the code
The init method of LogisticsCenter is called when ARouter is initialized to load the routing table into memory.
Note 1: loadRouterMap is called, loadRouterMap is inserted into bytecode, register is called, and the Boolean registerByPlugin is set to true. Therefore, at comment 2, if registerByPlugin is false, it indicates that no bytecode has been inserted and goes to comment 3. The arouter API module’s ClassUtils getFileNameByPackageName method does the initialization by scanning all classnames in the specified package name:
ClassUtils.getFileNameByPackageName: public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException { final Set<String> classNames = new HashSet<>(); List<String> paths = getSourcePaths(context); final CountDownLatch parserCtl = new CountDownLatch(paths.size()); for (final String path : paths) { DefaultPoolExecutor.getInstance().execute(new Runnable() { @Override public void run() { DexFile dexfile = null; try { if (path.endsWith(EXTRACTED_SUFFIX)) { //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache" dexfile = DexFile.loadDex(path, path + ".tmp", 0); } else { dexfile = new DexFile(path); } Enumeration<String> dexEntries = dexfile.entries(); while (dexEntries.hasMoreElements()) { String className = dexEntries.nextElement(); if (className.startsWith(packageName)) { classNames.add(className); } } } catch (Throwable ignore) { } finally { if (null ! = dexfile) { try { dexfile.close(); } catch (Throwable ignore) { } } parserCtl.countDown(); }}}); } parserCtl.await(); return classNames; }Copy the code
As you can see, a locking CountDownLatch is used to open the thread pool to scan the classnames of the packages specified in each DEX file. The main thread must wait for all dex files to be scanned, which may be time-consuming if there are multiple dex files. However, since ARouter caches this, any classnames found will be cached in sp, and will be fetched directly from the cache the next time they are loaded. Only the first entry into the application will be time-consuming.
The aim of the arouter-Gradle-plugin is to reduce even the time it takes to enter an application for the first time.
How to analyze the module arouter-gradle-plugin? Arouter-gradle-plugin is first and foremost a plug-in. To analyze a plug-in, start with its plug-in class.
The arouter-gradle-plugin module starts with the PluginLaunch class:
public class PluginLaunch implements Plugin<Project> {
@Override
public void apply(Project project) {
def isApp = project.plugins.hasPlugin(AppPlugin)
if (isApp) {
def android = project.extensions.getByType(AppExtension)
def transformImpl = new RegisterTransform(project)
//init arouter-auto-register settings
ArrayList<ScanSetting> list = new ArrayList<>(3)
list.add(new ScanSetting('IRouteRoot'))
list.add(new ScanSetting('IInterceptorGroup'))
list.add(new ScanSetting('IProviderGroup'))
RegisterTransform.registerList = list
//register this plugin
android.registerTransform(transformImpl) //1
}
}
}
Copy the code
You can see that it is a typical plug-in, and you can determine that only the Application module needs to run the associated logic.
Note 1 registers a transformImpl. The transformImpl is of type RegisterTransform and inherits from Transform. Transform makes changes to the class file or resource file during gradle build from the class file to the dex file.
The key to the RegisterTransform class is in the transform method:
void transform(Context context, Collection<TransformInput> inputs , Collection<TransformInput> referencedInputs , TransformOutputProvider outputProvider , boolean isIncremental) throws IOException, TransformException, InterruptedException { boolean leftSlash = File.separator == '/' inputs.each { TransformInput input -> //1 input.jarInputs.each { JarInput jarInput -> String destName = jarInput.name def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath) if (destName.endsWith(".jar")) { destName = destName.substring(0, destName.length() - 4) } File src = jarInput.file File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR) if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) { ScanUtil.scanJar(src, dest) //2 } FileUtils.copyFile(src, dest) } // scan class files input.directoryInputs.each { DirectoryInput directoryInput -> File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) String root = directoryInput.file.absolutePath if (! root.endsWith(File.separator)) root += File.separator directoryInput.file.eachFileRecurse { File file -> def path = file.absolutePath.replace(root, '') if (! leftSlash) { path = path.replaceAll("\\\\", "/") } if(file.isFile() && ScanUtil.shouldProcessClass(path)){ ScanUtil.scanClass(file) //3 } } // copy to dest FileUtils.copyDirectory(directoryInput.file, dest) } } if (fileContainsInitClass) { registerList.each { ext -> if (ext.classList.isEmpty()) { } else { RegisterCodeGenerator.insertInitCodeTo(ext) //4 } } } }Copy the code
Transform inputs jar and Directory note 1 identifies the inputs for transform. Looking at comment 2, call scanJar on ScanUtils and let’s see what it looks like:
static void scanJar(File jarFile, File destFile) {
if (jarFile) {
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) { //1
InputStream inputStream = file.getInputStream(jarEntry)
scanClass(inputStream) //2
inputStream.close()
} else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
// mark this jar file contains LogisticsCenter.class
// After the scan is complete, we will generate register code into this file
RegisterTransform.fileContainsInitClass = destFile //3
}
}
file.close()
}
}
Copy the code
ROUTER_CLASS_PACKAGE = ROUTER_CLASS_PACKAGE = ROUTER_CLASS_PACKAGE = ROUTER_CLASS_PACKAGE = ROUTER_CLASS_PACKAGE
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.registerList.each { ext -> if (ext.interfaceName && interfaces ! = null) { interfaces.each { itName -> if (itName == ext.interfaceName) { //fix repeated inject init code when Multi-channel packaging if (! ext.classList.contains(name)) { ext.classList.add(name) } } } } } } }Copy the code
ScanClass determines whether the class implements the relevant interfaces. The relevant interfaces are IRouteRoot, IInterceptorGroup, and IProviderGroup. If any of these interfaces are implemented (and in fact should not and should not be allowed to implement more than one), its className is added to the corresponding classList in preparation for later insertion of bytecode.
Let’s go back to scanJar method of ScanUtils. Note 3 finds fileContainsInitClass, which is logisticScenter. Java, ARouter then inserts the bytecode into the loadRouterMap method of LogisticsCenter.
Return to the transform method of the RegisterTransform class, note 3: DirectoryInputs need to be scanned in the same way as jarInputs.
If the fileContainsInitClass (LogisticsCenter) class is found, then the insertInitCodeTo method of RegisterCodeGenerator is called:
static void insertInitCodeTo(ScanSetting registerSetting) { if (registerSetting ! = null && ! registerSetting.classList.isEmpty()) { RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting) File file = RegisterTransform.fileContainsInitClass if (file.getName().endsWith('.jar')) processor.insertInitCodeIntoJarFile(file) } }Copy the code
InsertInitCodeTo related initialization and sentenced to empty, calls the insertInitCodeIntoJarFile method:
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 (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) { //1
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
}
Copy the code
If the class name is GENERATE_TO_CLASS_FILE_NAME, then LogisticsCenter is the class. Then call the referHackWhenInit method and insert the bytecode into LogisticsCenter:
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() }Copy the code
This is where the final ASM bytecode implementation logic is inserted. As you can see in a typical ASM code, MyClassVisitor processes the input stream. MyClassVisitor looks like this:
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) //generate code into this method if (name == ScanSetting.GENERATE_TO_METHOD_NAME) { mv = new RouteMethodVisitor(Opcodes.ASM5, mv) } return mv } } class RouteMethodVisitor extends MethodVisitor { RouteMethodVisitor(int api, MethodVisitor mv) { super(api, mv) } @Override void visitInsn(int opcode) { //generate code before return if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) { extension.classList.each { name -> name = name.replaceAll("/", ". "). The mv visitLdcInsn (name) / / the name of the class / / generate invoke the register method into LogisticsCenter. LoadRouterMap () mv.visitMethodInsn(Opcodes.INVOKESTATIC , ScanSetting.GENERATE_TO_CLASS_NAME , ScanSetting.REGISTER_METHOD_NAME , "(Ljava/lang/String;) V" , false) } } super.visitInsn(opcode) } @Override void visitMaxs(int maxStack, int maxLocals) { super.visitMaxs(maxStack + 4, maxLocals) } }Copy the code
The key to the RouteMethodVisitor is the visitInsn method, which determines whether opCode is the returned opCode, and if so, inserts the various bytecodes before doing so. Since the operand stack is changed, the visitMaxs method also needs to be overwritten to change the maximum operand stack.
Since this article is not dedicated to ASM, the ANALYSIS of ASM will be skipped (nor will the author)
Let’s look at the effect of ASM insertion as shown in figure (apK decompiled code, actually inserted bytecode: dex2jar+ JD-gui) :
At this point, the logic of arouter-gradle-plugin is completed.
conclusion
So far, we’ve answered some of the questions posed at the beginning of this article:
1. What if we annotate the same path? If one SecondActivity uses a/ A /b path and the other ThirdActivity uses a/ a/b path, will the compilation pass? If so, which Activity is retrieved through path?
A: If you are in the same module, since the ARouter source code uses Set, you get the first elements of the alphabet. If in a different module, the compilation will not work (as in question 2).
2. What if there are two activities in the same group under different Modules? Module1 has a SecondActivity path that uses/A /c, and module2 has a ThirdActivity path that also uses /a/d.
Answer: Compile however. Because the same group file is generated, an error is reported when dex is merged.
ARouter can also be used to retrieve services. If the service is discovered through an interface, what will happen if there is more than one implementation of the interface?
A: If there is more than one implementation of the interface, and the implementation of the interface is annotated with path, the alphabetic interface will override the alphabetic interface.
4. ARouter service, why can’t we inherit the IProvider from an abstract class and implement the abstract class instead of inheriting the IProvider from an interface and implementing the interface?
Answer: ARouter only handles the interface case, not the abstract class.
5. Is it the same object or a different object each time you get the same service path from ARouter?
A: Every time a service is discovered through ARouter’s interface, the same object is retrieved.
6. What is the purpose of the arouter-gradle-plugin? ARouter added apK and the first load will take time.
A: Arouter -gradle-plugin is a plugin used by Arouter to speed up the first time an application is installed. If a plug-in is used, the bytecode is inserted directly by ASM, eliminating the need to scan all classnames in the specified package name at runtime. When ARouter is added to the APK, the main thread must wait for the child thread to scan all the classnames in the specified package name. If ARouter is added to the APK, the first load will take time.