Recently, Small has been used to implement the plug-in of the original project, and the effect is not bad. As long as the project is a componentized structure, it is easy to reconstruct. When you use ARouter, however, you cannot find the ARouter automatically generated by the Route annotation because the queried apK path is only base.apk when initialized. Group? XXX file. In order to adapt to the plug-in version, you need to manually build a simplified version of the ARouter framework.

APT

The Activity class marked with annotations is processed through APT to generate the corresponding mapping file. Here we create two Modules of type Java Library. A library (ARouter handles the logic), a Compiler (annotations, source code generation)

Gradle introduces dependencies

The library of the build. Gradle

apply plugin: 'java'
dependencies {
    compile fileTree(dir: 'libs'.include: ['*.jar'])
    compileOnly 'com. Google. Android: android: 4.1.1.4'
}
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
Copy the code

CompilerOnly is the Android related library

The build of the compiler. Gradle

apply plugin: 'java'
dependencies {
    compile 'com. Squareup: javapoet: 1.9.0'
    compile 'com. Google. Auto. Services: auto - service: 1.0 -rc3'
    compile project(':library')}targetCompatibility = '1.7'
sourceCompatibility = '1.7'
Copy the code

Auto-service automatically generates a Processor configuration file in the META-INF folder so that the processing class corresponding to the annotation can be found during compilation. Javapoet is an open source library from Square that gracefully generates Java source files.

Create an annotation @route

Next, we create an annotation CLASS in the Library. Target indicates the TYPE of the modification (CLASS or interface, method, attribute, TYPE indicates CLASS or interface), Retention indicates the level of visibility (compile-time, run-time, etc., CLASS indicates visible at compile time)

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String path(a);
}
Copy the code

We then introduce dependencies in app Gradle

dependencies {
    annotationProcessor project(':compiler')
    compile project(':library')}Copy the code

Note: Gradle2.2 requires the annotationProcessor to be apt and introduced in the project root directory

classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'
Copy the code

Add annotations to MainActivity

.import io.github.iamyours.aarouter.annotation.Route;

@Route(path = "/app/main")
public class MainActivity extends AppCompatActivity {... }Copy the code

Create the annotation processing class RouteProcessor

package io.github.iamyours.compiler;

import com.google.auto.service.AutoService;

import java.util.LinkedHashSet;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

import io.github.iamyours.aarouter.annotation.Route;

/** * Created by yanxx on 2017/7/28. */
@AutoService(Processor.class)
public class RouteProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("= = = = = = = = = = = = ="+roundEnvironment);
        return true;
    }


    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Route.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        returnSourceVersion.latestSupported(); }}Copy the code

Then we make project below, get the following log information, it indicates that apt configuration is successful.

:app:javaPreCompileDebug
:compiler:compileJava UP-TO-DATE
:compiler:processResources NO-SOURCE
:compiler:classes UP-TO-DATE
:compiler:jar UP-TO-DATE
:app:compileDebugJavaWithJavac
=============[errorRaised=false, rootElements=[io.github.iamyours.aarouter.MainActivity, ...]  =============[errorRaised=false, rootElements=[], processingOver=true]
:app:compileDebugNdk NO-SOURCE
:app:compileDebugSources
Copy the code

Generate source files using Javapoet

The use of Javapoet can be found here github.com/square/java… To hold the class name marked by the Route annotation, a mapping class is saved as a method call. The generated classes are as follows

public final class AARouterMap_app implements IRoute {
  @Override
  public void loadInto(Map<String, String> routes) {
    routes.put("/app/main"."io.github.iamyours.aarouter.MainActivity"); }}Copy the code

In order to find mapped classes from DexFile in Android APK later, we need to put these mapped Java classes in the same package as follows: Add IRoute interface to library

public interface IRoute {
    void loadInto(Map<String, String> routes);
}
Copy the code

In the compiler

@AutoService(Processor.class)
public class RouteProcessor extends AbstractProcessor {
    private Filer filer;
    private Map<String, String> routes = new HashMap<>();
    private String moduleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element e : roundEnvironment.getElementsAnnotatedWith(Route.class)) {
            addRoute(e);
        }
        createRouteFile();
        return true;
    }

    private void createRouteFile(a) {
        TypeSpec.Builder builder = TypeSpec.classBuilder("AARouterMap_" + moduleName).addModifiers(Modifier.PUBLIC);
        TypeName superInterface = ClassName.bestGuess("io.github.iamyours.aarouter.IRoute");
        builder.addSuperinterface(superInterface);
        TypeName stringType = ClassName.get(String.class);
        TypeName mapType = ParameterizedTypeName.get(ClassName.get(Map.class), stringType, stringType);
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("loadInto")
                .addAnnotation(Override.class)
                .returns(void.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(mapType, "routes");
        for (String key : routes.keySet()) {
            methodBuilder.addStatement("routes.put($S,$S)", key, routes.get(key));
        }
        builder.addMethod(methodBuilder.build());
        JavaFile javaFile = JavaFile.builder(ARouter.ROUTES_PACKAGE_NAME, builder.build()).build();// Output the source code to arouter.routes_package_name,
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
// e.printStackTrace();}}/* moduleName (); /* moduleName (); /* moduleName (); Ali's access method is to build in each module file by annotationProcessorOptions incoming, this simplified directly from the path (such as "/ app/login" app, "/ news/newsinfo" news) * /
    private void addRoute(Element e) {
        Route route = e.getAnnotation(Route.class);
        String path = route.path();
        String name = e.toString();
        moduleName = path.substring(1,path.lastIndexOf("/"));
        routes.put(path, name);
    }


    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Route.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        returnSourceVersion.latestSupported(); }}Copy the code

ARouter initialization

