Mobile Architecture 06-Teach you handwritten Arouter routing framework

Mobile Architecture 06 - Handwritten Arouter Framework

ToolRouter is modeled after Arouter, the purpose is to learn the Arouter framework.

The ToolRouter framework mainly includes three parts: core, annotation and compiler.

  • core: core api, used to implement the jump function of the routing module;
  • annotation: custom annotation used to declare the page that needs to be routed;
  • compiler: Process annotations and generate Java classes that register routing tables based on custom annotations at compile time;

1. The annotation module

There are mainly two annotations Route and Extra, with a RouteMeta class;

The Route annotation is used to declare the routing path, and the path is at least two levels:

//元注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    //路由的路径,标识一个路由节点
    String path();
    //将路由节点进行分组,可以实现按组动态加载
    String group() default "";
}

Extra annotations are used to declare extra information to implement the transfer of data or ISERVICE objects (custom interfaces) between modules:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface Extra {
    String name() default "";
}

RouteMeta is used to store routing information, which is obtained by parsing Route annotations. There are two types of routing pages: 1. Activity, which represents the Activity that needs to be routed; 2. ISERVICE, which represents the ISERVICE object that can be passed, implements the interface of ISERVICE, and can be passed through routing.

public class RouteMeta {
    //路由页面的类型:Activity或者IService
    public enum Type {
        ACTIVITY,
        ISERVICE
    }

    private Type type;
    //节点 (Activity)
    private Element element;
    //注解使用的类对象
    private Class<?> destination;
    //路由地址
    private String path;
    //路由分组
    private String group;
    ...
}

**Note: The Chinese annotations of the **annotation module and the compiler module use GBK format, otherwise an error will be reported when compiling

2. The compiler module

The compiler module uses Javax's Processor to process annotations, google's AutoService to automatically compile Processor classes, and javapoet to generate Java classes. Javax is a Java library, which is not supported by Android, so the compiler module is a Java library module.

Students who have never used javapoet can refer to https://github.com/square/javapoet .

1. Add configuration

Before using it, you need to import the framework in the build.gradle of the compiler module:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.7.0'
    //导入自定义注解
    implementation project(':ToolRouterAnnotation')
}

//Android对Java1.8的兼容性不好,所以指定1.7版本
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

2. Process Route annotations

Declare a RouteProcessor that inherits Processor to process Route annotations. First add an annotation to the RouteProcessor and set the processing parameters:

//自动编译Processor类
@AutoService(Processor.class)
//处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数
@SupportedOptions(Consts.ARGUMENTS_NAME)
//指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//注册给哪些注解的  替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数
@SupportedAnnotationTypes({Consts.ANN_TYPE_ROUTE})
public class RouteProcessor extends AbstractProcessor {

Then, initialize the RouteProcessor and get the required processing tools from the ProcessingEnvironment:

/**
 * 初始化 从 {@link ProcessingEnvironment} 中获得一系列处理器工具
 *
 * @param processingEnvironment
 */
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    //获得apt的日志输出
    log = Log.newLog(processingEnvironment.getMessager());
    //获取节点
    elementUtils = processingEnvironment.getElementUtils();
    //获取类型
    typeUtils = processingEnvironment.getTypeUtils();
    //获取文件生成器
    filerUtils = processingEnvironment.getFiler();
    //参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件
    Map<String, String> options = processingEnvironment.getOptions();
    if (!Utils.isEmpty(options)) {
        moduleName = options.get(Consts.ARGUMENTS_NAME);
    }
    log.i("RouteProcessor Parmaters:" + moduleName);
    if (Utils.isEmpty(moduleName)) {
        throw new RuntimeException("Not set Processor Parmaters.");
    }
}

Then, traverse the nodes annotated by Route in the process method (annotation processing method), first determine the type of the node, then use RouteMeta to save the node information, then verify whether the routing address of the node complies with the rules, and then use categories() according to the group name To save node information to groupMap;

Then, traverse the node information in the groupMap, use the javapoet tool, and generate a Java class that inherits the IRouteGroup interface according to the group name, called the grouping information class, which is used to save the routing information of each group, such as:

public class ToolRouter$$Group$$demo1 implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/demo1/activityToolRouterDemo1", RouteMeta.build(RouteMeta.Type.ACTIVITY,ActivityToolRouterDemo1.class, "/demo1/activitytoolrouterdemo1", "demo1"));
    atlas.put("/demo1/activityToolRouterDemo2", RouteMeta.build(RouteMeta.Type.ACTIVITY,ActivityToolRouterDemo2.class, "/demo1/activitytoolrouterdemo2", "demo1"));
  }
}

