框架的初始化 - 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进程启动会检索是否有保存这些文件,如果有就会直接调用保存后的数据去初始化。