前言:
截止到目前版本,本项目仅支持已经在manifest中提前注册好Service!
Activity已可以无清单启动了,但没有处理资源文件
项目地址:https://gitee.com/yutousama/ProxyActivity
先附上几个用得上的Android 源码地址吧
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文件的,暂时没有接触过