The purpose of component-based development is to improve the reuse of business for decoupling, each business is independent of each other, how to jump page and data transmission has become the primary problem to be solved, Arouter framework of Ali provides a way of thinking for component-based transformation, as a common framework in development, it is necessary to know its implementation principle. Today we will analyze a wave of common modules arouter-API and arouter-compiler source implementation.

A, arouter – the compiler

1. Change your mind

In common development, there are often some repetitive and boring template code that needs to be manually typed, such as findViewById, setOnClickListener, etc., in the early days of development, no amount of such code will improve, and even give the impression that Android development has been fixed. It wasn’t until frameworks like ButterKnife showed up that we could take the templated code and hand it over to the annotation processor to automatically generate helper files in which the templated code was created. Not just the findViewById operation, but anything that is regular and repetitive during development can be implemented in this way. Arouter-compiler is a compiler that automatically generates template files for the Arouter framework at compile time.

2. The processor is introduced

Annotation Processing Tool (APT) is a Tool for Annotation Processing. It detects source code files, finds annotations, and automatically generates codes according to annotations. Use APT advantage is convenient, simple, can write a lot of repeated code. There are three major annotation handlers in Arouter: AutowiredProcessor, InterceptorProcessor, and RouteProcessor all inherit from BaseProcessor. Routing table creation is related to RouteProcessor. Later we will focus on the analysis of RouteProcessor source code.

3.BaseProcessor

The BaseProcessor is Arouter’s annotation processor base class. Let’s take a look at its implementation:

BaseProcessor.java @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); . // Attempt to get user configuration [moduleName] Map<String, String> options = processingenv.getoptions (); if (MapUtils.isNotEmpty(options)) { moduleName = options.get(KEY_MODULE_NAME); ModuleName generateDoc = VALUE_ENABLE. Equals (options.get(KEY_GENERATE_DOC_NAME)); } if (StringUtils.isNotEmpty(moduleName)) { moduleName = moduleName.replaceAll("[^0-9a-zA-Z_]+", ""); // Replace moduleName with null logger.info("The user has configuration The module name, it was [" + moduleName + "]"); } else { logger.error(NO_MODULE_NAME_TIPS); throw new RuntimeException("ARouter::Compiler >>> No module name, for more information, look at gradle log."); }}Copy the code

In init, getOptions() will first fetch the map options with the key “AROUTER_MODULE_NAME”, which is the configuration item in build.gradle at the module levelValue is moduelName. ModuleName replaces non-numeric and lowercase characters with null characters.

The main thing a BaseProcessor does is take the moduleName and modify it according to certain rules.

4.RouteProcessor

There are two global variables in RouteProcessor:

private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta.
private Map<String, String> rootMap = new TreeMap<>();  // Map of root metas, used for generate class file in order.
Copy the code

The data sources stored in the two maps will be analyzed below. Here, we only need to pay attention to the groupMap value is Set and rootMap is a TreeMap type. Take a look at the annotation handler’s core method, Process:

RouteProcessor.java @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (CollectionUtils.isNotEmpty(annotations)) { Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class); Try {logger.info(">>> Found routes, start... < < < "); this.parseRoutes(routeElements); } catch (Exception e) {logger.error(e); } return true; } return false; }Copy the code

Process takes all the elements annotated with Route and returns a set, which is then passed to parseRoutes() as arguments. Take a look at the parseRoutes() implementation