Note: Different modules cannot use the same group name. Because the same group name will generate the same group information class, only one is reserved when it is packaged into an APK.

Then, traverse all the grouping information classes, use the javapoet tool, and generate a Java class that inherits the IRouteRoot interface according to the module name, called the table information class, which is used to save all the grouping information classes, such as:

public class ToolRouter$$Root$$DemoComponent1 implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("demo1", ToolRouter$$Group$$demo1.class);
    routes.put("main", ToolRouter$$Group$$main.class);
  }
}

The generated classes for processing Route annotations are placed in the gsw.toolrouter.routes directory, that is, the package name is gsw.toolrouter.routes.

At this point, the Route annotation is processed.

3. Handling Extra annotations

Declare an ExtraProcessor that inherits Processor to process Extra annotations. The basic process is consistent with RouteProcessor, it should be noted that

The Route annotation only supports two types, Activity and IService. The Extra annotation supports more types, including basic types, arrays, String, Object, etc., so more type judgments are required:

if (type == TypeKind.BOOLEAN.ordinal()) {
    statement += "getBooleanExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.BYTE.ordinal()) {
    statement += "getByteExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.SHORT.ordinal()) {
    statement += "getShortExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.INT.ordinal()) {
    statement += "getIntExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.LONG.ordinal()) {
    statement += "getLongExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.CHAR.ordinal()) {
    statement += "getCharExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.FLOAT.ordinal()) {
    statement += "getFloatExtra($S, " + defaultValue + ")";
} else if (type == TypeKind.DOUBLE.ordinal()) {
    statement += "getDoubleExtra($S, " + defaultValue + ")";
} else {
    //数组类型
    if (type == TypeKind.ARRAY.ordinal()) {
        addArrayStatement(statement, fieldName, extraName, typeMirror, element);
    } else {
        //Object
        addObjectStatement(statement, fieldName, extraName, typeMirror, element);
    }
    return;
}

Then according to the class name + $$Extra, use javapoet to generate an Extra information class that inherits IExtra, which is used to copy the member variables with the Extra annotation, for example:

public class ActivityToolRouterDemo2$$Extra implements IExtra {
    @Override
    public void loadExtra(Object target) {
        ActivityToolRouterDemo2 t = (ActivityToolRouterDemo2) target;
        t.key1 = t.getIntent().getLongExtra("key1", t.key1);
        t.key2 = t.getIntent().getStringExtra("key2");
    }
}

If the member variable annotated with Extra is of type TestService (a custom interface used to pass objects), and the routing path is set in the Extra annotation, the following class will be generated:

public class ActivityToolRouterDemo1$$Extra implements IExtra {
    @Override
    public void loadExtra(Object target) {
        ActivityToolRouterDemo1 t = (ActivityToolRouterDemo1) target;
        t.testService1 = (TestService) ToolRouter.getInstance().build("/main/service1").navigation();
        t.testService2 = (TestService) ToolRouter.getInstance().build("/main2/service2").navigation();
    }
}

The generated classes for processing Extra annotations are placed in the gsw.[module name].toolrouter directory, that is, the package name is gsw.[module name].toolrouter.

3. core module

The core module is used to implement jump logic.

1. Initialization

Initialize in Application and load the table information class.

First define a routing table Warehouse to store all routing information.

public class Warehouse {
    // root 映射表 保存分组信信息类
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    // group 映射表 保存所有的路由页面
    static Map<String, RouteMeta> routes = new HashMap<>();
    // group 映射表 保存所有的IService对象
    static Map<Class, IService> services = new HashMap<>();
}

Then, traverse all dex files to find all grouping information classes and table information classes generated by javapoet. Lookup is a time-consuming operation. Use ThreadPoolExecutor to maintain a thread pool to look up all packages named gsw.toolrouter.routes, and use the synchronization counter to determine whether the lookup is complete.