To get all the routes with the @route annotation mark, find the AARouterMap_xxx class file in arouter.routes_package_name from DexFile and load the Route with the loadInto call via reflection initialization.

public class ARouter {
    private Map<String, String> routes = new HashMap<>();
    private static final ARouter instance = new ARouter();
    public static final String ROUTES_PACKAGE_NAME = "io.github.iamyours.aarouter.routes";
    public void init(Context context){
        try {// Find the mapping class file in the ROUTES_PACKAGE_NAME directory
            Set<String> names = ClassUtils.getFileNameByPackageName(context,ROUTES_PACKAGE_NAME);
            initRoutes(names);
        } catch(Exception e) { e.printStackTrace(); }}// Initialize the route through reflection
    private void initRoutes(Set<String> names) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        for(String name:names){
            Class clazz = Class.forName(name);
            Object obj = clazz.newInstance();
            if(obj instanceofIRoute){ IRoute route = (IRoute) obj; route.loadInto(routes); }}}private ARouter(a) {}public static ARouter getInstance(a) {
        return instance;
    }

    public Postcard build(String path) {
        String component = routes.get(path);
        if (component == null) throw new RuntimeException("could not find route with " + path);
        return newPostcard(component); }}Copy the code

Get the route mapping class file

We put the mapped classes under the ROUTES_PACKAGE_NAME using the RouterProcessor, and we just need to walk through the dex file to find them. While The ARouter of Alibaba takes the dex file sought by the current app application directory base.apk, and then loads the DexFile by DexClassLoader. But if the project is made up of plug-ins, the dexFile is not just base.apk, so you need to get it in another way. Through breakpoint debugging, it is found that the pathList in the Context’s classloader contains all apK paths. We can get the dexFile simply by reflecting the context’s classloader, and we don’t need to reload the dexFile. LoadDex in the field ourselves.

public class ClassUtils {
    // Obtain all dexfiles of app through BaseDexClassLoader reflection
    private static List<DexFile> getDexFiles(Context context) throws IOException {
        List<DexFile> dexFiles = new ArrayList<>();
        BaseDexClassLoader loader = (BaseDexClassLoader) context.getClassLoader();
        try {
            Field pathListField = field("dalvik.system.BaseDexClassLoader"."pathList");
            Object list = pathListField.get(loader);
            Field dexElementsField = field("dalvik.system.DexPathList"."dexElements");
            Object[] dexElements = (Object[]) dexElementsField.get(list);
            Field dexFilefield = field("dalvik.system.DexPathList$Element"."dexFile");
            for(Object dex:dexElements){ DexFile dexFile = (DexFile) dexFilefield.get(dex); dexFiles.add(dexFile); }}catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return dexFiles;
    }

    private static Field field(String clazz,String fieldName) throws ClassNotFoundException, NoSuchFieldException {
        Class cls = Class.forName(clazz);
        Field field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field;
    }
    /** * Scan all classnames ** contained under the package by specifying the package name@param context     U know
     * @paramPackageName package name *@returnThe set of all classes */
    public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws IOException {
        final Set<String> classNames = new HashSet<>();

        List<DexFile> dexFiles = getDexFiles(context);
        for (final DexFile dexfile : dexFiles) {
            Enumeration<String> dexEntries = dexfile.entries();
            while (dexEntries.hasMoreElements()) {
                String className = dexEntries.nextElement();
                if(className.startsWith(packageName)) { classNames.add(className); }}}returnclassNames; }}Copy the code

With the above implementation, we can get the mapped route files by passing in the context’s classloader during initialization, and then reflect them and call loadInto to get all the routes. The next route jump is as simple as wrapping it as ComponentName

public class ARouter {...public Postcard build(String path) {
        String component = routes.get(path);
        if (component == null) throw new RuntimeException("could not find route with " + path);
        return newPostcard(component); }}Copy the code
public class Postcard {
    private String activityName;
    private Bundle mBundle;

    public Postcard(String activityName) {
        this.activityName = activityName;
        mBundle = new Bundle();
    }

    public Postcard withString(String key, String value) {
        mBundle.putString(key, value);
        return this;
    }

    public Postcard withInt(String key, int value) {
        mBundle.putInt(key, value);
        return this;
    }

    public Postcard with(Bundle bundle) {
        if (null! = bundle) { mBundle = bundle; }return this;
    }

    public void navigation(Activity context, int requestCode) {
        Intent intent = new Intent();
        intent.setComponent(newComponentName(context.getPackageName(), activityName)); intent.putExtras(mBundle); context.startActivityForResult(intent, requestCode); }}Copy the code

The project address

Github.com/iamyours/AA…

added

The current version also works with Small, but finding mapped classes by reflecting private apis is still a bit of a catch. Then I came up with another solution: each module build passes in the package name of the module, and the generated file is named AARouterMap. During initialization, small can get the package name of each plug-in through small.getBundleversions ().keys

ARouter.getInstance().init(Small.getBundleVersions().keys)
Copy the code

To get the package name for each plug-in and then ARouter initializes it using the package name list

public void init(Set<String> appIds) {
        try {
            initRoutes(appIds);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(InstantiationException e) { e.printStackTrace(); }}private void initRoutes(Set<String> appIds) throws IllegalAccessException, InstantiationException {
        for (String appId : appIds) {
            Class clazz = null;
            try {
                clazz = Class.forName(appId + ".AARouterMap");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            if(clazz==null)continue;
            Object obj = clazz.newInstance();
            if (obj instanceofIRoute) { IRoute route = (IRoute) obj; route.loadInto(routes); }}}Copy the code

You don’t have to traverse the dex to get the map, and the performance and security are better. Non-plug-in projects can also be adapted by manually passing the package name list.