鸿蒙版组件化路由框架HRouter

footballboy
发布于 2021-4-2 09:40
浏览
0收藏

目的


不管在什么平台上,大型项目组件化的趋势是不可逆的,而协助项目组件化的路由框架又是首当其冲。比如在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

 

 

 

 

 

作者:暗影萨满

已于2021-4-2 09:40:12修改
收藏
回复
举报
回复
    相关推荐