打造一個簡易版ARouter框架

最近用Small實現原有項目的插件化,效果還不錯,只要工程是組件化的結構就很好重構。但在使用ARouter時,因爲初始化時,查詢的apk路徑只有base.apk,因此不能找到由Route註解自動生成的ARouter$$Group$$xxx文件。爲了適配插件化版本,因此須要本身手動打造簡易版的ARouter框架。java

APT

經過APT處理用註解標記的Activity類,生成對應的映射文件。這裏建立兩個類型爲java library的module。一個library(ARouter處理邏輯),一個compiler(處理註解,生成源碼)android

gradle引入相關依賴

library的build.gradlegit

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'
複製代碼

compilerOnly裏的是Android的相關類庫github

compiler的build.gradleapi

apply plugin: 'java'
dependencies {
    compile 'com.squareup:javapoet:1.9.0'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    compile project(':library')
}
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
複製代碼

auto-service會自動在META-INF文件夾下生成Processor配置信息文件,使得編譯時能找到annotation對應的處理類。javapoet則是由square公司出的開源庫,能優雅的生成java源文件。安全

建立註解@Route

接着,咱們在library中建立一個註解類,Target代表修飾的類型(類或接口、方法、屬性,TYPE表示類或接口),Retention代表可見級別(編譯時,運行時期等,CLASS表示在編譯時可見)bash

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String path();
}
複製代碼

而後在app的gradle引入依賴app

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

複製代碼

注意:gradle2.2如下須要將annotationProcessor改成apt,同時在工程根目錄引入框架

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
複製代碼

在MainActivity中添加註解ide

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

@Route(path = "/app/main")
public class MainActivity extends AppCompatActivity {
...
}
複製代碼

建立註解處理類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() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Route.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}
複製代碼

而後咱們make project如下,獲得以下日誌信息,則代表apt配置成功。

: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
複製代碼

使用javapoet生成源文件

javapoet的用法能夠看這裏github.com/square/java… 爲了保存由Route註解標記的class類名,這裏用一個映射類經過方法調用的形式保存。具體生成的類以下

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

爲了以後可以從Android apk中的DexFile中找到映射類,咱們要把這些映射java類放到同一個package下,具體實現邏輯以下: 在library中添加IRoute接口

public interface IRoute {
    void loadInto(Map<String, String> routes);
}
複製代碼

在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() {
        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();//將源碼輸出到ARouter.ROUTES_PACKAGE_NAME,
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
// e.printStackTrace();
        }
    }
    /* 這裏有一個注意的點事moduleName,因爲每一個library或application模塊的環境不一樣, 也只能取到當前模塊下的註解,所以須要生成不一樣的映射文件保存到每一個模塊下, 阿里的獲取的方法是在每一個模塊的build文件經過annotationProcessorOptions傳入, 這邊簡化直接從path獲取(如「/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() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Route.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

複製代碼

ARouter初始化

爲了獲得全部有@Route註解標記的路由,須要從DexFile中找到ARouter.ROUTES_PACKAGE_NAME目錄下的AARouterMap_xxx的class文件,經過反射初始化調用loadInto加載路由。

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 {//找到ROUTES_PACKAGE_NAME目錄下的映射class文件
            Set<String> names = ClassUtils.getFileNameByPackageName(context,ROUTES_PACKAGE_NAME);
            initRoutes(names);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //經過反射初始化路由
    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 instanceof IRoute){
                IRoute route = (IRoute) obj;
                route.loadInto(routes);
            }
        }
    }

    private ARouter() {

    }

    public static ARouter getInstance() {
        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 new Postcard(component);
    }
}
複製代碼

獲取路由映射class文件

以前咱們經過RouterProcessor將映射class放到了ROUTES_PACKAGE_NAME下,咱們只須要在dex文件中遍歷尋找到它們便可。而alibaba的ARouter取的是當前app應用目錄的base.apk尋找的dex文件,而後經過DexClassLoader加載取得DexFile。但若是項目插件化構成的,dexFile就不僅是base.apk下了,所以須要經過其餘方式獲取了。 經過斷點調試,發現context的classloader中的pathList便含有了全部apk的路徑。咱們只需經過反射context的classloader就能夠獲取dexFile,並且也不須要本身經過現場DexFile.loadDex從新加載了。

public class ClassUtils {
    //經過BaseDexClassLoader反射獲取app全部的DexFile
    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;
    }
    /** * 經過指定包名,掃描包下面包含的全部的ClassName * * @param context U know * @param packageName 包名 * @return 全部class的集合 */
    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);
                }
            }
        }
        return classNames;
    }
}
複製代碼

有了上面的實現,咱們就能夠在初始化時,經過傳入context的classloader,獲取到映射路由文件,而後反射初始化他們,調用loadInto,便可獲得全部的路由。而接下來的路由跳轉就很簡單了,只需包裝成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 new Postcard(component);
    }
}

複製代碼
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(new ComponentName(context.getPackageName(), activityName));
        intent.putExtras(mBundle);
        context.startActivityForResult(intent, requestCode);
    }
}
複製代碼

項目地址

github.com/iamyours/AA…

補充說明

如今這個版本雖然也適配Small,可是經過反射私有api找到映射class終究仍是有些隱患。後來想到另一種方案:每一個模塊build傳入模塊的包名,生成的文件統一命名爲AARouterMap,初始化時small能夠經過Small.getBundleVersions().keys獲取每一個插件的包名

ARouter.getInstance().init(Small.getBundleVersions().keys)
複製代碼

來獲取每一個插件的包名 而後ARouter使用包名列表初始化

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 instanceof IRoute) {
                IRoute route = (IRoute) obj;
                route.loadInto(routes);
            }
        }
    }
複製代碼

這樣就不用遍歷dex獲取映射,性能和安全性也會好一點。非插件化的項目也能夠經過手動傳包名列表適配了。

相關文章
相關標籤/搜索