在这个案例中,使用常规的startActivity启动一个未安装apk中的Activity,需要解决以下几个问题:
1、 ClassLoader如何找到需实例化Activity
2、 未在androidmanifest.xml中注册如何启动对应Activity
3、 Activity生命周期怎么实现
4、 资源如何加载
1、 ClassLoader如何找到需实例化Activity
ActivityThread中的performLaunchActivity中实例化Activity:
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
这里看到ClassLoader相信大家已经明白Activity实例怎么来的了,没错,就是通过反射。这里重点看下这个ClassLoader怎么来的,这样也许我们可以通过它干点事情。这个ClassLoader实际是在LoadedApk中创建的,如下:
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
libraryPermittedPath, mBaseClassLoader,
mApplicationInfo.classLoaderName);
一直跟踪代码,最终找到ClassLoader创建的地方:
public static ClassLoader createClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, String classloaderName) {
if (isPathClassLoaderName(classloaderName)) {
return new PathClassLoader(dexPath, librarySearchPath, parent);
} else if (isDelegateLastClassLoaderName(classloaderName)) {
return new DelegateLastClassLoader(dexPath, librarySearchPath, parent);
}
throw new AssertionError("Invalid classLoaderName: " + classloaderName);
}
classloaderName为null,所以创建的是PathClassLoader。
这里就要说到PathClassLoader和DexClassLoader及它们的父类BaseDexClassLoader了。PathClassLoader只能加载本地文件系统也就是自身应用的dex文件,如果需要加载其他的dex文件则需要使用DexClassLoader帮忙了,从两个类的构造函数即可以看出它们的差异。
在父类BaseDexClassLoader中有创建一个DexPathList对象pathlist,而自身应用的dex文件及其他的dex文件统一由pathlist的私有成员变量dexElements维护。
父类BaseDexClassLoader:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@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;
}
}
DexPathList的findClass:
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;
}
到这里,我们是否可以将createClassLoader中PathClassLoader做一些修改,让它可以findClass到其他外部的dex文件中的类,而不抛出ClassNotFoundException呢?答案是肯定的。我们只需要创建一个DexClassLoader加载dex文件,并将它的dexElements拼接到PathClassLoader的dexElements中即可。
如下为拼接两个ClassLoader的Element数组:
private static Object getField(Object srcObject, String className, String fieldName) {
Object object = null;
try {
Field field = getField(className, fieldName);
object = field.get(srcObject);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return object;
}
private static Field getField(String className, String fieldName) {
Field field = null;
try {
Class clazz = Class.forName(className);
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return field;
}
private static Object getPathList(Object srcObject) {
return getField(srcObject, "dalvik.system.BaseDexClassLoader", FIELD_PATH_LIST);
}
private static Object getDexElements(Object srcObject) {
Class clazz = srcObject.getClass();
Object object = null;
try {
Field field = clazz.getDeclaredField(FIELD_DEX_ELEMENTS);
field.setAccessible(true);
object = field.get(srcObject);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return object;
}
private static void mergeDexElements(ClassLoader src, ClassLoader des) {
Object srcPathList = getPathList(src);
Object desPathList = getPathList(des);
Object srcElements = getDexElements(srcPathList);
Object desElements = getDexElements(desPathList);
int srcLen = Array.getLength(srcElements);
int desLen = Array.getLength(desElements);
Log.i(TAG, "desLen = " + desLen);
Class compoment = srcElements.getClass().getComponentType();
Object value = Array.newInstance(compoment, srcLen + desLen);
for (int i = 0; i < srcLen + desLen; i++) {
if (i < srcLen) {
Array.set(value, i, Array.get(srcElements, i));
} else {
Array.set(value, i, Array.get(desElements,i - srcLen));
}
}
try {
Field field = srcPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS);
field.setAccessible(true);
field.set(srcPathList, value);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
这里需要注意的一点是需读取外部存储的权限,尤其是android N以上版本需动态申请权限才可以进行以上操作,如果未进行权限申请,ClassNotFoundException会一直困扰你。这个案例在启动界面中申请权限,成功后跳转到工作界面,确保权限申请成功后可以正常完成拼接逻辑。