Android路由的实现

最近在做一个项目,因为有多个功能模块,所以遇到了一个困难:当Moudle A依赖Moudle B,Moudle B依赖Moudle C,Moudle C依赖MoudleD,Moudle D为壳App,但是当我们需要在Moudle B调用Moudle C的时候,跳转不过去,因为找不到这个类,因此有了Android路由这个概念的提出,即我们可以在任意一个Moudel调用任意Moudel的Activity以及Services等组件。
了解了这个概念的时候,我还是有一点懵,因为完全没有一点思路。后来看了两篇相关的文章:

学习了大神的代码,我还是打算自己来写一下!
我的想法是新建一个Moudle,然后所有的项目都依赖于这个Moudle,然后在Mooudle中扫描所有的类,得到类名,最后加载这个类,得到Class< ?>对象,后面我们加载的时候就可以直接调方法得到类了! 代码如下:



import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import dalvik.system.DexFile;

/**
 * Created by 魏兴 on 2017/5/8.
 */

public class ClassContainer extends AppCompatActivity {
    private static final String TAG = "ClassContainer";
    private static List<String> classesName = new ArrayList<>();
    private static List<Class<?>> classes = new ArrayList<>();

    /**
     * 扫描类,并保存类名称及对象
     * @param context
     * @param packageNames
     */
    public static void scan(Context context,String[] packageNames) {
        try {
            String str = context.getPackageResourcePath();
            DexFile df = new DexFile(context.getPackageResourcePath());
            Enumeration<String> n=df.entries();
            while(n.hasMoreElements()){
                String className = n.nextElement();
                for (String packageName:packageNames) {
                    if (className.contains(packageName)) {//在当前所有可执行的类里面查找包含有该包名的所有类
//                        com.example.zhaoshuang.camera.MyVideoView$1
                        if(className.contains("$")){//内存中有多个类,原因未知,此处也排除了静态内部类
                            int length = className.indexOf("$");
                            className = className.substring(0,length);
                        }
                        classesName.add(className);
                        Class cls = Thread.currentThread().getContextClassLoader().loadClass(className);
                        classes.add(cls);
                        break;
                    }
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取类对象
     * @param packageName
     * @return
     */
    public static Class<?> getClass(String packageName){

        try {
            for (int i=0;i<classesName.size();i++){
                String name = classesName.get(i);
                if(name.contains(packageName)){
                    Log.d(TAG, "getClass: "+classes.get(i).getName());
                    return classes.get(i).newInstance().getClass();
//                    return Thread.currentThread().getContextClassLoader().loadClass("com.luckytry.hybrid.myapplication.controller.FormActivity");
                }
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }


}

这里遇到了几个坑,第一个就是类名于实际不一样,比如com.example.zhaoshuang.camera.MyVideoView我只有一个,但是实际加载的时候加载了5次,后来才知道因为我的MyVideoView类中有多个匿名内部类,但是我在保存类的时候,没有保存到期望的类。导致后面开启Activity的时候,出了问题。

com.example.zhaoshuang.camera.MyVideoView$1
com.example.zhaoshuang.camera.MyVideoView$2
com.example.zhaoshuang.camera.MyVideoView$3
com.example.zhaoshuang.camera.MyVideoView$4
com.example.zhaoshuang.camera.MyVideoView$5

后来就在上面的代码中,修正了匿名内部类,直接保存了普通类。该代码需要在Applicaition中执行,耗时约800毫秒(保存了200个类,未保存的类包括系统包及依赖库的包没有统计)。
然后当我们需要调用组件的时候,直接调用方法即可,代码如下:

//注册
@Override
    public void onCreate() {
        // TODO Auto-generated method stub
        super.onCreate();
...
        ClassContainer.scan(this,new String[]{"com.luckytry.luckylibrary.MyAplication",             "com.luckytry.hybrid.myapplication","com.example.zhaoshuang.camera",
                "com.luckytry.luckylibrary"});

    }

//调用
@Override
    public void onClick(View v) {
        int i = v.getId();
       ...
        }else if(i == R.id.rl_iv_sumbit){
            Log.d(TAG, "onClick: 提交照片");

                Intent intent = new Intent(this, ClassContainer.getClass("controller.FormActivity"));
                startActivity(intent);
            finish();
        }
    }

测试了一下,木有问题。后来在优化的过程中又发现新的问题:就是我们的类名是从dex文件中得到的。

/data/app/com.luckytry.hybrid.mainapplication-2.apk

隐患在我们的APK比较大的时候,方法超过了65536个的时候,dex需要分包(后来测试的过程中发现努比亚6.0的机子,以及三星7.0的机子拿到的dex文件都和上面的dex文件不一样,代码没有任何变化,但是dex文件名称有变化——“/data/app/com.luckytry.hybrid.mainapplication-2/base.apk”,而且这个dex文件没有class文件),也就是说目前这个方法不确定是否能有效的将全部Activity扫描出来。
这个时候,有两个办法解决:

  • 手动将所有的类名保存下来
  • 不再保存类名,当我们需要Class< ?>对象的时候,手动的给类名的全部字符,而不是部分,如下:
//以前
 Intent intent = new Intent(this, ClassContainer.getClass("controller.FormActivity"));
 startActivity(intent);
//现在
Intent intent = new Intent(this, ClassContainer.getClass("com.luckytry.hybrid.myapplication.controller.FormActivity"));
 startActivity(intent);

最后,我发现这个路由Moudle根本木有必要单独新建,直接将ClassContainer类写在Moudle A的工具类,甚至工具类都不用,一句代码就搞定了,越来越简单了!

Module2Module相比较而言,该项目有一个缺点就是硬编码,就是说我们将所有的class类名都写在了代码中,这一点不应该;优点是代码非常简单,维护及扩展的时候,自己比较清楚,不会遇到异常了自己抓瞎。

后续填坑:

上面提到硬编码,于是我将继续将代码做了一些优化,首先第一步是增加一个自定义注解:

/**
 * 路由器注解
 * Created by 魏兴 on 2017/5/8.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAnnptation {
    String name();

}

接下来我们在需要用到的Activity(就是需要任意Moudle均能调用到这个Activity)增加我们刚刚自定义的路由注解,例如:

/**
 * 拍照
 * Created by 魏兴 on 2017/4/17.
 */
@ClassAnnptation(name = "CaremaActivity")
public class CaremaActivity extends Activity {}

在完成了注解的添加以后,我们还需要注册注解,意思就是将所有的注解添加到一个容器内,待后续调用,这个方法与之前的方法参数完全一致,不一样的只是类名:

/**
     * class容器
     */
    private static Map<String,Class<?>> map = new HashMap<>();
/**
     * 扫描类,并添加到路由器
     * @param context
     * @param packageNames
     */
    public static void scan(Context context, String[] packageNames) {
        try {
            DexFile df = new DexFile(context.getPackageResourcePath());
            Enumeration<String> n=df.entries();
            while(n.hasMoreElements()){
                String className = n.nextElement();
                for (String packageName:packageNames) {
                    if (className.contains(packageName)) {//在当前所有可执行的类里面查找包含有该包名的所有类
//                        com.example.zhaoshuang.camera.MyVideoView$1
                        if(className.contains("$")){//内存中有多个类,原因未知,此处也排除了静态内部类
                            int length = className.indexOf("$");
                            className = className.substring(0,length);
                        }

                        Class cls = Thread.currentThread().getContextClassLoader().loadClass(className);
                        addRouter(cls);
                        break;
                    }
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

/**
     * 添加到路由器
     * @param cls
     */
    private static void addRouter(Class<?> cls){
        if(cls == null){
            return;
        }
        ClassAnnptation clsa =cls.getAnnotation(ClassAnnptation.class);
        if(clsa!=null){
            if(map.containsValue(cls))
                return;
            String name = clsa.name();
            if(!map.containsKey(name)){
                map.put(name,cls);
            }else{
                Class<?> ocls = map.get(name);
                throw  new IllegalArgumentException(cls.getName()+" 注解的key与"+ocls.getName()+"重复,请重新设置key");
            }

        }
/**
     * 根据key获取到路由器容器的Class<?>
     * @param key
     * @return
     */
    public static Class<?> getRouter(String key){
        if(map.containsKey(key)){
            return map.get(key);
        }
        return null;
    }
    }

接下来我们获取Class< ?>对象时就不需要直接传人完整的类名或者部分包名+类名了,而是直接输入我们设置的别名,注册与调用如下:

//注册注解越早越好,建议放在Application里面
new Thread(){
            @Override
            public void run() {
                super.run();
                Router.scan(getApplicationContext(),new String[]{"com.luckytry.luckylibrary.MyAplication",
                        "com.luckytry.hybrid.myapplication","com.example.zhaoshuang.camera",
                        "com.luckytry.luckylibrary"});
            }
        }.start();

//调用方法
@Override
public void onClick(View v) {
    v.getId();
    switch (v.getId()) {
        case R.id.action_a://绘制按钮
            Intent polygon = new Intent(this,Router.getRouter("PolygonActivity"));
            polygon.putExtra(ParameterUtil.belong,0);
            startActivity(polygon);

            break;
          }
}

如此貌似完美了,但是我的想法是将这部分设置为一个库,可以供其它项目使用,而且需要实现编译时注解,这样就不用手动注解了,但是后来发现一个问题:

Created with Raphaël 2.1.0 Moudle A Moudle B Moudle C Moudle D 打包APP

我的所有Moudle是队列的形式排列的A依赖B,B依赖C,C依赖D,D依赖APP,而一般情况下不会有这种情况。所以在这种情况我可以将所有的Class< ?>对象放在底部的Moudle A,这样所有的Moudle都可以调用到需要被注解的。举个栗子:本来正常情况下是Moudle A被Moudle B调用,而MoudleA不能调用Moudle B的东西,这个时候Moudle B也需要调用Moudle D里面的功能,也是不能实现的,需要在打包App里面建一个处理器,来处理这些调用关系。

看到这里应该明白我们的设计的路由器的问题了,就是我们的路由有一定的局限性,不能被所有的项目拿来使用。因此就算我通过编译时注解或者是运行时注解将Class< ?>对象保存下来了,却苦于找不到存储的地方。也许可以通过持久化存储来解决,嗯,以后有时间再尝试一下!到这里大家应该和我一起对这个Android路由有了非常深刻的理解了,今后不管是使用人家的库或者自己编写方法来实现,都不再是难题了!
参考文章:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值