Learning record of ARouter principle

preface

The first share after the Spring Festival, the working state is also slowly found back. This article will share with you the principle of ARouter. By understanding its principle, we can know how it supports calls between componentized and non interdependent modules or page jumps.

text

Introduction to ARouter

ARouter is a component-based routing framework open source by Alibaba. It can help page Jump and service call between independent components.

ARouter use

Add dependency:

android {
  //...
  defaultConfig {
       kapt {
            arguments {
                arg("AROUTER_MODULE_NAME", project.getName())
            }
        }
  }
  //...
}

dependencies {
    api 'com.alibaba:arouter-api:1.5.0'
    kapt 'com.alibaba:arouter-compiler:1.2.2'
}

Define the path to jump Activity:

@Route(path = "/test/router_activity")
class RouterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_router)
    }
}

Initialize the Router frame:

class RouterDemoApp : Application() {
    override fun onCreate() {
        super.onCreate()
      //Initialization and injection
        ARouter.init(this)
    }
}

Call jump:

ARouter.getInstance().build("/test/router_activity").navigation()

Generated code (generated routing table)

When we annotate the Activity or service with Route and build it, the ARouter framework will help us generate java files according to the template, which can be accessed at runtime. The technology used is apt technology. Let's take a look at the code generated by the above example:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("test", ARouter$$Group$$test.class);
  }
}

public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/test/router_activity", RouteMeta.build(RouteType.ACTIVITY, RouterActivity.class, "/test/router_activity", "test", null, -1, -2147483648));
  }
}

According to the code generated above, it can be seen that the generated code is a routing table. First, correspond the group to the class of the group. In each group, there is the routing table under the group.

Initialize init() (the group that loads the routing table)

Next, let's look at what is done in the routing framework during initialization:

//#ARouter
public static void init(Application application) {
    if (!hasInit) {
      //... Omit some codes
        hasInit = _ARouter.init(application);
 			//... Omit some codes
    }
}

//#_ARouter
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;
    }

The core code of initialization appears in the LogisticsCenter:

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    mContext = context;
    executor = tpe;

    try {
        //... Omit code
        if (registerByPlugin) {
            //... Omit code
        } else {
            Set<String> routerMap;

            // If it is a debug package or a newer version
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
               //Get on COM alibaba. android. arouter. All class names under routes
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
              //Update to sp
                if (!routerMap.isEmpty()) {
                    context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                }
								//Updated version
                PackageUtils.updateVersion(context); 
            } else {
                //Take out the previously stored class directly from the cache
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }
						
          //Traverse routerMap and load the class of group into the cache
            for (String className : routerMap) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                  //For the generated Root, such as ARouter$$Root$$app in our example above, calling loadInto is equivalent to loading routes put("test", ARouter$$Group$$test.class)
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    //Load interceptors, such as the generated ARouter$$Interceptors$$app
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    // Load providers, such as the generated ARouter$$Providers$$app
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }
        }
        //... Omit code
    } catch (Exception e) {
        throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
    }
}

The core logic above is that if it is a debug package or an updated version, go to get com alibaba. android. arouter. All class class names under routes are updated to sp and the version number is updated. Then load IRouteRoot through reflection to load groups and corresponding class objects. In addition, it will also load interceptors and providers.

Here, let's focus on the method getFileNameByPackageName to get the class file path:

public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws NameNotFoundException, IOException, InterruptedException {
    final Set<String> classNames = new HashSet();
  //Get dex file path
    List<String> paths = getSourcePaths(context);
    final CountDownLatch parserCtl = new CountDownLatch(paths.size());
    Iterator var5 = paths.iterator();

    while(var5.hasNext()) {
        final String path = (String)var5.next();
        DefaultPoolExecutor.getInstance().execute(new Runnable() {
            public void run() {
                DexFile dexfile = null;
                try {
                  //Load out the dexfile file
                    if (path.endsWith(".zip")) {
                        dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                    } else {
                        dexfile = new DexFile(path);
                    }

                    Enumeration dexEntries = dexfile.entries();
									//	Traverse the elements in the dexFile and load it class file
                    while(dexEntries.hasMoreElements()) {
                        String className = (String)dexEntries.nextElement();
                      //Start with "com.alibaba.android.arouter.routes"
                        if (className.startsWith(packageName)) {
                            classNames.add(className);
                        }
                    }
                } catch (Throwable var12) {
                    Log.e("ARouter", "Scan map file in dex files made error.", var12);
                } finally {
                    //... Omit code
                    parserCtl.countDown();
                }
            }
        });
    }

    parserCtl.await();
  	//. . .  Omit code
    return classNames;
}

The core logic of this method is to load the path of the dex file, then build the DexFile through the path, and traverse the elements inside it after construction, if it is com alibaba. android. arouter. The class file starting with routes is saved in the list and waiting for return.

getSourcePaths:

public static List<String> getSourcePaths(Context context) throws NameNotFoundException, IOException {
    ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
    File sourceApk = new File(applicationInfo.sourceDir);
    List<String> sourcePaths = new ArrayList();
    sourcePaths.add(applicationInfo.sourceDir);
    String extractedFilePrefix = sourceApk.getName() + ".classes";
  //Whether multidex is enabled. If it is enabled, you need to obtain each dex path
    if (!isVMMultidexCapable()) {
        int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
        File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
				//Traverse each dex file
        for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
          //app.classes2.zip,app.classes3.zip ...
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            File extractedFile = new File(dexDir, fileName);
            if (!extractedFile.isFile()) {
                throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
            }
            sourcePaths.add(extractedFile.getAbsolutePath());
        }
    }

    if (ARouter.debuggable()) {
        sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
    }

    return sourcePaths;
}

The function of getSourcePaths is to obtain the paths of all dex files of the app and provide data for converting them into class files to obtain the path of class files.

Summary:

  • ARouter. The init (this) call was handed over to the internal_ ARouter.init(application), and then the logistics center init(mContext, executor)
  • If it is a debug package or an upgraded version, load com alibaba. android. arouter. The path of the dex file under the routes package and update it to the cache
  • Use these dex to get the path of all corresponding class files
  • Finally, it is loaded into the corresponding map in the Warehouse according to the prefix of the class name, including group, interceptor and provider

Call and processing

ARouter.getInstance().build("/test/router_activity").navigation()

Build will build a Postcard object:

//#Router
public Postcard build(String path) {
    return _ARouter.getInstance().build(path);
}

//#_ARouter
    protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
          //The extractGroup method is to extract the group from the path, such as "/ test / router_activity", and test is the extracted group
            return build(path, extractGroup(path));
        }
    }

The build(path, group) method will eventually build a Postcard object.

After building the PostCard, the navigation method can be used to achieve our jump or get the corresponding entities. The navigation method is finally called to_ Navigation method of ARouter:

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    //... Omit code
    try {
      //1. Load the routing table according to the group of postCard and complete the information of postCard
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        //... exception handling
        return null;
    }
    if (null != callback) {
        callback.onFound(postcard);
    }

  //If it is not a green channel, you need to follow the logic of the interceptor, otherwise you will skip the interceptor
    if (!postcard.isGreenChannel()) { 
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
              //2. Truly realize action processing
                _navigation(context, postcard, requestCode, callback);
            }
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }
								//... Omit code
            }
        });
    } else {
      //2. Truly realize action processing
        return _navigation(context, postcard, requestCode, callback);
    }
    return null;
}

The core logic of the navigation method is: load the routing table, complete the information of the postCard, and then really handle the jump or request logic.

LogisticsCenter. The core source code of completion (Postcard) is as follows:

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) {
      //groupsIndex has been loaded during init. Here you can get the class object of the corresponding group through group
        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 {
                //... Omit code
                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
              //Load the routing table in the group into memory. Our first example is to execute: Atlas put("/test/router_activity", RouteMeta.build(RouteType.ACTIVITY, RouterActivity.class, "/test/router_activity", "test", null, -1, -2147483648));
                iGroupInstance.loadInto(Warehouse.routes);
              //Because the routing table is loaded, the group can be removed from memory to save memory
                Warehouse.groupsIndex.remove(postcard.getGroup());
								//... Omit code
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }
						//The routing table in the group has been loaded. Execute the function again.
            completion(postcard);   // Reload
        }
    } else {
      	//	The second time, fill in the information for postCard
        postcard.setDestination(routeMeta.getDestination());
        postcard.setType(routeMeta.getType());
        postcard.setPriority(routeMeta.getPriority());
        postcard.setExtra(routeMeta.getExtra());
			//... Omit the code, mainly parsing the uri and then assigning the parameter

      //According to different types of route acquisition, continue to supplement some information to postCard
        switch (routeMeta.getType()) {
            case PROVIDER:  
            		//... Omit the code and mainly supplement some other parameters
                postcard.greenChannel();    // Provider should skip all of interceptors
                break;
            case FRAGMENT:
                postcard.greenChannel();    // Fragment needn't interceptors
            default:
                break;
        }
    }
}

After adding the postCard information, let's take a look_ navigation method:

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:
            //Construct Intent, then switch to the main thread and jump to the specified Activity
            break;
        case PROVIDER:
            return postcard.getProvider();
        case BOARDCAST:
        case CONTENT_PROVIDER:
        case FRAGMENT:
        	//Reflection constructs an instance and returns
        case METHOD:
        default:
            return null;
    }

    return null;
}

It can be seen that different responses will be made according to different type s. For example, in the case of activity, the jump of activity will be carried out, and other operations such as instance return will be constructed through reflection.

Summary:

  • At the beginning of the call, a PostCard object will be built to initialize path and group
  • The navigation method will eventually be called to_ The navigation method of ARouter, and then through logisticscenter Completion (Postcard) to load the routing table in the group and complete the postcard information.
  • If there is a green channel, do not execute the interceptor and skip directly, otherwise the interceptor needs to be executed.
  • Finally, perform corresponding operations through different types.

epilogue

The sharing of this article ends here. I believe that after reading it, we can have a certain understanding of the principle of ARouter, so that if we use it later, we can better use it, or provide a good idea reference for customizing the routing framework for the project. At the same time, such an excellent framework is also worth learning some of its design ideas.

Keywords: Android

Added by PandaFi on Mon, 14 Feb 2022 18:00:13 +0200