RouteProcessor.java private void parseRoutes(Set<? extends Element> routeElements) throws IOException { if (CollectionUtils.isNotEmpty(routeElements)) { // prepare the type an so on. logger.info(">>> Found routes, size is " + routeElements.size() + " <<<"); rootMap.clear(); . For (Element Element: routeElements) {TypeMirror tm = element.astype (); Route route = element.getAnnotation(Route.class); RouteMeta routeMeta; // Activity or Fragment if (types.isSubtype(tm, type_Activity) || types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) { // Get all fields annotation by @Autowired Map<String, Integer> paramsType = new HashMap<>(); Map<String, Autowired> injectConfig = new HashMap<>(); injectParamCollector(element, paramsType, injectConfig); if (types.isSubtype(tm, type_Activity)) { // Activity logger.info(">>> Found activity route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType); } else { // Fragment logger.info(">>> Found fragment route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), paramsType); } routeMeta.setInjectConfig(injectConfig); } else if (types.isSubtype(tm, iProvider)) { // IProvider logger.info(">>> Found provider route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null); } else if (types.isSubtype(tm, type_Service)) { // Service logger.info(">>> Found service route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null); } else { throw new RuntimeException("The @Route is marked on unsupported class, look at [" + tm.toString() + "]."); } categories(routeMeta); // Key 2}..... For (map.entry <String, Set<RouteMeta>> Entry: groupmap.entrySet ()) {String groupName = entry.getKey(); MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(groupParamSpec); List<RouteDoc> routeDocList = new ArrayList<>(); // Build group method body Set<RouteMeta> groupData = entry.getValue(); for (RouteMeta routeMeta : groupData) { ..... Omit part of the code loadIntoMethodOfGroupBuilder. AddStatement (" atlas. The put ($S, $T.b uild ($t. "+ routeMeta. GetType () +", $tc lass, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", routeMeta.getPath(), routeMetaCn, routeTypeCn, className, routeMeta.getPath().toLowerCase(), routeMeta.getGroup().toLowerCase()); Routedoc.setclassname (classname.tostring ()); routeDocList.add(routeDoc); } // Generate groups String groupFileName = NAME_OF_GROUP + groupName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(groupFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_IRouteGroup)) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfGroupBuilder.build()) .build() ).build().writeTo(mFiler); / / / / four key logger. The info (" > > > Generated group: "+ groupName +" < < < "); rootMap.put(groupName, groupFileName); Docsource. put(groupName, routeDocList); } if (MapUtils.isNotEmpty(rootMap)) { // Generate root meta by group name, it must be generated before root, then I can find out the class of group. for (Map.Entry<String, String> entry : rootMap.entrySet()) { loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue())); // Key 6}}...... // Write root meta into disk. String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName; // Write root meta into disk. String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(rootFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT))) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfRootBuilder.build()) .build() ).build().writeTo(mFiler); Logger. info(">>> Generated root, name is "+ rootFileName +" <<<"); }}Copy the code

ParseRoutes () has more points to focus on, one by one

  • Point 1: Create a RouteMeta by type by iterating through all the elements obtained through the Route annotation
  • Point 2: Add the routing metadata RouteMeta to the Set
/** * Sort metas in group. * * @param routeMete metas. */ private void categories(RouteMeta routeMete) { if RouteVerify (routeMete) {// Verify the existence of a routeMete group name. Logger.info (">>> Start categories, group = "+ routeMete. GetGroup () + ", path = " + routeMete.getPath() + " <<<"); Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup()); If (collectionUtils.isempty (routeMetas)) {collectionUtils.isempty (routeMetas)) { RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {@override public int compare(RouteMeta r1, RouteMeta r2) { try { return r1.getPath().compareTo(r2.getPath()); } catch (NullPointerException npe) { logger.error(npe.getMessage()); return 0; }}}); routeMetaSet.add(routeMete); groupMap.put(routeMete.getGroup(), routeMetaSet); Routemetas.add (routeMete);} else {routemetas.add (routeMete); } } else { logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<"); }}Copy the code

In categories(), the existence of the group is first verified. If not, the string after the first ‘/’ of the path is truncated as the group name (routeVerify()). Then, based on the group name, the set of routing metadata under a group is obtained from the groupMap.

  • If a set already exists, it is directly added to a set. The set feature cannot add the same element (treeSet sorts by path, RouteMeta objects with the same path are treated as the same object), which will result in the failure to add the same group and path elements. At the APP level, if two classes with the same path are created under module, the classes with the first alphabet in the class name can be obtained or jumped through routes.
  • If not, create TreeSet, add data in alphabetical order of routing metadata path, add routeMete to set, add set to groupMap.

So far we know that groupMap stores all routemetas in the same group.

  • Add statement to loadInto() for Arouter$$Group$$groupname.java, add routeMeta to atlas as specified:

  • Arouter$$Group$$GroupName;

  • Key 5: Save group names and corresponding group files to rootMap, which stores group file names generated by all groups.
  • Add statement to loadInto() for Arouter$$Root$$ModuleName. Extends IRouteGroup>> routes Adds to routes:

  • Generate root file: Arouter$$root $$ModuleName

5. Conclusion:

Arouter – Compiler is used to process annotations in Arouter. After the introduction of arouter framework, group files, root files, provider files, etc., are generated in a certain style in the compilation stage, and the method implementation in the file is also specified by the annotation processor.

For Java projects: the path to automatically generate files is module\build\generated\ap_generated_sources\debug\out\com\alibaba\ Android \arouter\routes.

For Kotlin or Java and Kotlin hybrid projects: the path to automatically generate files is module\build\generated\source\kapt\debug\com\ Alibaba \ Android \arouter\routes.

Second, the arouter – API

1. Init () source analysis

Arouter-api is the API used by the application layer. We can look at the source code from arouter.init () (version 1.5.0).

Arouter.java
  
    public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            hasInit = _ARouter.init(application);

            if (hasInit) {
                _ARouter.afterInit();
            }

            _ARouter.logger.info(Consts.TAG, "ARouter init over.");
        }
    }
Copy the code

AfterInit () _arouter.init () _arouter.afterinit () _arouter.init ()

_ARouter.java protected static synchronized boolean init(Application application) { mContext = application; LogisticsCenter.init(mContext, executor); logger.info(Consts.TAG, "ARouter init success!" ); hasInit = true; mHandler = new Handler(Looper.getMainLooper()); return true; }Copy the code

Logisticscenter.init (mContext, executor) is called by _arouter.init (). Note that the second argument passes in a thread pool object.

_ARouter.java

ThreadPoolExecutor executor = DefaultPoolExecutor.getInstance();

DefaultPoolExecutor.java

 public static DefaultPoolExecutor getInstance() {
        if (null == instance) {
            synchronized (DefaultPoolExecutor.class) {
                if (null == instance) {
                    instance = new DefaultPoolExecutor(
                            INIT_THREAD_COUNT,
                            MAX_THREAD_COUNT,
                            SURPLUS_THREAD_LIFE,
                            TimeUnit.SECONDS,
                            new ArrayBlockingQueue<Runnable>(64),
                            new DefaultThreadFactory());
                }
            }
        }
        return instance;
    }
Copy the code

The thread pool uses an array blocking queue of capacity 64 that sorts elements FIFO(first in, first out). Using the classic “bounded buffer,” a fixed-size array holds elements that are inserted by producers and taken out by consumers. Block the queue when an element is put into a queue that is already at its maximum capacity, and block the queue when an element is removed from a queue that is empty. ArrayBlockingQueue logisticScenter.init (mContext, executor)

LogisticsCenter.java /** * LogisticsCenter init, load all metas in memory. Demand initialization */ public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException { mContext = context; executor = tpe; try { ...... If (registerByPlugin) {logger.info(TAG, "Load router map by arouter-auto-register plugin."); } else { Set<String> routerMap; // It will rebuild router map every times when debuggable. if (ARouter.debuggable() || PackageUtils. IsNewVersion (context)) {/ / a key logger. The info (TAG, "Run with the debug mode or a new install, rebuild the router map."); // These class was generated by arouter-compiler. routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); // if (! routerMap.isEmpty()) { context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply(); / / key 3} PackageUtils. UpdateVersion (context). // Save new version name when router map update finishes. } else { logger.info(TAG, "Load router map from cache."); routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>())); Logger. Info (TAG, "Find router map finished, map size = "+ RouterMap.size () + ", cost " + (System.currentTimeMillis() - startInit) + " ms."); startInit = System.currentTimeMillis(); 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) {throw new HandlerException(TAG + "ARouter init Logistics Center Exception! [" + e.getMessage() + "]"); }}Copy the code
  • Key Point 1: If the debug or APP version (versionName or versionCode) changes, rebuild the routing table.
  • Point 2: obtain all dex file, all access to the file name from dex file, iterate through all the files to filter out “com. Alibaba. Android. Arouter. Routes” began to file (in the above definition of traverse thread pool filter), forming the routing table.
  • Key 3: Store the routing table in SharedPreferences so that you can directly obtain the routing table from SharedPreferences next time. Then store the versionCode and versionName in SharedPreferences and update the version.
  • Key 4: The routing table data is not debug and the version is not changed.
  • Key 5: Traverse the routing table and add it to different map sets of Warehouse according to different types.

The following Warehouse code stores routing metadata and other data.

/** * Storage of route meta and other data. * * @author zhilong <a href="mailto:[email protected]">Contact Me.</a> * @version 1.0 * @since 2017/2/23 PM 1:39 */ class Warehouse {// Cache route and metas static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); static Map<String, RouteMeta> routes = new HashMap<>(); // Cache provider static Map<Class, IProvider> providers = new HashMap<>(); static Map<String, RouteMeta> providersIndex = new HashMap<>(); // Cache interceptor static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]"); static List<IInterceptor> interceptors = new ArrayList<>(); static void clear() { routes.clear(); groupsIndex.clear(); providers.clear(); providersIndex.clear(); interceptors.clear(); interceptorsIndex.clear(); }}Copy the code

Here’s an implementation of _arouter.afterinit () :

  _ARouter.java

  static void afterInit() {
        // Trigger interceptor init, use byName.
        interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }
Copy the code

The logic is simple: Assign the interceptorService value through ARouter.

2.navigation() source code analysis

Navigation () passes multiple layers of method overloading and finally calls:

_ARouter.java protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { ...... Omit part of the code LogisticsCenter.com pletion (it); // focus 1...... If (! postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR. interceptorService.doInterceptions(postcard, new InterceptorCallback() { /** * Continue process * * @param postcard route meta */ @Override public void onContinue(Postcard postcard) { _navigation(context, postcard, requestCode, callback); } /** * Interrupt process, pipeline will be destory when this method called. * * @param exception Reson of interrupt. */ @Override public void onInterrupt(Throwable exception) { if (null ! = callback) { callback.onInterrupt(postcard); } logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage()); }}); } else { return _navigation(context, postcard, requestCode, callback); } return null; }Copy the code
  • Point 1: Assign values to the fields of the postcard.
  • Key 2: Redirects the page or obtains fragments, services, and providers without interceptors.
LogisticsCenter.java /** * Completion the postcard by route metas * * @param postcard Incomplete postcard, should complete by this method. */ public synchronized static void completion(Postcard postcard) { if (null == postcard)  { throw new NoRouteFoundException(TAG + "No postcard!" ); } RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath()); If (null == routeMeta) {// Maybe its does not exist, or didn't load. Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta. if (null == groupMeta) { throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]"); } else { // Load route and cache it into memory, then delete from metas. try { if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath())); } IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); iGroupInstance.loadInto(Warehouse.routes); / / key 2 Warehouse. GroupsIndex. Remove (it) getGroup ()); If (ARouter. Debuggable ()) {logger.debug(TAG, string.format (locale.getdefault ()), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath())); } } catch (Exception e) { throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "] "); } completion(postcard); // Reload key 4}} else {// key 5 postcard.setdestination (routemeta.getDestination ()); postcard.setType(routeMeta.getType()); postcard.setPriority(routeMeta.getPriority()); postcard.setExtra(routeMeta.getExtra()); . 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); 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; case FRAGMENT: postcard.greenChannel(); // Fragment needn't interceptors default: break; }}}Copy the code
  • Key point 1: Get the RouteMeta based on path from the Warehouse. Routes collection
  • RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta = RouteMeta
  • Remove Group file Arouter$$Group$$groupname. Java from Warehouse. GroupsIndex
  • Warehouse. Routes Add data and reload.
  • Key five: Set the fields for the postcard.
_ARouter.java private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { final Context currentContext = null == context ? mContext : context; Switch (postcard.getType()) {Case ACTIVITY:// Build Intent Final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); // Set flags. int flags = postcard.getFlags(); if (-1 ! = flags) { intent.setFlags(flags); } else if (! (currentContext instanceof Activity)) { // Non activity, need less one flag. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } // Set Actions String action = postcard.getAction(); if (! TextUtils.isEmpty(action)) { intent.setAction(action); } // Navigation in main looper. runInMainThread(new Runnable() { @Override public void run() { startActivity(requestCode, currentContext, intent, postcard, callback); }}); break; Case PROVIDER:// key two return postcard.getProvider(); Case BOARDCAST: case CONTENT_PROVIDER: Case FRAGMENT:// focus Class fragmentMeta = postcard.getDestination(); try { Object instance = fragmentMeta.getConstructor().newInstance(); if (instance instanceof Fragment) { ((Fragment) instance).setArguments(postcard.getExtras()); } else if (instance instanceof android.support.v4.app.Fragment) { ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras()); } return instance; } catch (Exception ex) { logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace())); } case METHOD: case SERVICE: default: return null; } return null; }Copy the code

_arouter._navigation () is the final place to jump to the page or get instances of fragments, services, providers, etc.

  • Point 1: When the type of the postcard is Activity, navigation() finally jumps to startActivity().
  • Note 2: When the type of the postcard is Provider, Navigation () returns the Provider instance.
  • Note 3: When the type of the postcard is Fragment, Navigation () returns the fragment instance.

Third, summary

Arouter obtains all elements with Route annotation through annotation processor RouteProcessor during compilation, stores all elements in groupMap and rootMap according to different group names, traverses groupMap and rootMap, and generates group files and root files.

When the app is running, init() decides whether to rebuild the routing table or use the routing table cached in SharedPreferences, depending on whether the version has changed and whether it is in debug mode. Then the routing table is traversed and added to different maps of Warehouse according to different types.

After using Navigation () to jump to the Activity or obtain the instance of fragment, provider and other objects, add the data in the index map of Warehouse to the metadata map. Then call completion() again to set routing information for the postcard. Based on the route information and the postcard type, you can decide whether to go to another activity or return the fragment or Provider instances.