1.前言
在前面讲述的Android插件化探索(二)hook式插件化中有优点也有缺点。优点就是插件中的Activity等组件不需要依赖宿主APP的环境,可以随意的使用this和context等上下文对象。但是缺点也非常明显,就是如果插件越多,内存中的dexElements数组就会越来越大,可能会造成内存溢出等异常。今天讲的LoadedApk式插件化就解决了Hook式插件化的这个缺点。
2.startActivity源码分析
我们还是从startActivity 开始分析。
startActivity() --> Activity.startActivity() --> Activity.startActivityForResult() --> Instrumentation.execStartActivity() --> ActivityTaskManager.getService().startActivity(AMS检查)
ActivityThread.handleLaunchActivity() --> performLaunchActivity()(自己去处理LoadedApk中的ClassLoader)
我们知道,Activity 调用 startActivity 之后最后会在 ActivityThread 执行 handleLaunchActivity我们直接跟进这个方法查到我们的 Activity 初始化就是由 LoadedApk 里面的 mClassLoader进行加载的。由此得到我们的Hook点,我们如果自定定义一个LoadedApk 并把里面的 mClassLoader 给替换为我们自己的 ClassLoader 就可以加载插件的类了。由以下图中可以看出,我们在回调 handleLaunchActivity() 之前会通过 getPackageInfoNoCheck() 方法初始化我们的 LoadedApk 对象,并存放如全局的变量 mPackages 中,所以我们要创建一个插件的 LoadedApk 并且把它添加到 mPackages 这个集合中就行了。
我们简单画个图来简述一下该流程:
3.代码实现自定义LoadedApk
下面我们开始撸码实现自己的 LoadedApk,跟前面讲的 Hook 式插件化一下,我们也需要 Hook住AMS的检查和ActivityThread的 Handle 的回调方法,所以我们在之前的代码上面改动,只是屏蔽掉了融合宿主和插件的 dexElements数组。没有看过Hook式插件化的小伙伴可以点击这里查看(Android插件化探索(二)hook式插件化)。
前面我们查看源码可以得到,ActivityThread 里面有个存放 LoadedApk 的集合为 mPackages,所以我们首先获取到这个变量,其次自定义我们的 LoadedApk,由于 LoadedApk是不对开发人员开放的,所以我们只能通过上面讲到的 getPackageInfoNoCheck() 方法来返回一个 LoadedApk 实例,接着拿到 自定义一个 ClassLoader,把 LoadedApk 里面的 ,,mClassLoader替换为我们自己定义的 ClassLoader。最后把我们自定义的 LoadedApk 存入 mPackages 这个集合中。
简单流程分析为:
1、反射获取 ActivityThread 的 mPackages
2、自定义一个 LoadedApk
3、自定义一个 ClassLoader
4、反射 LoadedApk 的 mClassLoader,并将自定义的 ClassLoader 赋值给它
5、把自定义的 LoadedApk 存入 mPackages 中
下面我们跟着上面的流程一步一步的进行代码实现。
3.1 获取ActivityThread的mPackages
这个比较简单,我这里直接贴上代码了。
//获取 ActivityThread 类
Class<?> mActivityThreadClass = Class.forName("android.app.ActivityThread");
//获取 ActivityThread 的 currentActivityThread() 方法
Method currentActivityThread = mActivityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
//获取 ActivityThread 实例
Object mActivityThread = currentActivityThread.invoke(null);
//final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
//获取 mPackages 属性
Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
//获取 mPackages 属性的值
ArrayMap<String, Object> mPackages = (ArrayMap<String, Object>) mPackagesField.get(mActivityThread);
3.2 自定义一个LoadedApk
自定义我们的 LoadedApk 比较复杂,我们着重分析一下这个。首先我们知道获取一个 LoadedApk 实例我们可以通过反射调用 getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo) 方法。该方法要传递两个参数,第一个类型是 ApplicationInfo, 第一个类型是 CompatibilityInfo,我们如何能获取到这两个类的实例呢?
3.2.1 获取CompatibilityInfo实例
第二个参数比较容易获取,我们先易后难。跟进 CompatibilityInfo 这个类的源码可以得到,该类有一个静态变量 DEFAULT_COMPATIBILITY_INFO 就是它本身实例对象,我们可以通过反射来拿到这个变量,然后就解决第二个参数问题了。
3.2.2 获取ApplicationInfo实例
到第二个参数了,在之前第一篇文章中提过,源码中有一个PackageParser.java 类,里面有一个方法 generateApplicationInfo() 可以返回一个 ApplicationInfo 实例,我们可以通过反射这个方法来获取一个 ApplicationInfo 实例,但是方法中,需要传递三个参数,类型分别为 Package(该类是 PackageParser 的内部类),和int以及 PackageUserState。这三个类型的实例我们从何获取呢?
查看 PackageParser 源码发现可以通过 parsePackage() 方法可以返回一个 Package 实例,并且只需要传入我们插件的 File实例和一个int值就可以了。
这样第一个参数解决了,第三个参数我们可以通过反射获取类,然后执行 newInstance() 来获取一个实例,第二个参数我们直接传个0就好了,通过以上方法,我们就可以获取到一个 ApplicationInfo 实例了,如下:
private ApplicationInfo getAppInfo(File file) throws Exception {
/*
执行此方法获取 ApplicationInfo
public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state)
*/
Class<?> mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Class<?> mPackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
//获取 generateApplicationInfo 方法
Method generateApplicationInfoMethod = mPackageParserClass.getDeclaredMethod("generateApplicationInfo",
mPackageClass, int.class, mPackageUserStateClass);
//创建 PackageParser 实例
Object mPackageParser = mPackageParserClass.newInstance();
//获取 Package 实例
/*
执行此方法获取一个 Package 实例
public Package parsePackage(File packageFile, int flags)
*/
//获取 parsePackage 方法
Method parsePackageMethod = mPackageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
//执行 parsePackage 方法获取 Package 实例
Object mPackage = parsePackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);
//执行 generateApplicationInfo 方法,获取 ApplicationInfo 实例
ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(null, mPackage, 0,
mPackageUserStateClass.newInstance());
//我们获取的 ApplicationInfo 默认路径是没有设置的,我们要自己设置
// applicationInfo.sourceDir = 插件路径;
// applicationInfo.publicSourceDir = 插件路径;
applicationInfo.sourceDir = file.getAbsolutePath();
applicationInfo.publicSourceDir = file.getAbsolutePath();
return applicationInfo;
}
3.2.3 获取LoadedApk实例
得到两个参数的实例之后,我们就可以获取一个 LoadedApk实例了,代码如下:
//自定义一个 LoadedApk,系统是如何创建的我们就如何创建
//执行下面的方法会返回一个 LoadedApk,我们就仿照系统执行此方法
/*
this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo,
compatInfo);
public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
int flags)
*/
Class<?> mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getLoadedApkMethod = mActivityThreadClass.getDeclaredMethod("getPackageInfoNoCheck",
ApplicationInfo.class, mCompatibilityInfoClass);
/*
public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {};
*/
//以上注释是获取默认的 CompatibilityInfo 实例
Field mCompatibilityInfoDefaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
Object mCompatibilityInfo = mCompatibilityInfoDefaultField.get(null);
//获取一个 ApplicationInfo实例
ApplicationInfo applicationInfo = getAppInfo(file);
//执行此方法,获取一个 LoadedApk
Object mLoadedApk = getLoadedApkMethod.invoke(mActivityThread, applicationInfo, mCompatibilityInfo);
3.3 自定义一个ClassLoader
这个比较简单,前面文章也有提过,就不多讲了,直接上代码:
//自定义一个 ClassLoader
String optimizedDirectory = context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath();
DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory,
null, context.getClassLoader());
3.4 替换LoadedApk的mClassLoader为自定义的ClassLoader
这个也是很简单,直接上代码:
//获取 LoadedApk 的 mClassLoader 属性
Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
//设置自定义的 classLoader 到 mClassLoader 属性中
mClassLoaderField.set(mLoadedApk, classLoader);
3.5 把自定义的LoadedApk存入mPackages中
WeakReference loadApkReference = new WeakReference(mLoadedApk);
//添加自定义的 LoadedApk
mPackages.put(applicationInfo.packageName, loadApkReference);
//重新设置 mPackages
mPackagesField.set(mActivityThread, mPackages);
通过上面的步骤,我们就成功的将自定义的 LoadedApk 存入 mPackages 中。完整的代码来一波:
public void customLoadApkAction() throws Exception {
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "plugin2.apk");
if (!file.exists()) {
throw new FileNotFoundException("插件包不存在");
}
//获取 ActivityThread 类
Class<?> mActivityThreadClass = Class.forName("android.app.ActivityThread");
//获取 ActivityThread 的 currentActivityThread() 方法
Method currentActivityThread = mActivityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
//获取 ActivityThread 实例
Object mActivityThread = currentActivityThread.invoke(null);
//final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
//获取 mPackages 属性
Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
//获取 mPackages 属性的值
ArrayMap<String, Object> mPackages = (ArrayMap<String, Object>) mPackagesField.get(mActivityThread);
//自定义一个 LoadedApk,系统是如何创建的我们就如何创建
//执行下面的方法会返回一个 LoadedApk,我们就仿照系统执行此方法
/*
this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo,
compatInfo);
public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
int flags)
*/
Class<?> mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getLoadedApkMethod = mActivityThreadClass.getDeclaredMethod("getPackageInfoNoCheck",
ApplicationInfo.class, mCompatibilityInfoClass);
/*
public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {};
*/
//以上注释是获取默认的 CompatibilityInfo 实例
Field mCompatibilityInfoDefaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
Object mCompatibilityInfo = mCompatibilityInfoDefaultField.get(null);
//获取一个 ApplicationInfo实例
ApplicationInfo applicationInfo = getAppInfo(file);
// applicationInfo.uid = context.getApplicationInfo().uid;
//执行此方法,获取一个 LoadedApk
Object mLoadedApk = getLoadedApkMethod.invoke(mActivityThread, applicationInfo, mCompatibilityInfo);
//自定义一个 ClassLoader
String optimizedDirectory = context.getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath();
DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory,
null, context.getClassLoader());
//private ClassLoader mClassLoader;
//获取 LoadedApk 的 mClassLoader 属性
Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
//设置自定义的 classLoader 到 mClassLoader 属性中
mClassLoaderField.set(mLoadedApk, classLoader);
WeakReference loadApkReference = new WeakReference(mLoadedApk);
//添加自定义的 LoadedApk
mPackages.put(applicationInfo.packageName, loadApkReference);
//重新设置 mPackages
mPackagesField.set(mActivityThread, mPackages);
Thread.sleep(2000);
}
4. 运行报错解决
代码编写好了,运行一下,大力出奇迹,竟然崩溃了。
还是要静下心来看看崩溃日志,我的工程是运行在 Android10.0的设备上的,在低版本的设备上不会报此异常。
原因是我的插件不属于这个进程,查看一下调用的系统api方法,最终定位在了 ContentProviderNative.call() 上,由于这个是远程Binder,我们hook不到,所以我们退回来看android.provider.Settings.NameValueCache.getStringForUser(Settings.java:2374) 这一段,跟踪这一个发现,报错的位置了。
我们发现, NameValueCache 这个类的 mProviderHolder 属性可以返回一个 IContentProvider 实例,IContentProvider 是一个接口,我们可以以动态代理的方式,替换掉 call() 方法的包名为宿主的包名应该就可以避免上面那个异常了。通过日志调用栈发现我们先是通过 Settings$Global.getStringForUser()
通过源码发现,Global 类里有这两个关键属性,通过这两属性,我们就可以反射到刚才导致报错的call方法了。下面按步骤进行反射获取:
1、获取Settings$Global类的sProviderHolder属性
Field sProviderHolderFiled = Settings.Global.class.getDeclaredField("sProviderHolder");
sProviderHolderFiled.setAccessible(true);
Object sProviderHolder = sProviderHolderFiled.get(null);
2、获取Settings$ContentProviderHolder的getProvider()方法
Method getProviderMethod = sProviderHolder.getClass().getDeclaredMethod("getProvider", ContentResolver.class);
getProviderMethod.setAccessible(true);
3、获取原来的 IContentProvider 实例对象
final Object iContentProvider = getProviderMethod.invoke(sProviderHolder, context.getContentResolver());
4、Settings$获取 ContentProviderHolder类的mContentProvider属性
Field mContentProviderFiled = sProviderHolder.getClass().getDeclaredField("mContentProvider");
mContentProviderFiled.setAccessible(true);
5、获取IContentProvider类
Class<?> mIContentProviderClass = Class.forName("android.content.IContentProvider");
6、创建我们自己的代理对象
Object mContentProviderProxy = Proxy.newProxyInstance(
context.getClassLoader(),
new Class[]{mIContentProviderClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("call")) {
Log.d("yuongzw", method.getName());
//替换成宿主的包名
args[0] = context.getPackageName();
}
return method.invoke(iContentProvider, args);
}
}
);
7、设置我们的代理对象到 mContentProvider属性中
mContentProviderFiled.set(sProviderHolder, mContentProviderProxy);
通过这7个步骤完成了我们对错误日志报错的修复,再次运行一遍看看效果。
又出现报错了,不应该啊?我不是已经修复了吗?看了一下错误日志,报的错跟上次的不太一样了,这次的是 Settings$System 这个类了,我们根据前面的7个步骤再次对这个类的一些属性进行hook就行了。到这里,所有的错误信息都修复了,最后上一个效果图:
项目地址:LoadApkDemo