前言:
截止到目前版本,本项目仅支持已经在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文件的,暂时没有接触过