深入理解 Android 之路由框架源码解析

感觉 Android 中的这个路由机制还是比较有意思的,研究了一段时间。写篇文章记录一下。个人认为能够带来的一些好处如下:

  • 炫酷。注解的特性,路由机制听着就很不错。好吧,其实相对于传统的跳转机制是简洁。
  • 高度解耦。好吧,其实 Intent 机制本身就是能够实现解耦功能。
  • 组件化开发。提前配置具有关系的页面映射,具备协同工作机制。
  • 备灾功能。如果原先预备跳转的界面不存在,可以跳转至一个临时的备灾界面。或者说拦截功能。

主要的核心知识点如下:

  • Android 中的 Intent 机制
  • Java 反射机制
  • Android 的编译时注解
  • APT 技术以及 Javapoet 技术

在进行源码分析之前,先总结一下其大概的原理,就是用一个 Map 来存储具体的路由、实体 Activity 之间的映射。然后在利用 Android 自身的 Intent 机制,来进行界面的跳转。

简单使用过程

根据 readme 来说明一个简单的使用过程:

全局注册

注意注入的这个字段一定要是全局唯一的,避免与其他模块冲突:

Router.init("test");//设置Scheme

定义路由

给 SecondActivity 定义了路由 @RouterActivity(“second”)

@RouterActivity("second")
public class SecondActivity extends Activity {
    @RouterField("uid")
    private int uid;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        Router.inject(this);
        Log.e("uid", String.valueOf(uid));
    }
}

界面跳转

存在以下两种方式跳转到指定界面:

// 方式一
RouterHelper.getSecondActivityHelper().withUid(24).start(this);
// 方式二
Router.startActivity(context, "test://second?uid=233");

实现机制

定义 Annotation

修饰 class

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterActivity {
    String[] value();
}

修饰具体 field

@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterField {
    String[] value();
}

注解处理

文章开始的时候知道本项目是利用 APT 技术生成辅助代码的,为了能更好的全局去分析一下,先看一下 APT 下面生成的代码是什么。

好吧,其实看着一页截图大概就明白怎么回事了,程序整个流程应该是初始化某个类,其中会涉及到 init() 这个方法,进而通过上述这个方法进行路由和 Activity 的映射存储。具体的实例,就看一下注解处理器如何生成截图中的这个 AptRouterInitializer 这个类吧,看下面这张截图:

核心代码如下所示,做了部分处理:

        // 生成 AptRouterInitializer 这个类
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(RouterActivity.class);
        ClassName activityRouteTableInitializer = ClassName.get("com.thejoyrun.router", "RouterInitializer");
        TypeSpec.Builder typeSpec = TypeSpec.classBuilder((targetModuleName.length() == 0 ? "Apt" : targetModuleName) + "RouterInitializer")
                .addSuperinterface(activityRouteTableInitializer)
                .addModifiers(Modifier.PUBLIC)
                .addStaticBlock(CodeBlock.of(String.format("Router.register(new %sRouterInitializer());", (targetModuleName.length() == 0 ? "Apt" : targetModuleName))));

        // Method build,生成 AptRouterInitializer 类中的 init() 方法
        TypeElement activityRouteTableInitializertypeElement = elementUtils.getTypeElement(activityRouteTableInitializer.toString());
        List<? extends Element> members = elementUtils.getAllMembers(activityRouteTableInitializertypeElement);
        MethodSpec.Builder bindViewMethodSpecBuilder = null;
        for (Element element : members) {
            if ("init".equals(element.getSimpleName().toString())) {
                bindViewMethodSpecBuilder = MethodSpec.overriding((ExecutableElement) element);
                break;
            }
        }
        if (bindViewMethodSpecBuilder == null) {
            return false;
        }
        //  往 init 方法中写入 args.put(xx,yy) 代码
        ClassName activityHelperClassName = ClassName.get("com.thejoyrun.router", "ActivityHelper");
        List<MethodSpec> methodSpecs = new ArrayList<>();
        for (Element element : elements) {
            RouterActivity routerActivity = element.getAnnotation(RouterActivity.class);
            TypeElement typeElement = (TypeElement) element;
            for (String key : routerActivity.value()) {
                bindViewMethodSpecBuilder.addStatement("arg0.put($S, $T.class)", key, typeElement.asType());
            }
            ClassName className = buildActivityHelper(routerActivity.value()[0], activityHelperClassName, (TypeElement) element);

            MethodSpec methodSpec = MethodSpec.methodBuilder("get" + className.simpleName())
                    .addStatement("return new $T()", className)
                    .returns(className)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .build();
            methodSpecs.add(methodSpec);
        }
        TypeSpec typeSpecRouterHelper = TypeSpec.classBuilder(targetModuleName + "RouterHelper")
                .addModifiers(Modifier.PUBLIC)
                .addMethods(methodSpecs)
                .build();
        // Build Java File
    }

主要就像上面介绍的这样了,借助于 JavaPoet 这个代码生成的类库,来根据获取的核心元素生成指定格式的代码。

逆向分析

看一下 SecondActivity 的结构,具体代码如下所示:

@RouterActivity({"second", "other2://www.thejoyrun.com/second", "test://www.thejoyrun.com/second"})
public class SecondActivity extends BaseActivity {
    @RouterField("uid")
    private int uid;
    @RouterField("age")
    private int age;
    @RouterField("name")
    private String name;
    @RouterField("man")
    private Boolean man = true;
    @RouterField("manger")
    private Boolean manger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        long time = System.currentTimeMillis();
        // 注意这里
        Router.inject(this);
        StringBuilder builder = new StringBuilder();
        builder.append("uid:" + String.valueOf(uid)).append('\n');
        builder.append("age:" + String.valueOf(age)).append('\n');
        builder.append("name:" + String.valueOf(name)).append('\n');
        builder.append("man:" + String.valueOf(man)).append('\n');
        builder.append("manger:" + String.valueOf(manger)).append('\n');
        builder.append("formActivity:" + String.valueOf(formActivity)).append('\n');
        TextView textView = (TextView) findViewById(R.id.text);
        textView.setText(builder.toString());
    }
}

在第一行 @RouterActivity 主要作用是将三个路由机制映射到 SecondActivity 上。

在 MyApplication 中:

Router.init("test");

在 Router 中找到 init 方法

public static void init(String scheme) {
        Router.sScheme = scheme;
        try {
      Class.forName("com.thejoyrun.router.AptRouterInitializer");
        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
        }
    }

通过 forName 找到 AptRouterInitializer 类,其实这个类就是通过 APT 生成的。

public class AptRouterInitializer implements RouterInitializer {
  static {
    // 初始化一个 map
    Router.register(new AptRouterInitializer());}
  @Override
  public void init(Map<String, Class<? extends Activity>> arg0) {
    arg0.put("second", SecondActivity.class);
    arg0.put("other2://www.thejoyrun.com/second", SecondActivity.class);
    arg0.put("test://www.thejoyrun.com/second", SecondActivity.class);
    arg0.put("third", ThirdActivity.class);
  }
}

如上所示代码,用一个 static 代码块来初始话一个 Map ,而后进行路由和 Activity 之间的映射存储。接下来看看 Router.inject(this);

 public static void inject(Activity activity) {
        SafeBundle bundle = new SafeBundle(activity.getIntent().getExtras(), activity.getIntent().getData());
        Class clazz = activity.getClass();
        List<Field> fields = getDeclaredFields(clazz);
        System.out.println(fields.size());
        for (Field field : fields) {
            RouterField annotation = field.getAnnotation(RouterField.class);
            if (annotation == null) {
                continue;
            }
            String type = field.getGenericType().toString();
            field.setAccessible(true);
            String[] names = annotation.value();
            try {
                for (String name : names) {
                    if (!bundle.containsKey(name)) {
                        continue;
                    }
                    if (type.equals("double")) {
                        field.set(activity, bundle.getDouble(name, field.getDouble(activity)));
                        continue;
                    } else if (type.equals("float")) {
                        field.set(activity, bundle.getFloat(name, field.getFloat(activity)));
                        continue;
                    } ...
                    Object defaultValue = field.get(activity);
                    if (field.getGenericType() == String.class) {
                        field.set(activity, bundle.getString(name, (String) defaultValue));
                    } ...
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

从上面可以看见,其作用是利用反射机制给字段赋予数值。接下来看一下如何去处理跳转过程的:

 Router.startActivity(this, "test://www.thejoyrun.com/second?uid=233&age=24");

进入 Router 中的对应方法看一下:

public static boolean startActivity(Context context, String url) {
        // 是否有拦截机制
  		if (sFilter != null) {
            url = sFilter.doFilter(url);
            if (sFilter.start(context, url)) {
                return true;
            }
        }
        if (TextUtils.isEmpty(url)) {
            return false;
        }
        Uri uri = Uri.parse(url);
  		// 解析 uri 获取到指定的 Activity
        Class clazz = getActivityClass(url, uri);
        if (clazz != null) {
            Intent intent = new Intent(context, clazz);
            intent.setData(uri);
            if (!(context instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
          	// 跳转
            context.startActivity(intent);
            return true;
        } else {
            new Throwable(url + "can not startActivity").printStackTrace();
        }
        return false;
    }

本质上就是一个 Map 的映射过程,根据路由寻找到 Activity,然后进行跳转机制。但是这段代码中开始一段挺有意思的,一个路由的拦截过程/替换过程。在 Application 全局/手动配置过程中,有以下代码:

// 可选,针对自己的业务做调整
Router.setFilter(new Filter() {
    public String doFilter(String url) {
    	//return url.replace("test://www.thejoyrun.com/","test://");
        return url;
    }
});

进行一个 url 的返回(即不做任何操作)或者一个 url 的替换,这样就达到路由替换或者拦截的目的了。验证以下 Filter 中确实提供了这个功能。

如果觉得本文对你有帮助,请打赏以回报我的劳动