在Android上加载jar包中的Activity以及Service

前言:

截止到目前版本,本项目仅支持已经在manifest中提前注册好Service!

Activity已可以无清单启动了,但没有处理资源文件

项目地址:https://gitee.com/yutousama/ProxyActivity

先附上几个用得上的Android 源码地址吧

BaseDexClassLoader

DexPathList

1.原理&逻辑

首先我们都知道,要在Android中加载jar包,可以通过DexClassLoader这个类进行加载

DexClassLoader loader=new DexClassLoader(jar.getAbsolutePath(),"/data/data/"+getPackageName()+"/plugs/",null,getClassLoader());

但其实apk在启动的时候也有个ClassLoader,它负责加载app的类,此时就会产生两个ClassLoader了,一个系统类加载器,一个jar包类加载器。

如果此时我们通过反射直接去启动jar包的Activity的话,就会出现ClassNotFoundException异常

Class activity=getClassLoader().loadClass("com.yutou.plug.ActivityTest");
           Intent intent=new Intent(MainActivity.this,activity);
           intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
           startActivity(intent);

异常

 java.lang.ClassNotFoundException: Didn't find class "com.yutou.plug.ActivityTest" on path: DexPathList[[zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/base.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_dependencies_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_resources_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_0_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_1_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_2_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_3_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_4_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_5_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_6_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_7_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_8_apk.apk", zip file "/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/split_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/data/app/com.luckyboy.mmxing-Gzo5nbOesipD8FMDNSKzcA==/lib/arm64, /system/lib64, /system/vendor/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:93)
07-25 16:41:20.479 16250-16250/com.luckyboy.mmxing W/System.err:     at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at com.yutou.droid.MainActivity.startJarActivity(MainActivity.java:30)
        at com.yutou.droid.MainActivity$1.onClick(MainActivity.java:20)
        at android.view.View.performClick(View.java:6266)
        at android.view.View$PerformClick.run(View.java:24730)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)

这就是因为startActivity是查找的系统类加载器

所以我们的将两个类加载器合二为一,这样startActivity就能成功启动了

2.如何做?

首先我们查看DexClassLoader.java,发现它是继承BaseDexClassLoader的,根据源码我们可以看到有个pathList变量,估摸着是负责加载dex的

private final DexPathList pathList;

然后我们可以看到一个名为dexElements的变量,它就是我们要处理的东西了

*注意,Element并不是android.sax.Element,而是DexPathList的内部类

/**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

2.1获取dexElements

/**
     *获取类加载器的DexElements数组
     * @param loader 类加载器
     * @return dexElements
     */
    private Object[] getDexElements(ClassLoader loader) {
        try {
            Field[] fields= BaseDexClassLoader.class.getDeclaredFields(); //获取BaseDexClassLoader的pathList变量
            Field pathListField = null;
            for (Field field : fields) {
                System.out.println(field.getName());
                if(field.getName().equals("pathList"))
                    pathListField=field;
            }
            if(pathListField==null)
                return  null;
            pathListField.setAccessible(true);
            Object pathList=pathListField.get(loader);//获取指定类加载器的pathList
            Field dexElementsField=pathList.getClass().getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[]  dexElements= (Object[]) dexElementsField .get(pathList); //获取dexElements
            return dexElements;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

 

Object[] t1=getDexElements(getClassLoader()); //获取系统类加载器的dexElements数组
 Object[] t2=getDexElements(loader);  //获取jar包类加载器的dexElements数组

此时我们就获取了t1系统类加载器和t2 jar包类加载器的dexElements数组了

然后我们再合并

 /**
     * 合并两个Elements数组并覆盖掉系统加载类的dexElements
     * @param t1 合成的Elements
     * @param t2 被合并的Elements
     */
private void saveLibPath(Object[] t1,Object[] t2) throws Exception { 
        Field[] fields= BaseDexClassLoader.class.getDeclaredFields();
        Field pathListField = null;
        for (Field field : fields) {
            System.out.println(field.getName());
            if(field.getName().equals("pathList"))
            {
                pathListField=field;
            }
        }
        if(pathListField==null)
            return;
        pathListField.setAccessible(true);
        Object pathList=pathListField.get(getClassLoader());
        Field dexElements=pathList.getClass().getDeclaredField("dexElements"); //获取到系统类加载器的dexElements
        dexElements.setAccessible(true);
        //合并两个获取到系统类加载器的dexElements
        Object[] obj= Arrays.copyOf(t1,t1.length+t2.length); //复制一份t1+t2长度的数组
        for (int i=t1.length,j=0;i<t1.length+t2.length;i++,j++){
            Array.set(obj,i,t2[j]); //因为是copy的t1,所以要把t2部分加上

        }
        //覆盖操作
        dexElements.set(pathList,obj);
    }

现在工作已经完成了一大半,如果此时你运行项目的话,就会提示有jar包有多个类加载器,从而不知道加载哪一个。所以我们要把jar包的类加载器给清空

/**
     * 移除类加载器的dexElements,不然会提示jar包有多个类加载器
     * @param loader 被移除dexElements的加载器
     */
    private void removeLib(ClassLoader loader){
        try{
            Field[] fields= BaseDexClassLoader.class.getDeclaredFields();
            Field pathListField = null;
            for (Field field : fields) {
                if(field.getName().equals("pathList"))
                    pathListField=field;
            }
            if(pathListField==null)
                return;
            pathListField.setAccessible(true);
            Object pathList=pathListField.get(loader);
            Field dexElements=pathList.getClass().getDeclaredField("dexElements");
            dexElements.setAccessible(true);
            dexElements.set(pathList,null);//设为null后就不要再使用这个加载器了,否则会报空指针
        }catch (Exception e){
            e.printStackTrace();
        }
    }

此时就ok了

3.能加载资源文件和不进行manifest注册吗?

目前Demo已经可以不注册manifest启动Activity了,具体参考项目的UselessCode里面的代码,大概其原理是hook掉startActivity过程,通过实际注册过的代理Activity欺骗系统认为Activity已经注册,然后再最后阶段把目标Activity替换回来就行了,具体可以参考 这个帖子

关于能不能加载资源文件,因为这个是肯定要处理R文件的,暂时没有接触过

发表评论

电子邮件地址不会被公开。