android动态加载


刚进入一家公司,需要解决以下问题:公司主要是做仪器的,目前有4款app,以后还会增加几款,这样,同一个用户手机上可能需要装多款我们的app,这样不仅安装麻烦,而且不同的功能要打开不同的app,给用户造成不便,用户体验会很差。

需求1:把目前已有的几款app整合成一个大的app,在这个大的app上给每个小app中的功能留有入口,这样用户安装一个大app后就可以从对应入口使用各个小app中的功能,用户不需要安装多个app,提升了用户体验

需求2:每个小app的功能可以独立修改升级,不能修改其中一个app中的功能,其他小app也要跟着升级发版,做到各个小app中的功能能够互相独立。

考虑了以下,准备使用android的动态加载技术,把每个功能模块作为一个插件,有一个宿主app,和多个插件,安装宿主app后,用户需要的时候才去下载插件。

插件化需要做的几件事

加载插件dex

将dex包注入ClassLoader(原理分析)

这里谈到注入,就要谈到Android的ClassLoader体系。
在这里插入图片描述
由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,通过查阅开发文档,我们发现他们有如下使用场景:

  1. 关于PathClassLoader,文档中写到: Android uses this class for its system class loader and for its application class loader(s),

由此可知,Android应用就是用它来加载;

  1. DexClass可以加载apk,jar,及dex文件,但PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。
  2. 两个叶子节点的类都继承BaseDexClassLoader中,而具体的类加载逻辑也在此类中:

BaseDexClassLoader:

@Override  
protected Class<?> findClass(String name) throws ClassNotFoundException {  

    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();  
    Class c = pathList.findClass(name, suppressedExceptions);  
    if (c == null) {  
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);  
        for (Throwable t : suppressedExceptions) {  
            cnfe.addSuppressed(t);  
       }  
        throw cnfe;  
    }  
     return c;  
}  

由上述函数可知,当我们需要加载一个class时,实际是从pathList中去寻找的,查阅源码,发现pathList是DexPathList类的一个实例。ok,接着去分析DexPathList类中的findClass函数
DexPathList:

public Class findClass(String name, List<Throwable> suppressed) {  
    for (Element element : dexElements) {  
        DexFile dex = element.dexFile;  
        if (dex != null) {  
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);  
            if (clazz != null) {  
                return clazz;  
            }  
        }  
   }  
    if (dexElementsSuppressedExceptions != null) {  
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));  
    }  
    return null;  

}  

上述函数的大致逻辑为:遍历一个装载dex文件(每个dex文件实际上是一个DexFile对象)的数组(Element数组,Element是一个内部类),然后依次去加载所需要的class文件,直到找到为止。

看到这里,注入的解决方案也就浮出水面,假如我们将第二个dex文件放入Element数组中,那么在加载第二个dex包中的类时,应该可以直接找到。

将dex包注入ClassLoader(代码实现)

在我们自定义的BaseApplication的onCreate中,我们执行注入操作:

public String inject(String libPath) {
        boolean hasBaseDexClassLoader = true;
        try {
            Class.forName("dalvik.system.BaseDexClassLoader");
        } catch (ClassNotFoundException e) {
            hasBaseDexClassLoader = false;
        }
        if (hasBaseDexClassLoader) {
            PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();
            PluginManager.getInstance().setContext(this);
            try {
                Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList( PluginManager.getInstance().loadApk(libPath))));
                Object pathList = getPathList(pathClassLoader);
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
                return "SUCCESS";
            } catch (Throwable e) {
                e.printStackTrace();
                return android.util.Log.getStackTraceString(e);
            }
        }
        return "SUCCESS";
    }
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
private static Object getField(Object obj, Class cls, String str) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }
 private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }
 private static Object combineArray(Object obj, Object obj2) {
        Class componentType = obj2.getClass().getComponentType();
        int length = Array.getLength(obj2);
        int length2 = Array.getLength(obj) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                Array.set(newInstance, i, Array.get(obj2, i));
            } else {
                Array.set(newInstance, i, Array.get(obj, i - length));
            }
        }
        return newInstance;
    }
private static void setField(Object obj, Class cls, String str, Object obj2) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        declaredField.set(obj, obj2);
    }

Activity等组件问题

插件中的组件,由于没有在manifest中进行注册,加载进来以后就是一个普通的类,不具有生命周期,所以不可以使用intent进行跳转,为了解决这个问题,可以使用代理的方式解决。但是,我这里没有,由于app都是自己开发的,所以把每个插件里面的组件,都在宿主的manifest中进行注册。

以下是360开源插件化框架RePlugin介绍

加载插件中的资源

加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。

public Resources getResources() {
        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assets, PluginManager.getInstance().getCacheDir(BaseActivity.this)+ File.separator + "otherapk-debug.apk");
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return new Resources(assets, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值