最近在做一个项目,因为有多个功能模块,所以遇到了一个困难:当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;
}
}
如此貌似完美了,但是我的想法是将这部分设置为一个库,可以供其它项目使用,而且需要实现编译时注解,这样就不用手动注解了,但是后来发现一个问题:
我的所有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路由有了非常深刻的理解了,今后不管是使用人家的库或者自己编写方法来实现,都不再是难题了!
参考文章: