java.lang.RuntimeException: Unable to start activity ComponentInfo{com.com.sohu.test.app/com.sohu.test.app.MainActivity}:
java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException:
Unable to get package info for com.sohu.test.app; is package not installed?
错误提示说是无法实例化 Application,而Application的创建也是在performLaunchActivity中进行的,这里有些蹊跷,我们仔细查看一下发现:
通过ActivityThread的performLaunchActivity方法可以得知,Application通过LoadedApk的makeApplication方法创建,我们查看这个方法,在源码中发现了上文异常抛出的位置:
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals(“android”)) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " + appClass
- ": " + e.toString(), e);
}
}
经过排查发现:
private void initializeJavaContextClassLoader() {
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId());
} catch (RemoteException e) {
throw new IllegalStateException("Unable to get package info for "
- mPackageName + “; is system dying?”, e);
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for " - mPackageName + “; is package not installed?”);
}
boolean sharedUserIdSet = (pi.sharedUserId != null);
boolean processNameNotDefault =
(pi.applicationInfo != null &&
!mPackageName.equals(pi.applicationInfo.processName));
boolean sharable = (sharedUserIdSet || processNameNotDefault);
ClassLoader contextClassLoader =
(sharable)
? new WarningContextClassLoader()
: mClassLoader;
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
这里,我们找出了这个异常的来源:原来这里调用了getPackageInfo方法获取包的信息;而我们的插件并没有安装在系统上,因此系统肯定认为插件没有安装,这个方法肯定返回null。所以,我们还要欺骗一下PMS,让系统觉得插件已经安装在系统上了.
private static void hookPackageManager() throws Exception {
// 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
// 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.
Class<?> activityThreadClass = Class.forName(“android.app.ActivityThread”);
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(“currentActivityThread”);
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取ActivityThread里面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField(“sPackageManager”);
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName(“android.content.pm.IPackageManager”);
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
new Class<?>[] { iPackageManagerInterface },
new IPackageManagerHookHandler(sPackageManager));
// 1. 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
}
到这里,我们已经能够成功地加载简单的独立的存在于外部文件系统中的apk了。
上面我们是通过开头我们说的第一种方法实现,下面介绍另一种方案:
方案二:委托系统,让系统帮忙加载
再来看看ActivityThread中加载Activity类的代码:
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
我们知道 这个r.packageInfo中的r是通过getPackageInfoNoCheck获取到的;在上述方案中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?
从上一种方案可以知道如果没有命中缓存的情况下,系统直接new了一个LoadedApk;注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。在方案一中我们为了获取独立插件的ApplicationInfo花了不少心思;那么如果不做任何处理这里传入的这个aInfo参数是什么?
追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的,而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息。
接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!
宿主的ClassLoader在哪里,是唯一的吗?
答案是肯定的!
因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;由类加载机制的双亲委派
特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。
表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader。
LoadedApk的ClassLoader到底是什么?
不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么?这个方法源码如下:
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}
if (mIncludeCode && !mPackageName.equals(“android”)) {
// 略…
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);
StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}
可以看到,非android开头的包和android开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
mLoaders.put(zip, pathClassloader);
return pathClassloader;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return pathClassloader;
}
}
可以看到,应用程序使用的ClassLoader都是PathClassLoader
类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException(“Stub!”);
}
public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException(“Stub!”);
}
}
我们继续查看它的父类BaseDexClassLoader
:我们查看findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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
类的对象,它的findClass方法如下:
public Class findClass(String name, List 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;
}
这个DexPathList
内部有一个叫做dexElements
的数组,然后findClass的时候会遍历这个数组来查找Class;如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?
通过上述分析,我们知道,可以把插件的相关信息放入BaseDexClassLoader的表示dex文件的数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载.
接下来,我们实现这个过程:
public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 获取 BaseDexClassLoader : pathList
Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField(“pathList”);
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(cl);
// 获取 PathList: Element[] dexElements
Field dexElementArray = pathListObj.getClass().getDeclaredField(“dexElements”);
dexElementArray.setAccessible(true);
Object[] dexElements = (Object[]) dexElementArray.get(pathListObj);
// Element 类型
Class<?> elementClass = dexElements.getClass().getComponentType();
// 创建一个数组, 用来替换原始的数组
Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);
// 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));
Object[] toAddElementArray = new Object[] { o };
// 把原始的elements复制进去
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 插件的那个element复制进去
System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);
// 替换
dexElementArray.set(pathListObj, newElements);
}
以上我们就完成了委托宿主ClassLoader加载插件类
的任务,因此第二种方案也宣告完成.
总结如下:
-
默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。
-
宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。
两种方案的对比
学习宝典
对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。
很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
Android学习PDF+架构视频+面试文档+源码笔记
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
8%BF%99%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E9%9D%A2%E8%AF%95%E6%8B%BF%E9%AB%98%E8%96%AA%EF%BC%81.md)
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-iKspgdgh-1645112709521)]
【算法合集】
[外链图片转存中…(img-mam3gPCl-1645112709522)]
【延伸Android必备知识点】
[外链图片转存中…(img-UWpKb5DH-1645112709523)]
【Android部分高级架构视频学习资源】