本文旨在总结插件化主要解决的问题,即假设没有使用任何框架,从使用流程来一步步分析,如何打开本地某个apk文件的launchActivity。
1. 占坑
首先要在宿主的清单文件中声明一个StubActivity。用来绕过AMS检查。可声明多个不同启动模式的Activity。因为如果打开没有在清单文件中声明过的Activity,就会抛出ActivityNotFound异常。该StubActivity就是用来绕过AMS的该项检查。
2. 加载插件
想要打开插件,获取插件的信息,比如launchActivity,首先要将其加载到内存中。
2.1 解析并获取插件的ApplicationInfo
通过PackageParser来解析插件apk的清单文件信息,然后生成对应的ApplicationInfo。
2.2 获取插件的启动Activity
获取到插件的ApplicationInfo对象,即可通过:
Intent intent = pm.getLaunchIntentForPackage(packageInfo.packageName);
来获取插件的启动Activity的Intent。因为宿主并不知道插件的启动Activity是哪个,也不知道插件中都有什么类。
3. 启动插件
想要启动一个插件,需要解决的问题如下:
1. 如何绕过AMS的检查
2. 如何加载插件中的类
问题一:如何绕过AMS检查
我们都知道,如果通过Intent打开一个没有在清单文件中注册过的Activity,系统会抛出一个ActivityNotFoundException异常。因此,我们需要在宿主的清单文件中声明一个StubActivity,然后在将该意图交给AMS的最后一步,hook掉该Intent中的受检信息,将该Intent的插件ComponentName替换为宿主的ComponentName,然后将原始的Intent作为param存放到新的Intent中,将新的Intent交给ASM。此时启动一个未注册的Activity就不会报异常了。
在经过AMS一系列操作后,最终AMS会通过ApplicationThread来通知宿主app来启动该Activity。最终会分发到H(Handler)中的handleLaunchActivity方法。通过hook掉ActivityThread中的mH,在dispatchMessage中,handleMessage之前,将之前被替换掉的插件的Intent中的信息再替换回来。
另外,由于Activity与AMS声明周期的回调时通过token来认证的,因此更换之后并不会影响其生命周期。该token是在AMS操作返回之后,在ActivityThread的handleLaunchActivity方法中,反射创建完Activity后,在随后的activity.attach方法中进行复制的,并会将AMS返回的token存放到插件Activity的mToken变量中。因此后续也可以进行正常的IPC。
详情可参考:Android 插件化原理解析——Activity生命周期管理
替换回来之后,在handleLaunchActivity方法中最终会通过反射创建该插件的Activity,但是插件中的类肯定没有被宿主app给加载到指定的内存或者目录,因此肯定会报出ClassNotFound异常。接下来该怎么办呢?
问题二:如何加载插件中的类
再看一下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.getClassLoader();
获取到一个classLoader,然后通过反射创建该Activity。
而r.packageInfo是一个LoadedApk对象。最终也是通过该对象中的getClassLoader
方法来获取一个类加载器。LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。
因此在创建Activity的时候,会先根据一个LoadedApk对象来获取一个类加载器,然后通过类加载器来加载该类的代码。
跟踪代码,我们找到了最终这个LoadedApk生成的方法中:
//存放 pkgName--LoadedApk 的缓存
final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
//读取缓存
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
//没有缓存,新建
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
//存入缓存
if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
该方法主要分三个步骤:
- 从缓存获取,命中则直接返回。
- 如没有在缓存中,则直接创建一个LoadedApk。
- 将创建的LoadedApk加入缓存。
方法一:
第一个方法就是我们自己创建一个插件对应的LoadedApk,然后通过反射将生成的插件的LoadedApk对象加入上述的Map缓存中,这样每次肯定会命中缓存,然后获取该对象中的类记载器来加载插件的类,从而实现加载插件类的目的。
所以下边的主要矛盾就成为了如何通过一个插件apk构建为LoadedApk。
但是此处创建LoadedApk所需参数甚多,中间过程不好把握,而且是私有的,容易有兼容问题,所以我们通过查看源码,找到该方法的上级调用方法:
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}
此处只需两个参数,第二个传默认的兼容信息即可。因此现在的首要任务就是如何构建插件的ApplicationInfo对象。ApplicationInfo内存中对清单文件解析结果的映射。系统是通过PackageParser来对apk文件的清单文件进行xml解析的。DroidPlugin也是通过反射该api进行插件的ApplicationInfo的解析。如对解析细节感兴趣的同学请自行观看源码。
在解析完成之后,我们的LoadedApk也就可以通过反射构建完毕,构建完成之后,再通过反射将其添加到mPackages的map缓存集合中。具体实现细节请参考Android 插件化原理解析——插件加载机制
这样一来,宿主中就会存在多个类加载器,来加载各自对应的LoadedApk中对应的类和资源。
上述的方法是多 类加载器机制,宿主中存在多个类记载器,每个插件对应自己的classLoader,即宿主与插件之间类的加载相互隔离互不干扰。因为默认情况下,宿主的类加载器无法加载插件中的类。
那么有没有一种方法可以让宿主的ClassLoader可以加载插件中的类呢?请看方法二。
方法二:
首先要看通过LoadedApk.getClassLoader返回的是什么。
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
if (mPackageName.equals("android")) {
if (mClassLoader != null) {
return;
}
if (mBaseClassLoader != null) {
mClassLoader = mBaseClassLoader;
} else {
mClassLoader = ClassLoader.getSystemClassLoader();
}
return;
}
if (mClassLoader == null) {
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
libraryPermittedPath, mBaseClassLoader);
}
}
非android开头,进入ApplicationLoader.getClassLoader:
最终调用了如下方法新建了一个PathClassLoader:
PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
zip,
librarySearchPath,
libraryPermittedPath,
parent,
targetSdkVersion,
isBundled);
image.png
PathClassLoader是ClassLoader一个子类。
在通过该PathClassLoader查找类的实现是在其父类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 { //根据构造的pathList查找dexFile中的类
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) { //没有找到类,则抛出ClassNotFound异常。
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
@Override
protected URL findResource(String name) { //查找插件中的资源
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
最后发现,findClass的真正实现是通过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 final Element[] dexElements;
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;
}
}
}
return null;
}
可以看到,最终的findClass是通过轮询Element数组来查找类。
因此,第二种方法就是构建一个插件的Element对象,然后通过反射将其插入dexElements数组中,使其实现加载插件类的功能
Element类构造函数如下:
/**
* Element of the dex/resource file path
* dex/resource 文件路径的元素
*/
/*package*/ static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile; //This class is used to read entries from a zip file.【该类用来从zip文件中读取条目】
private boolean initialized;
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
this.dir = dir;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFile = dexFile;
}
...
}
对于这两种方法各自的优缺点,请参考Android 插件化原理解析——插件加载机制一文。
通过上边的两种方法,宿主已经能够正常加载插件中的类,并通过hook来实现Activity的替换。
总结
解决宿主中接在插件类的问题:
方法一:多ClassLoader机制,每个插件有自己的类加载器。
- 通过PackageParser解析插件构建插件的ApplicationInfo。
- 根据构建的ApplicationInfo构建插件的LoadedApk。
- 通过反射将插件的LoadedApk存入ActivityThread中的mPackages缓存中。
方法二:单ClassLoader机制,让宿主类加载器可以加载插件类
- 查看源码,找到宿主的ClassLoader的findClass方法。【PathClassLoader的父类BaseDexClassLoader中,通过变量PathDexList中持有的dexElements数组来查找】
- 通过插件路径构建插件的Element对象,然后反射将其插入dexElements数组。
解决启动不再清单文件中声明的Activity问题:
- 占坑,宿主清单文件中声明StubActivity。
- 获取插件启动Activity的Intent,在startActivity后交给AMS检查之前,将Intent中插件的信息替换为宿主的信息,并将旧的Inent作为extra保存到新的Intent中
- 在AMS检查回来之后,新建Activity之前,再将其替换回来。此时通过反射创建的Activity就是插件中的Activity了,同时由于插件Activity持有的与AMS通信的binder,因此可以进行后续的生命周期回调及IPC。
网友评论