purpose

The trend toward componentization of large projects is irreversible, regardless of platform, and the routing frameworks that facilitate it are the first to do so. For example, ARouter is a well-known routing framework on Android. ARouter has rich and powerful functions, and the routing function is only part of his work. We try to imitate ARouter’s routing implementation to create a large mask version of HRouter.

Design ideas

Arouter maintains a Map with a Key as the route name and a Value as the corresponding Class object. When the user specifies the route name, Arouter will immediately find the corresponding Class object and implement the jump. So how do YOU set the data in the Map? Should users be asked to put everything they use? Such a design is a little low, it is estimated to be teased by users. Let the framework set it up, but how can you do that as a reference? ARouter uses both implementations internally, a bytecode peg and APT. Bytecode staking is the process of inserting the framework’s code into the original code, which is cool but unfamiliar. APT familiar, compiler annotation processing technology, widely used in various frameworks, is able to automatically generate some regular Java files through annotations, so the generated code path information can be controlled by the framework, and then through class loading technology to scan fixed package name, and then reflection generated instances and call. Ok! Logic worked!

Code implementation

The statement notes

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HRouter {
    String value();
}
Copy the code

Simple, declared a compiler annotation

Annotation processor

@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); }} // Generate file 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 (); // Clear 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(); } /** * Private class JavaFileDetail {private String mPackageName; // Generated package name 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); } public String getPackageName() {return this.mpackagename; } /** * Get the file to be generated ** @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; }}}Copy the code

It generates the following code:

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); }}Copy the code

Since the framework convention is to set the route name as “/ module name/page name “, all modules will generate only one route file, which will register all configured annotation classes, as shown above.

Api design

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; } /** * Call ** @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 load AbilitySlice ** @param routerName * @return */ public Class getClassByRouterName(String routerName) { return routerMap.get(routerName); } /** * abilitySlice start 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; } /** * in the Ability launch 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; }}Copy the code

RouterMap is the data structure used to store the corresponding relations, under the other method is relatively simple call HongMeng page jump method provided by the system, focus on the init method under the ClassUtils. This tool ScanClassInfoWithPackageName method.

@param packageName @Param abilityContext * @return */ public static List<Class<? >> ScanClassInfoWithPackageName(String packageName, AbilityContext AbilityContext) throws Exception {// Dalvik.system. DexFile cannot be referenced directly, but has been loaded into memory, so use reflection Class<? > dexFileClass = Class.forName("dalvik.system.DexFile"); / / get the dalvik. System. DexFile Constructor Constructor dexFileConstructor = dexFileClass. GetConstructor (String. Class); Method entriesMethod = dexfileclass. getMethod("entries"); / / hap file for physical path String bundleCodePath = abilityContext. GetBundleCodePath (); System.out.println("bundleCodePath=" + bundleCodePath); Set dexFiles = new HashSet(); // 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; / / filter to complete, and the Android, a dexFile corresponds to a hap (apk) Object dexFileObj = dexFileConstructor. NewInstance (absolutePath); dexFiles.add(dexFileObj); } System.out.println("dexFiles.size()=" + dexFiles.size()); List< class <? >> classList = new ArrayList<>(); / / get the class loader or PathClassLoader (essence) this this = abilityContext. GetClassloader (); for (Object dexFile : dexFiles) { if (dexFile == null) continue; 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; }Copy the code

This method scans the classes under the target package, loads them into the virtual machine through the class loader, and returns them. In fact, I am not confident in writing this code. When I originally wrote it based on my experience on Android, I found that some classes could not be referenced, such as PathClassLoader, DexFile and other classes related to class loading on Android, blood collapse…… But I print the Class type of getClassLoader and find it is PathClassLoader. Then I understand that the Class system has been copied and loaded into the virtual machine, but I can’t reference it directly. The code logic basically refers to the experience on Android, except for the hap suffix file output.

use

Now that we have introduced the core classes of the framework, we have created four modules according to the componentized pattern:

Layer 1: EntryMain (entry) Layer 2: EntryBusiness01 (business module 1), EntryBusiness02 (business module 2) Layer 3: EntryCommon (public module, which defines all route names)Copy the code

The four modules are referenced from top to bottom, with no dependencies between peers.

@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()); }); }}Copy the code

Ability jump between modules, success!

@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());
    }
}
Copy the code

Use the AbilitySlice instance of EntryMain in EntryBusiness02, success!

The end of the

All tests passed. HRouter already has the basic componentialized routing capability, of course, only the page jump, will continue to improve, the cross-module data supply capability, business processing capabilities are integrated into the ARouter, close to, hats off!

A link to the

Github:https://github.com/loubinfeng2013/HarmonyTools
Email:[email protected]
Copy the code