ARouter principle analysis and sharing

foreword

In the hot summer, I don't know how everyone's weekend will be arranged. This article will share with you the principle of ARouter. By understanding its principle, we can know how it supports componentization and calls or page jumps between modules that do not depend on each other.

text

1.ARouter introduction

ARouter is a component-based routing framework open sourced by Alibaba. It can help page jumps and service calls between independent components.

2.ARouter use

Add dependencies:

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 to 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 framework:

class RouterDemoApp : Application() {
    override fun onCreate() {
        super.onCreate()
      //初始化、注入
        ARouter.init(this)
    }
}

Call jump:

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

3. Generated code (generated routing table)

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

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, the group corresponds to the class of the group, and each group is the routing table under the group.

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

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

//#ARouter
public static void init(Application application) {
    if (!hasInit) {
      //...省略部分代码
        hasInit = _ARouter.init(application);
 			//...省略部分代码
    }
}

//#_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 for initialization looks to be in the LogisticsCenter:

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

    try {
        //...省略代码
        if (registerByPlugin) {
            //...省略代码
        } else {
            Set<String> routerMap;

            // 如果是debug包或者更新版本
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
               //获取在com.alibaba.android.arouter.routes下的所以class类名
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
              //更新到sp中
                if (!routerMap.isEmpty()) {
                    context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                }
								//更新版本
                PackageUtils.updateVersion(context); 
            } else {
                //直接从缓存拿出之前存放的class
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }
						
          //遍历routerMap,将group的类加载到缓存中
            for (String className : routerMap) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                  //生成的Root、比如我们上面示例的ARouter$$Root$$app,调用loadInto相当于加载了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)) {
                    //加载拦截器,例如生成的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)) {
                    // 加载Provider,例如生成的ARouter$$Providers$$app
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }
        }
        //...省略代码
    } 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, then go to get all the class names under com.alibaba.android.arouter.routes, then update it to sp, and update the version number. Then load IRouteRoot through reflection to load the group and the corresponding class object, and also load the interceptor, Provider.

Here we 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();
  //获取到dex文件路径
    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 {
                  //加载出dexfile文件
                    if (path.endsWith(".zip")) {
                        dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                    } else {
                        dexfile = new DexFile(path);
                    }

                    Enumeration dexEntries = dexfile.entries();
									//	遍历dexFile里边的元素,加载出.class文件
                    while(dexEntries.hasMoreElements()) {
                        String className = (String)dexEntries.nextElement();
                      //开头"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 {
                    //...省略代码
                    parserCtl.countDown();
                }
            }
        });
    }

    parserCtl.await();
  	//。。。省略代码
    return classNames;
}

The core logic in this method is to load the path of the dex file, then construct the DexFile through the path, and traverse the elements in it after construction. If it is a class file starting with com.alibaba.android.arouter.routes, save it to the list Waiting to 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";
  //是否开启了multidex,如果开启的话,则需获取每个dex路径
    if (!isVMMultidexCapable()) {
        int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
        File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
				//遍历每一个dex文件
        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;
}

getSourcePaths的功能就是获取app的所有dex文件的路径,为后面转成class文件从而获取class文件路径提供数据。

小结:

  • ARouter.init(this)调用交给了内部的_ARouter.init(application),然后真正做事的是LogisticsCenter.init(mContext, executor)
  • 如果是debug包或者升级版本,则去加载出com.alibaba.android.arouter.routes包下的dex文件的路径,并且更新到缓存里边
  • 通过这些dex去获取对应的所有class文件的路径
  • 最后根据类名的前缀加载到Warehouse中对应的map里,其中就有group、interceptor和provider

5.调用及处理

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

build会构建一个Postcard对象出来:

//#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);
            }
          //extractGroup方法就是从path中提取出group,比如"/test/router_activity",test便是提取出来的group
            return build(path, extractGroup(path));
        }
    }

build(path, group)方法最终会构建一个Postcard对象出来。

构建好PostCard之后,调用它的navigation方法便可以实现我们的跳转或者获取对应的实体。navigation方法最后会调用到_ARouter的navigation方法:

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    //...省略代码
    try {
      //1.根据postCard的group加载路由表,并且补全postCard的信息
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        //...异常处理
        return null;
    }
    if (null != callback) {
        callback.onFound(postcard);
    }

  //如果不是绿色通道的话,需要走拦截器的逻辑,否则会跳过拦截器
    if (!postcard.isGreenChannel()) { 
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
              //2.真正实现动作处理
                _navigation(context, postcard, requestCode, callback);
            }
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }
								//...省略代码
            }
        });
    } else {
      //2.真正实现动作处理
        return _navigation(context, postcard, requestCode, callback);
    }
    return null;
}

navigation方法的核心逻辑为:加载路由表,并且补全postCard的信息,然后真正处理跳转或者请求逻辑。

LogisticsCenter.completion(postcard)的核心源码如下:

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在init的时候已经加载好了,这里就可以通过group获取到对应group的class对象
        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 {
                //...省略代码
                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
              //将group里边的路由表加载进内存,我们最开始的例子想当于执行:atlas.put("/test/router_activity", RouteMeta.build(RouteType.ACTIVITY, RouterActivity.class, "/test/router_activity", "test", null, -1, -2147483648));
                iGroupInstance.loadInto(Warehouse.routes);
              //因为加载路由表了,所以可以将group从内存中移除,节省内存
                Warehouse.groupsIndex.remove(postcard.getGroup());
								//...省略代码
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }
						//已经将group里的路由表加载出来了,再执行一遍函数。
            completion(postcard);   // Reload
        }
    } else {
      	//	第二次时,给postCard填补信息
        postcard.setDestination(routeMeta.getDestination());
        postcard.setType(routeMeta.getType());
        postcard.setPriority(routeMeta.getPriority());
        postcard.setExtra(routeMeta.getExtra());
			//...省略代码,主要是解析uri然后参数的赋值

      //根据路由获取的不同的类型,继续补充一些信息给postCard
        switch (routeMeta.getType()) {
            case PROVIDER:  
            		//...省略代码,主要是补充一些其他参数
                postcard.greenChannel();    // Provider should skip all of interceptors
                break;
            case FRAGMENT:
                postcard.greenChannel();    // Fragment needn't interceptors
            default:
                break;
        }
    }
}

补充完postCard信息之后,接下来我们看看_navigation方法:

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:
            //构造Intent,然后切换到主线程,并且跳转到指定的Activity
            break;
        case PROVIDER:
            return postcard.getProvider();
        case BOARDCAST:
        case CONTENT_PROVIDER:
        case FRAGMENT:
        	//反射构造出实例并返回
        case METHOD:
        default:
            return null;
    }

    return null;
}

可以看到,最终会根据不同的type,去做出不同的响应,例如ACTIVITY的话,会进行activity的跳转,其他的会通过反射构造出实例返回等操作。

小结:

  • 调用的最开始,会构建一个PostCard对象,初始化path和group
  • navigation方法最终会调用到_ARouter的navigation方法,然后通过LogisticsCenter.completion(postcard)去加载group里边的路由表,并且补全postcard信息。
  • 如果是有绿色通道的话,则不执行拦截器,直接跳过,否则需要执行拦截器。
  • 最后便是通过不同类型执行对应的操作。

结语

本文的分享到这里就结束了,相信看完后,能够对ARouter的原理有了一定的理解,以便我们后面如果有使用到它的时候,能够更好的地使用,或者为项目定制出路由框架提供了很好的思路参考。同时,这么优秀的框架也值得我们去学习它的一些设计思路。

Guess you like

Origin juejin.im/post/7118385586850758686