框架的初始化 - Xiasm/EasyRouter GitHub Wiki

通过前几节的讲解,我们知道了看似很复杂的路由框架,其实原理很简单,我们可以理解为一个map(其实是两个map,一个保存group列表,一个保存group下的路由地址和activityClass关系)保存了路由地址和ActivityClass的映射关系,然后通过map.get("router address") 拿到AncivityClass,通过startActivity()调用就好了。但一个框架的设计要考虑的事情远远没有这么简单。下面我们就来分析一下:

要实现这么一个路由框架,首先我们需要在用户使用路由跳转之前把这些路由映射关系拿到手,拿到这些路由关系最好的时机就是应用程序初始化的时候,前面的讲解中我贴过几行代码,是通过apt生成的路由映射关系文件,为了方便大家理解,我把这些文件重新粘贴到下面代码中(这几个类都是单独的文件,在项目编译后会在各个模块的/build/generated/source/apt文件夹下面生成,为了演示方便我只贴出来了app模块下生成的类,其他模块如module1、module2下面的类跟app下面的没有什么区别),在程序启动的时候扫描这些生成的类文件,然后获取到映射关系信息,保存起来。

public class EaseRouter_Root_app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("main", EaseRouter_Group_main.class);
    routes.put("show", EaseRouter_Group_show.class);
  }
}


public class EaseRouter_Group_main implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/main/main",RouteMeta.build(RouteMeta.Type.ACTIVITY,MainActivity.class,"/main/main","main"));
    atlas.put("/main/main2",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2Activity.class,"/main/main2","main"));
  }
}

public class EaseRouter_Group_show implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/show/info",RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show"));
  }
}

    可以看到,这些文件中,实现了IRouteRoot接口的类都是保存了group分组映射信息,实现了IRouteGroup接口的类都保存了单个分组下的路由映射信息。只要我们得到实现IRouteRoot接口的所有类文件,便能通过循环调用它的loadInfo()方法得到所有实现IRouteGroup接口的类,而所有实现IRouteGroup接口的类里面保存了项目的所有路由信息。IRouteGroup的loadInfo()方法,通过传入一个map,便会将这个分组里的映射信息存入map里。可以看到map里的value是“RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show")”,RouteMeta.build()会返回RouteMeta,RouteMeta里面便保存着ActivityClass的所有信息。那么我们这个框架,就有了第一个功能需求,便是在app进程启动的时候进行框架的初始化(或者在你开始用路由跳转之前进行初始化都可以),在初始化中拿到映射关系信息,保存在map里,以便程序运行中可以快速找到路由映射信息实现跳转。下面看具体的初始化代码。
注:这里我们只讲解大体的思路,不会细致到讲解每一个方法每一行代码的具体作用,跟着我的思路你会明白框架设计的具体细节,每一步要实现的功能是什么,但是精确到方法和每一行代码的具体含义你还需要仔细研读demo。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        EasyRouter.init(this);
    }
}

public class EasyRouter {

  private static final String TAG = "EasyRouter";
   private static final String ROUTE_ROOT_PAKCAGE = "com.xsm.easyrouter.routes";
   private static final String SDK_NAME = "EaseRouter";
   private static final String SEPARATOR = "_";
   private static final String SUFFIX_ROOT = "Root";

   private static EasyRouter sInstance;
   private static Application mContext;
   private Handler mHandler;

   private EasyRouter() {
       mHandler = new Handler(Looper.getMainLooper());
   }

   public static EasyRouter getsInstance() {
       if (sInstance == null) {
            synchronized (EasyRouter.class) {
                if (sInstance == null) {
                    sInstance = new EasyRouter();
                }
            }
        }
       return sInstance;
   }

   public static void init(Application application) {
       mContext = application;
       try {
           loadInfo();
       } catch (Exception e) {
           e.printStackTrace();
           Log.e(TAG, "初始化失败!", e);
       }
   }

   //...
}

可以看到,init()方法中调用了loadInfo()方法,而这个loadInfo()便是我们初始化的核心。

private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //获得所有 apt生成的路由类的全类名 (路由表)
    Set<String> routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    for (String className : routerMap) {
        if (className.startsWith(ROUTE_ROOT_PAKCAGE + "." + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
            //root中注册的是分组信息 将分组信息加入仓库中
            ((IRouteRoot) Class.forName(className).getConstructor().newInstance()).loadInto(Warehouse.groupsIndex);
        }
    }
    for (Map.Entry<String, Class<? extends IRouteGroup>> stringClassEntry : Warehouse.groupsIndex.entrySet()) {
        Log.d(TAG, "Root映射表[ " + stringClassEntry.getKey() + " : " + stringClassEntry.getValue() + "]");
    }

}

我们首先通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有实现IRouteRoot接口的类文件集合,通过上面的讲解我们知道,拿到这些类文件便可以得到所有的routerAddress---activityClass映射关系。
这个ClassUtils.getFileNameByPackageName()方法就是具体的实现了,下面我们看具体的代码:

   /**
     * 得到路由表的类名
     * @param context
     * @param packageName
     * @return
     * @throws PackageManager.NameNotFoundException
     * @throws InterruptedException
     */
    public static Set<String> getFileNameByPackageName(Application context, final String packageName)
            throws PackageManager.NameNotFoundException, InterruptedException {
        final Set<String> classNames = new HashSet<>();
        List<String> paths = getSourcePaths(context);
        //使用同步计数器判断均处理完成
        final CountDownLatch countDownLatch = new CountDownLatch(paths.size());
        ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size());
        for (final String path : paths) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexFile = null;
                    try {
                        //加载 apk中的dex 并遍历 获得所有包名为 {packageName} 的类
                        dexFile = new DexFile(path);
                        Enumeration<String> dexEntries = dexFile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (!TextUtils.isEmpty(className) && className.startsWith(packageName)) {
                                classNames.add(className);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        if (null != dexFile) {
                            try {
                                dexFile.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        //释放一个
                        countDownLatch.countDown();
                    }
                }
            });
        }
        //等待执行完成
        countDownLatch.await();
        return classNames;
    }

这个方法会通过开启子线程,去扫描apk中所有的dex,遍历找到所有包名为packageName的类名,然后将类名再保存到classNames集合里。
List paths = getSourcePaths(context)这句代码会获得所有的apk文件(instant run会产生很多split apk),这个方法的具体实现大家看demo即可,不再阐述。这里用到了CountDownLatch类,会分path一个文件一个文件的检索,等到所有的类文件都找到后便会返回这个Set集合。所以我们可以知道,初始化时找到这些类文件会有一定的耗时,所以ARouter这里会有一些优化,只会遍历找一次类文件,找到之后就会保存起来,下次app进程启动会检索是否有保存这些文件,如果有就会直接调用保存后的数据去初始化。

⚠️ **GitHub.com Fallback** ⚠️