鸿蒙版组件化路由框架HRouter
目的
不管在什么平台上,大型项目组件化的趋势是不可逆的,而协助项目组件化的路由框架又是首当其冲。比如在android上比较有名的路由框架就有阿里的ARouter,ARouter的功能很丰富很强大,路由功能只是他一部分的工作,我们试着模仿ARouter的路由实现,来一个鸿蒙版的HRouter。
设计思路
熟悉Arouter的人都知道,ARouter内部维护了一个Map,Key是路由名,Value是对应的Class对象,当使用者指定路由名称的时候,那么ARouter就马上找到对应的Class对象,实现跳转等功能。那么Map中的数据怎么设置进去呢?难道让使用者在每个使用到的地方put一下吗?这样的设计有点low,估计要被使用者吐槽。那就让框架来自己设置,但是作为被引用者,怎么能做到这点呢?ARouter内部使用到了两者技术实现,一种字节码插桩,一个是APT。字节码插桩就是在原先的代码上插入框架需要的代码,很牛逼但不太熟悉。APT熟悉,编译器注解处理技术,普遍应用在各大框架中,就是能通过注解自动生成一些有规则的java文件的技术,那么生成代码路径信息都可以由框架控制,然后通过类加载技术扫描固定的包名,然后反射生成实例并调用。ok!逻辑通了!
代码实现
声明注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HRouter {
String value();
}
简单,声明了编译器注解一枚
注解处理器
@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {
private Map<String, JavaFileDetail> javaFileDetailMap = new HashMap<>();
private Elements mElementUtils;
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(HRouter.class);
for (Element element : elements) {
if (element.getKind() == ElementKind.CLASS) {
TypeElement typeElement = (TypeElement) element;
String routerName = typeElement.getAnnotation(HRouter.class).value();
String className = routerName.split("/")[1];
JavaFileDetail javaFileDetail = javaFileDetailMap.get(className);
if (javaFileDetail == null) {
javaFileDetail = new JavaFileDetail(className);
javaFileDetailMap.put(className, javaFileDetail);
}
javaFileDetail.addRouterName(routerName, typeElement);
}
}
//生成文件
for (Map.Entry<String, JavaFileDetail> entry : javaFileDetailMap.entrySet()) {
JavaFile javaFile = JavaFile.builder(entry.getValue().getPackageName(), entry.getValue().generateFile()).build();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
//清除,否则会有无谓的报错
javaFileDetailMap.clear();
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new HashSet<>();
annotations.add(HRouter.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 自动生成的java文件描述
*/
private class JavaFileDetail {
private String mPackageName;//生成的包名
private String mClassName;//生成的类名
private Map<String, TypeElement> routerNameMap = new HashMap<>();
public JavaFileDetail(String className) {
this.mPackageName = "com.lbf.hrouter";
this.mClassName = className;
}
public void addRouterName(String name, TypeElement element) {
routerNameMap.put(name, element);
}
/**
* 获取包名
*
* @return
*/
public String getPackageName() {
return this.mPackageName;
}
/**
* 获取待生成的文件信息
*
* @return
*/
public TypeSpec generateFile() {
MethodSpec.Builder addRouterBuilder = MethodSpec.methodBuilder("addRouter")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(Map.class, "routerMap");
for (Map.Entry<String, TypeElement> entry : routerNameMap.entrySet()) {
addRouterBuilder.addStatement("routerMap.put($S," + entry.getValue().asType() + ".class)", entry.getKey());
}
TypeSpec typeSpec = TypeSpec.classBuilder(mClassName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.get("com.lbf.lib.router", "IRouter"))
.addMethod(addRouterBuilder.build())
.build();
return typeSpec;
}
}
}
通过它可以生成如下代码:
public class entrymain implements IRouter {
public void addRouter(Map routerMap) {
routerMap.put("/entrymain/mainability",com.lbf.harmonytools.MainAbility.class);
routerMap.put("/entrymain/aptabilityslice",com.lbf.harmonytools.slice.AptAbilitySlice.class);
routerMap.put("/entrymain/mainabilityslice",com.lbf.harmonytools.slice.MainAbilitySlice.class);
}
}
因为框架约定路由名需要按照"/模块名/页面名"来设置,所有同一个模块只会生成一个路由文件,里面会注册所有配置注解的类,如上。
Api设计
public class HRouter {
private Map<String, Class> routerMap = new HashMap<>();
private HRouter() {
}
private static class Holder {
private static HRouter Instance = new HRouter();
}
public static HRouter NewInstance() {
return Holder.Instance;
}
/**
* 需要在主工程的MyApplication中调用
*
* @param abilityContext
* @return
*/
public boolean init(AbilityContext abilityContext) {
try {
List<Class<?>> classList = ClassUtils.ScanClassInfoWithPackageName("com.lbf.hrouter", abilityContext);
for (Class clz : classList) {
Constructor constructor = clz.getConstructor();
IRouter router = (IRouter) constructor.newInstance();
router.addRouter(routerMap);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* Ability装载AbilitySlice
*
* @param routerName
* @return
*/
public Class getClassByRouterName(String routerName) {
return routerMap.get(routerName);
}
/**
* abilitySlice启动abilitySlice
*
* @param abilitySlice
* @param routerName
* @param intent
* @return
*/
public boolean abilitySliceNavigation(AbilitySlice abilitySlice, String routerName, Intent intent) {
try {
abilitySlice.present((AbilitySlice) routerMap.get(routerName).getConstructor().newInstance(), intent);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 在Ability启动Ability
*
* @param ability
* @param routerName
* @param intent
* @return
*/
public boolean abilityNavigation(Ability ability, String routerName, Intent intent) {
try {
ability.startAbility(intent.setElementName(ability.getBundleName(), routerMap.get(routerName)));
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
routerMap就是用来存放对应关系的数据结构,其他方法比较简单就是调用来下鸿蒙系统提供的页面跳转方法,着重来看下init方法中的ClassUtils.ScanClassInfoWithPackageName这个工具方法。
/**
* 扫描目标包下面的类
*
* @param packageName
* @param abilityContext
* @return
*/
public static List<Class<?>> ScanClassInfoWithPackageName(String packageName, AbilityContext abilityContext) throws Exception {
//dalvik.system.DexFile无法直接引用,但是已经被加载到了内存中,所以用采用反射
Class<?> dexFileClass = Class.forName("dalvik.system.DexFile");
//获得dalvik.system.DexFile的构造方法
Constructor dexFileConstructor = dexFileClass.getConstructor(String.class);
//获得dalvik.system.DexFile的entries方法
Method entriesMethod = dexFileClass.getMethod("entries");
//获取hap文件的物理路径
String bundleCodePath = abilityContext.getBundleCodePath();
System.out.println("bundleCodePath=" + bundleCodePath);
//准备存放dexFile的集合,考虑到可能会有多个hap文件
Set dexFiles = new HashSet();
File dir = new File(bundleCodePath).getParentFile();
System.out.println("dir=" + dir.getAbsolutePath());
File[] files = dir.listFiles();
for (File file : files) {
String absolutePath = file.getAbsolutePath();
System.out.println(absolutePath);
if (!absolutePath.contains(".")) continue;
String suffix = absolutePath.substring(absolutePath.lastIndexOf("."));
if (!suffix.endsWith(".hap")) continue;
//过滤完成,和Android类似,一个dexFile对应一个hap(apk)
Object dexFileObj = dexFileConstructor.newInstance(absolutePath);
dexFiles.add(dexFileObj);
}
System.out.println("dexFiles.size()=" + dexFiles.size());
//用来存放扫描到的class对象集合
List<Class<?>> classList = new ArrayList<>();
//获取类加载器(本质上还是PathClassLoader)
ClassLoader classLoader = abilityContext.getClassloader();
for (Object dexFile : dexFiles) {
if (dexFile == null) continue;
//获取当前dexFile下面所有的类信息
Enumeration<String> entries = (Enumeration<String>) entriesMethod.invoke(dexFile);
//遍历过滤目标包名下的class
while (entries.hasMoreElements()) {
String currentClassPath = entries.nextElement();
System.out.println(currentClassPath);
if (currentClassPath == null || currentClassPath.isEmpty() || currentClassPath.indexOf(packageName) != 0)
continue;
Class<?> entryClass = Class.forName(currentClassPath, true, classLoader);
if (entryClass != null) classList.add(entryClass);
}
}
return classList;
}
这个方法扫描了目标包下面的类,并通过类加载器加载到虚拟机,并返回。其实这段代码写的我相当不自信,我本来根据android上的经验写的时候发现一些类引用不了,比如PathClassLoader,DexFile这些在android上跟类加载相关的类,血崩......但是我打印了getClassLoader的Class类型发现居然是PathClassLoader,那就明白了,这些类系统已经复制加载进了虚拟机,只是我不能直接引用,那就反射!代码逻辑基本上参考android上的经验,只不过鸿蒙输出的hap后缀的文件。
使用
框架核心的类已经介绍完了,我们用用看,我们按照组件化的模式,创建了4个module:
第一层:entrymain(入口)
第二层:entrybusiness01(业务模块1),entrybusiness02(业务模块2)
第三层:entrycommon(公共模块,里面定义了所有路由名称)
4个module的引用关系至上而下,同级之间没有依赖关系。
@HRouter(Constant.RouterName.EntryBusiness01MainAbilitySlice)
public class MainAbilitySlice extends AbilitySlice {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
setUIContent(ResourceTable.Layout_ability_main3);
findComponentById(ResourceTable.Id_text_helloworld3).setClickedListener(component -> {
com.lbf.lib.router.HRouter.NewInstance().abilityNavigation(getAbility(), Constant.RouterName.EntryBusiness02MainAbility, new Intent());
});
}
}
夸模块之间的Ability跳转,成功!
@HRouter(Constant.RouterName.EntryBusiness02MainAbility)
public class MainAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(com.lbf.lib.router.HRouter.NewInstance().getClassByRouterName(Constant.RouterName.EntryMainAptAbilitySlice).getName());
}
}
在entrybusiness02中使用entrymain的AbilitySlice实例,成功!
结束
所有测试全部通过。HRouter已经具备了基础的组件化路由能力了,当然仅限于页面跳转,后面还会不断完善,把跨模块的数据提供能力,业务处理能力都集成进去,向ARouter靠拢,致敬!
Github:https://github.com/loubinfeng2013/HarmonyTools
Email:516898224@qq.com
相关链接
Github:https://github.com/loubinfeng2013/HarmonyTools
作者:暗影萨满