public static Set<String> getFileNameByPackageName(...) {
    final Set<String> classNames = new HashSet<>();
    //获得程序所有的apk(instant run会产生很多split apk)
    List<String> paths = getSourcePaths(context);
    //使用同步计数器判断均处理完成
    final CountDownLatch parserCtl = new CountDownLatch(paths.size());
    ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths
            .size());
    for (final String path : paths) {
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                DexFile dexfile = null;
                try {
                    //加载 apk中的dex 并遍历 获得所有包名为 {packageName} 的类
                    dexfile = new DexFile(path);
                    Enumeration<String> dexEntries = dexfile.entries();
                    while (dexEntries.hasMoreElements()) {
                        String className = dexEntries.nextElement();
                        if (className.startsWith(packageName)) {
                            classNames.add(className);
                        }
                    }
                } 
                ...
                    //释放1个
                    parserCtl.countDown();
                }
            }
        });
    }
    //等待执行完成
    parserCtl.await();
    return classNames;
}

Then call the table information class (eg: ToolRouter

R O O t
The loadInfo method of DemoComponent1) stores all the grouping information classes in the groupsIndex of the Warehouse.

private static void loadInfo()  {
    //获得所有 apt生成的路由类的全类名 (路由表)
    Set<String> routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    for (String className : routerMap) {
        if (className.startsWith(ROUTE_ROOT_PAKCAGE + "." + SDK_NAME + SEPARATOR +
                SUFFIX_ROOT)) {
            // root中注册的是分组信息 将分组信息加入仓库中
            ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto
                    (Warehouse.groupsIndex);
        }
    }
}

2. Create a jump card

First, generate a Postcard that inherits RouteMeta according to the routing address, and save the routing address, group name, and data to be transmitted in the Postcard.

Call Postcard's withXXX method to add the data to be passed and save it in a Bundle object of Postcard, for example:

public Postcard withParcelableArray(@Nullable String key, @Nullable Parcelable[] value) {
    mBundle.putParcelableArray(key, value);
    return this;
}

You can also set the transition animation of the Activity:

public Postcard withTransition(int enterAnim, int exitAnim) {
    this.enterAnim = enterAnim;
    this.exitAnim = exitAnim;
    return this;
}

Then call Postcard's navigation() to jump to the page.

3. Flip the page

When calling the navigation() of Postcard for page jump, if it is an activity page, it will be transferred directly; if it is an IService page, it will be put back in the IService object.

First, find the group information class from the groupsIndex of Warehouse according to the group name, create this class, call its loadInfo(), and add all the routing pages of this group to Warehouse.routes according to the routing path.

//创建并调用 loadInto 函数,然后记录在仓库
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card
        .getGroup());
if (null == groupMeta) {
    throw new NoRouteFoundException("没找到对应路由: " + card.getGroup() + " " +
            card.getPath());
}
IRouteGroup iGroupInstance;
try {
    iGroupInstance = groupMeta.getConstructor().newInstance();
} catch (Exception e) {
    throw new RuntimeException("路由分组映射表记录失败.", e);
}
iGroupInstance.loadInto(Warehouse.routes);
//已经准备过了就可以移除了 (不会一直存在内存中)
Warehouse.groupsIndex.remove(card.getGroup());

Then look up the class object of the routing page according to the routing path and save it in Postcard. If the routing page is an IService implementation class, also save it in Postcard and Warehouse services.

//路由页面的class对象类
card.setDestination(routeMeta.getDestination());
//设置路由页面的类型(activity 或IService实现类)
card.setType(routeMeta.getType());
...
Class<?> destination = routeMeta.getDestination();
IService service = Warehouse.services.get(destination);
if (null == service) {
    try {
        service = (IService) destination.getConstructor().newInstance();
        Warehouse.services.put(destination, service);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
card.setService(service);

Then, make a jump. If the routing page is an activity, generate an Intent based on the class object of the routing page, and add the data that needs to be passed

, and then call startActivity() to realize the jump. If the routing page is an IService, return postcard.getService().

switch (postcard.getType()) {
case ACTIVITY:
    final Context currentContext = null == context ? mContext : context;
    final Intent intent = new Intent(currentContext, postcard.getDestination());
    intent.putExtras(postcard.getExtras());
    int flags = postcard.getFlags();
    if (-1 != flags) {
        intent.setFlags(flags);
    } else if (!(currentContext instanceof Activity)) {
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            //可能需要返回码
            if (requestCode > 0) {
                ActivityCompat.startActivityForResult((Activity) currentContext, intent,
                        requestCode, postcard.getOptionsBundle());
            } else {
                ActivityCompat.startActivity(currentContext, intent, postcard
                        .getOptionsBundle());
            }

            if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) &&
                    currentContext instanceof Activity) {
                //老版本
                ((Activity) currentContext).overridePendingTransition(postcard
                                .getEnterAnim()
                        , postcard.getExitAnim());
            }
            //跳转完成
            if (null != callback) {
                callback.onArrival(postcard);
            }
        }
    });
    break;
case ISERVICE:
    return postcard.getService();
default:
    break;
}

4. Receive the passed data

Data can be passed by calling Postcard's withXXX method. Receiving the passed data needs to be implemented through the Extra annotation.

First, on the page that needs to receive routing data, pass the key and data type in the withXXX method, declare a member property, and add Extra annotations, such as:

@Extra
long key1;

Then, call ToolRouter.getInstance().inject(this) to load the Extra information. According to the class name of the Activity + the class name $$Extraof the Extra information class, for example: ActivityToolRouterDemo1$$Extra, then create an object of the Extra information class, call its loadExtra() method, and assign a value to the key1 of the Activity.

public void loadExtra(Object target) {
  ActivityToolRouterDemo2 t = (ActivityToolRouterDemo2)target;
  t.key1 = t.getIntent().getLongExtra("key1", t.key1);
  t.key2 = t.getIntent().getStringExtra("key2");
}

If you add annotations to members of the TestService type, you need to set the routing path.

//给IService接口设置Extra注解,用来向其他路由页面传递IService对象
@Extra(name = "/main/service1")
TestService testService1;

At this point, the core module is basically completed. Let's talk about the use of ToolRouter.

Fourth, the use of ToolRouter

The use of ToolRouter is basically the same as that of Arouter.

1. Add dependencies and configuration

Add the following configuration to the build.gradle of the application module and library module:

android {
    defaultConfig {
    ...
    //设置moduleName,不添加会报错
    javaCompileOptions {
        annotationProcessorOptions {
        arguments = [ moduleName : project.getName() ]
        }
    }
    }
}

dependencies {
    //ToolRouter路由框架
    annotationProcessor project(':ToolRouterCompiler')
    implementation project(':ToolRouter')
    ...
}

When used alone, you need to use jars: ToolRouter , ToolRouterCompiler

2. Initialize SDK

Initialize ARouter in Application of application module

ToolRouter.init(this);

Load the Extra information class in the Acitivity that needs to be routed to process the Extra annotation and receive the passed data:

ToolRouter.getInstance().inject(this);

3. Add annotations

Add the Route annotation to the Activity that needs routing in the application module and the library module. The annotation must be set to two levels, because the initialization of the routing module is carried out in groups.

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/demo1/activityArouterDemo1")
public class ActivityArouterDemo1 extends AppCompatActivity {
...
}

On the page that needs to receive routing data, declare a member property according to the key and data type passed in the withXXX method, and add the Extra annotation.

//传递普通数据
@Extra
long key1;
@Extra
String key2;

4. Initiate a routing operation

// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/test/activity").navigation();

// 2. 跳转并携带参数
ARouter.getInstance().build("/test/1")
            .withLong("key1", 666L)
            .withString("key2", "888")
            .withObject("key3", new Test("Jack", "Rose"))
            .navigation();

5. Add obfuscation rules

-keep public gsw.toolrouter.routes.**{*;}
-keep class * implements gsw.toolrouter.core.template.**{*;}

6. Jump to custom objects

First create a class to implement TestService and add the Route annotation

@Route(path = "/main/service1")
public class TestServiceImpl1 implements TestService {
    @Override
    public void test(Context context) {
        Toast.makeText(context, "我是demo1模块测试服务通信1", Toast.LENGTH_SHORT).show();
        Log.i("Service", "我是demo1模块测试服务通信1");
    }
}

Then, get this object through navigation()

TestService t=(TestService) ToolRouter.getInstance().build("/main/service1").navigation()

In addition, if you declare a member variable of type TestService and add the Extra annotation (the routing address needs to be set), you do not need to obtain it through navigation(), and the framework automatically assigns a value to this member variable:

//给IService接口设置Extra注解,用来向其他路由页面传递IService对象
@Extra(name = "/main/service1")
TestService testService1;

testService1.test();

The code has been uploaded to gitee, and students who like it can download it. By the way, give a like!

[Previous: Mobile Architecture 05-Componentization and Arouter Routing Framework

Next:

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325525314&siteId=291194637