引言
上一节,我们学习了Shadow源码解析之sample-manager(一),主要讲解了宿主的插件脚本/sample-manager.apk插件设计理念/宿主到sample-manager.apk插件入口的流程等
接下来,我们将学习宿主到sample-manager.apk插件入口后的系列流程,帮助我们了解插件sample-manager的动态管理其他业务插件的功能(如:加载其他插件/更新插件逻辑等)
代码分析
1.上节回顾
根据宿主的代码可以看出,mPluginManager.enter进入到了插件里面
2.插件入口代码位置
进入到插件里面后,enter的实现如上图,其中可以看到核心的实现是:onStartActivity
3.onStartActivity实现
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
//0)参数准备
final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
Log.i(TAG, "SamplePluginManager, onStartActivity,pluginZipPath = " + pluginZipPath);
Log.i(TAG, "SamplePluginManager, onStartActivity,partKey = " + partKey);
Log.i(TAG, "SamplePluginManager, onStartActivity,className = " + className);
executorService.execute(() -> {
try {
//1)插件的优化等,然后返回插件列表的第一个(默认)
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
//2)intent的包装
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
if (extras != null) {
pluginIntent.replaceExtras(extras);
}
//3)加载框架插件(如:loader/runtime)和业务插件,同时启动插件activity
startPluginActivity(installedPlugin, partKey, pluginIntent);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "SamplePluginManager, 插件启动,这个环节先不展开,下个阶段展开");
}
});
}
复制代码
根据代码可以看出,主要做了如下几件事:
1)参数解析
2)异步处理一些事物
2.1)插件的优化等,然后返回插件列表的第一个(默认)
2.2)intent的包装
2.3)加载框架插件(如:loader/runtime)和业务插件,同时启动插件activity
复制代码
3.参数解析
这里解析了几个参数:
1)pluginZipPath:插件包的路径,具体包里面的内容戳这里>>>
2)partKey:要启动插件的名字
3)className:要启动插件的activity名字
4.异步处理一些事物之插件的优化等
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
复制代码
简单的1句代码调用,完成了插件的优化等系列工作
可以看出输入参数是 pluginZipPath,输出为 InstalledPlugin 对象
InstalledPlugin 为 pluginZipPath 内存抽象,具体戳这里>>>
下面,我们看下具体的实现代码:
public InstalledPlugin installPlugin(String zip, String hash, boolean odex)
throws IOException, JSONException, InterruptedException, ExecutionException {
//1)zip 转换为 PluginConfig 配置
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
final String uuid = pluginConfig.UUID;
List<Future> futures = new LinkedList<>();
//2)插件runTime/pluginLoader 的 odex 优化
if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
//runTime
Future odexRuntime = mFixedPool.submit((Callable) () -> {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME, pluginConfig.runTime.file);
return null;
});
futures.add(odexRuntime);
//pluginLoader
Future odexLoader = mFixedPool.submit((Callable) () -> {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER, pluginConfig.pluginLoader.file);
return null;
});
futures.add(odexLoader);
}
//3)业务插件的so解压/odex优化等
for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
final String partKey = plugin.getKey();
final File apkFile = plugin.getValue().file;
//业务插件,插件apk的so解压
Future extractSo = mFixedPool.submit((Callable) () -> {
//插件apk的so解压
extractSo(uuid, partKey, apkFile);
return null;
});
futures.add(extractSo);
//业务插件,odex优化
if (odex) {
Future odexPlugin = mFixedPool.submit((Callable) () -> {
oDexPlugin(uuid, partKey, apkFile);
return null;
});
futures.add(odexPlugin);
}
}
//4)任务执行
for (Future future : futures) {
/**
* get()方法可以:
* 1)当任务结束后返回一个结果值,
* 2)如果工作没有结束,则会阻塞当前线程,直到任务执行完毕
* */
future.get();
}
//5)执行完毕,将插件信息持久化到数据库(如:soDir/oDexDir等)
onInstallCompleted(pluginConfig);
//6)获取已安装的插件,最后安装的排在返回List的最前面
return getInstalledPlugins(1).get(0);
}
复制代码
代码较长,主要做了如下几件事:
1)zip 转换为 PluginConfig 配置
2)框架插件runTime/pluginLoader 的 odex 优化
3)业务插件的so解压/odex优化等
4)插件信息持久化到数据库(如:soDir/oDexDir等)
5)返回业务插件中的第一个
那么每一件事怎么实现的?然后涉及的安卓知识有哪些?下面进行一一讲解
4.1 zip 转换为 PluginConfig 配置
//1)zip 转换为 PluginConfig 配置
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
复制代码
可以看出输入参数是 pluginZipPath,输出为 PluginConfig 对象
这里把zip内存化,具体戳这里>>>
4.2 框架插件runTime/pluginLoader 的 odex 优化
//2)框架插件runTime/pluginLoader 的 odex 优化
if (pluginConfig.runTime != null && pluginConfig.pluginLoader != null) {
//runTime
Future odexRuntime = mFixedPool.submit((Callable) () -> {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME, pluginConfig.runTime.file);
return null;
});
futures.add(odexRuntime);
//pluginLoader
Future odexLoader = mFixedPool.submit((Callable) () -> {
oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER, pluginConfig.pluginLoader.file);
return null;
});
futures.add(odexLoader);
}
复制代码
这里主要是对插件框架 runTime 和 pluginLoader的优化,下面我们看进一步的实现
public final void oDexPluginLoaderOrRunTime(String uuid, int type, File apkFile) throws InstallPluginException {
try {
File root = mUnpackManager.getAppDir();
File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
String key = type == InstalledType.TYPE_PLUGIN_LOADER ? "loader" : "runtime";
//核心实现
ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, key));
} catch (InstallPluginException e) {
throw e;
}
}
复制代码
上面创建一些目录,给到后面odex优化做准备
下面看下具体优化实现
public static void oDexPlugin(File apkFile, File oDexDir, File copiedTagFile) throws InstallPluginException {
String key = apkFile.getAbsolutePath();
//key:sample-loader-debug.apk / sample-runtime-debug.apk 等
//value:Object
Object lock = sLocks.get(key);
if (lock == null) {
lock = new Object();
sLocks.put(key, lock);
}
synchronized (lock) {
if (copiedTagFile.exists()) {
return;
}
//如果odex目录存在但是个文件,不是目录,那超出预料了。删除了也不一定能工作正常。
if (oDexDir.exists() && oDexDir.isFile()) {
throw new InstallPluginException("oDexDir=" + oDexDir.getAbsolutePath() + "已存在,但它是个文件,不敢贸然删除");
}
//创建oDex目录
oDexDir.mkdirs();
new DexClassLoader(
apkFile.getAbsolutePath(),//dexPath
oDexDir.getAbsolutePath(),//optimizedDirectory
null,//librarySearchPath
ODexBloc.class.getClassLoader());//ClassLoader parent
//执行成功,就创建tag标志文件
try {
copiedTagFile.createNewFile();
} catch (IOException e) {
throw new InstallPluginException("oDexPlugin完毕 创建tag文件失败:" + copiedTagFile.getAbsolutePath(), e);
}
}
}
复制代码
这里最核心的操作就是实例化了 DexClassLoader 来达到优化dex的操作
怎么实例化就可以了?
这里涉及到了一个安卓dex加载的知识点:
应用程序在第一次启动app的时候,会在/dalvik/dalvik-cache目录下生成odex文件结构
而我们的插件apk是没有经过安装的,启动也不是系统启动,所以插件的odex优化需要自己做,也就是直接new DexClassLoader 对象即可;
具体为什么系统需要做odex这件事,又涉及到了系统方面的知识,这里截取网上的一段话:
然后为什么直接new DexClassLoader 就可以实现dex优化为odex?具体细节流程解析可以网上自行查找知识点或者戳这里>>>
4.3 业务插件的so解压/odex优化等
for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
final String partKey = plugin.getKey();
final File apkFile = plugin.getValue().file;
//业务插件,插件apk的so解压
Future extractSo = mFixedPool.submit((Callable) () -> {
//插件apk的so解压
extractSo(uuid, partKey, apkFile);
return null;
});
futures.add(extractSo);
//业务插件,odex优化
if (odex) {
Future odexPlugin = mFixedPool.submit((Callable) () -> {
oDexPlugin(uuid, partKey, apkFile);
return null;
});
futures.add(odexPlugin);
}
}
复制代码
首先是so的解压
public final void extractSo(String uuid, String partKey, File apkFile) throws InstallPluginException {
try {
File root = mUnpackManager.getAppDir();
String filter = "lib/" + getAbi() + "/";
File soDir = AppCacheFolderManager.getLibDir(root, uuid);
CopySoBloc.copySo(apkFile, soDir
, AppCacheFolderManager.getLibCopiedFile(soDir, partKey), filter);
} catch (InstallPluginException e) {
throw e;
}
}
复制代码
代码比较简答,把so拷贝到指定的目录文件夹里面
然后是dex的优化
public final void oDexPlugin(String uuid, String partKey, File apkFile) throws InstallPluginException {
try {
File root = mUnpackManager.getAppDir();
File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, partKey));
} catch (InstallPluginException e) {
throw e;
}
}
复制代码
和上面奖到的优化原理一样,所以不再累赘
4.4 插件信息持久化到数据库(如:soDir/oDexDir等)
//5)执行完毕,将插件信息持久化到数据库(如:soDir/oDexDir等)
onInstallCompleted(pluginConfig);
复制代码
public final void onInstallCompleted(PluginConfig pluginConfig) {
File root = mUnpackManager.getAppDir();
String soDir = AppCacheFolderManager.getLibDir(root, pluginConfig.UUID).getAbsolutePath();
String oDexDir = AppCacheFolderManager.getODexDir(root, pluginConfig.UUID).getAbsolutePath();
mInstalledDao.insert(pluginConfig, soDir, oDexDir);
}
复制代码
这个只是简单的把插件包zip的一些信息数据库持久化,为后续业务使用做准备
4.5 返回业务插件中的第一个
//6)获取已安装的插件,最后安装的排在返回List的最前面
return getInstalledPlugins(1).get(0);
复制代码
/**
* 获取已安装的插件,最后安装的排在返回List的最前面
*
* @param limit 最多获取个数
*/
public final List<InstalledPlugin> getInstalledPlugins(int limit) {
return mInstalledDao.getLastPlugins(limit);
}
复制代码
这个也是比较简单,在刚才持久化数据库里面去除第一个业务插件的信息返回加载
5.异步处理一些事物之intent的包装
//2)intent的包装
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
if (extras != null) {
pluginIntent.replaceExtras(extras);
}
复制代码
要启动的插件的activity类名字给到intent等
6.异步处理一些事物之加载/启动系列插件等
//3)加载框架插件(如:loader/runtime)和业务插件,同时启动插件activity
startPluginActivity(installedPlugin, partKey, pluginIntent);
复制代码
public void startPluginActivity(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
//1)intent 的包装
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//2)启动
mPluginLoader.startActivityInPluginProcess(intent);
}
复制代码
这个环节出要做了2件事:
1)intent 2次的包装
2)启动插件的activity
6.1 intent 2次的包装
首先看下第一点,intent 2次的包装
//1)intent 的包装
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
复制代码
public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
//1)加载框架插件(如:loader/runtime)和业务插件
loadPlugin(installedPlugin.UUID, partKey);
//2)包装插件的intent等
return mPluginLoader.convertActivityIntent(pluginIntent);
}
复制代码
根据代码可以看出,在对intent进行包装前进行了框架插件的加载,然后才是包装
因为intent目的是启动业务插件的业务activity,所以首先要先准备好框架插件和业务插件,然后在插件中提取信息,封装到intent,这样才能启动成功
下面我们来详细看看里面的实现
a)加载框架插件(如:loader/runtime)和业务插件
private void loadPlugin(String uuid, String partKey) {
//1)加载框架插件: loader 和 runtime
loadPluginLoaderAndRuntime(uuid, partKey);
//2)加载业务插件:通过框架loader加载插件;
//例子:partKey = sample-plugin-app
mPluginLoader.loadPlugin(partKey);
}
复制代码
这里是先加载框架插件loader 和 runtime,然后利用框架loader来加载业务插件
下面我们来看下是如何加载框架插件的
private void loadPluginLoaderAndRuntime(String uuid, String partKey) {
/***
* sample-runtime-release.apk
* */
loadRunTime(uuid);
/**
*sample-loader-release.apk
* */
loadPluginLoader(uuid);
}
复制代码
我们以加载框架插件runtime为例子展开,其实就是加载插件zip里面的《sample-runtime-release.apk》
public final void loadRunTime(String uuid) {
InstalledApk installedApk;
// 1)loader插件内存化
installedApk = getRuntime(uuid);
InstalledApk installedRuntimeApk = new InstalledApk(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath);
//2)根据内存化对象,加载框架插件loader
boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
if (loaded) {
DynamicRuntime.saveLastRuntimeInfo(mHostContext, installedRuntimeApk);
}
}
复制代码
这个实现是阉割过的,因为原著那边是通过IPC BInder来远程调用,这里为了避免链路长所以实现把IPC那边调用省略了
这里主要是《loader插件内存化》 和 《根据内存化对象加载框架插件loader》
loader插件内存化,这一块没什么好讲的,所以省略
根据内存化对象加载框架插件loader,下面来重点看下这个
public static boolean loadRuntime(InstalledApk installedRuntimeApk) {
//宿主的 ClassLoader
ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();
//ClassLoader 的继承关系被改过,具体见: hackParentToRuntime(installedRuntimeApk, contextClassLoader);
RuntimeClassLoader runtimeClassLoader = getRuntimeClassLoader();
if (runtimeClassLoader != null) {
String apkPath = runtimeClassLoader.apkPath;
if (TextUtils.equals(apkPath, installedRuntimeApk.apkFilePath)) {
//已经加载相同版本的runtime了,不需要加载
Log.i(TAG, "DynamicRuntime, 已经加载相同apkPath的runtime了,不需要加载");
return false;
} else {
//版本不一样,说明要更新runtime,先恢复正常的classLoader结构
recoveryClassLoader();
}
}
//将runtime 挂到 pathclassLoader 之上
try {
Log.i(TAG, "DynamicRuntime, 正常处理,将runtime 挂到 pathclassLoader 之上");
hackParentToRuntime(installedRuntimeApk, contextClassLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
return true;
}
复制代码
/*
* contextClassLoader 为宿主的loader
*/
private static void hackParentToRuntime(InstalledApk installedRuntimeApk,ClassLoader contextClassLoader) throws Exception {
RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(
installedRuntimeApk.apkFilePath,
installedRuntimeApk.oDexPath,
installedRuntimeApk.libraryPath,
contextClassLoader.getParent());
hackParentClassLoader(contextClassLoader, runtimeClassLoader);
}
复制代码
static void hackParentClassLoader(ClassLoader classLoader, ClassLoader newParentClassLoader) throws Exception {
Field field = getParentField();
if (field == null) {
throw new RuntimeException("在ClassLoader.class中没找到类型为ClassLoader的parent域");
}
field.setAccessible(true);
field.set(classLoader, newParentClassLoader);
}
复制代码
这里看起来代码比较多,但实际上原理很简单
就是构建Classloader来加载插件,用的方式是《替换 PathClassloader 的 parent》的方案,这个方案的介绍具体戳这里>>>
到这里加载框架插件runtime(sample-runtime-release.apk)就讲完了;
下面我们看下框架插件loader(sample-loader-release.apk)的加载
public final void loadPluginLoader(String uuid) {
InstalledApk installedApk;
installedApk = getPluginLoader(uuid);
File file = new File(installedApk.apkFilePath);
if (!file.exists()) {
Log.e(TAG, file.getAbsolutePath() + ", 文件不存在");
}
LoaderImplLoader implLoader = new LoaderImplLoader();
mPluginLoader = implLoader.load(installedApk, uuid, mHostContext);
}
复制代码
public PluginLoaderImpl load(InstalledApk installedApk, String uuid, Context appContext) {
ApkClassLoader pluginLoaderClassLoader = new ApkClassLoader(
installedApk,
LoaderImplLoader.class.getClassLoader(),
loadWhiteList(installedApk),
1
);
//从apk中,读取接口的实现
//plugin-debug.zip/sample-loader-debug.apk
LoaderFactory loaderFactory = null;
try {
loaderFactory = pluginLoaderClassLoader.getInterface(
LoaderFactory.class,
sLoaderFactoryImplClassName
);
} catch (Exception e) {
e.printStackTrace();
}
if (loaderFactory == null) {
return null;
}
//buildLoader
return loaderFactory.buildLoader(uuid, appContext);
}
复制代码
根据代码可以看出loader的实现是在sample-loader-release.apk里面的
而加载插件框架用到的是加载器是
class ApkClassLoader extends DexClassLoader {
static final String TAG = "daviAndroid";
private ClassLoader mGrandParent;
private final String[] mInterfacePackageNames;
ApkClassLoader(InstalledApk installedApk,
ClassLoader parent,////parent = 宿主ClassLoader
String[] mInterfacePackageNames,
int grandTimes) {
super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);
//默认代
ClassLoader grand = parent;//parent = 宿主ClassLoader
//外面定第几代
for (int i = 0; i < grandTimes; i++) {
grand = grand.getParent();
}
mGrandParent = grand;
this.mInterfacePackageNames = mInterfacePackageNames;
}
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
String packageName;
int dot = className.lastIndexOf('.');
if (dot != -1) {
packageName = className.substring(0, dot);
} else {
packageName = "";
}
boolean isInterface = false;
for (String interfacePackageName : mInterfacePackageNames) {
if (packageName.equals(interfacePackageName)) {
isInterface = true;
break;
}
}
//apkFilePath = /data/user/0/com.tencent.shadow.sample.host/files/pluginmanager.apk
//oDexPath = /data/user/0/com.tencent.shadow.sample.host/files/ManagerImplLoader/ksi9pl9k
if (isInterface) {
//情况1:插件可以加载宿主的类实现:
return super.loadClass(className, resolve);
} else {
//情况2:插件不需要加载宿主的类实现
Class<?> clazz = findLoadedClass(className);//1)系统里面找
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
//否则先从自己的dexPath中查找
clazz = findClass(className);//2)自己的dexPath中查找
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
//如果找不到,则再从parent的parent ClassLoader中查找。
//BootClassLoader
try {
clazz = mGrandParent.loadClass(className);//父亲找
} catch (ClassNotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
e.addSuppressed(suppressed);
}
throw e;
}
}
}
return clazz;
}
}
<T> T getInterface(Class<T> clazz, String className) throws Exception {
try {
Class<?> interfaceImplementClass = loadClass(className);
Object interfaceImplement = interfaceImplementClass.newInstance();
return clazz.cast(interfaceImplement);
} catch (ClassNotFoundException | InstantiationException
| ClassCastException | IllegalAccessException e) {
throw new Exception(e);
}
}
}
复制代码
这个加载器的原理其实就是以前说过的,具体戳这里>>>
加载完框架插件runtime和loader之后,那么就会利用框架插件loader来加载业务插件
//2)加载业务插件:通过框架loader加载插件;
//例子:partKey = sample-plugin-app
mPluginLoader.loadPlugin(partKey);
复制代码
有人可能会好奇,为什么业务插件的loader要做成框架插件方式来加载业务插件?
这个就是shadow的动态设计,所有的模块都是动态的(如:业务插件下载逻辑/业务插件加载等),具体前面有介绍戳这里>>>
因为目前阶段的讲解是在sample-manager模块,也就是框架插件中的sample-manager.apk的源码解析,具体框架loader是怎么加载业务插件的实现这里先不展开,后面模块解析到loader.apk的时候会讲,知道有这么回事即可
b)包装插件的intent等
//2)包装插件的intent等
return mPluginLoader.convertActivityIntent(pluginIntent);
复制代码
这个实现也是在loader里面的,其实内容主要是把业务插件的一些信息包装到intent里面
具体实现暂时不展开,后续讲解loader.apk模块的时候讲解
6.2 启动插件的activity
//2)启动
mPluginLoader.startActivityInPluginProcess(intent);
复制代码
这个实现也是在loader里面的,具体实现暂时不展开,后续讲解loader.apk模块的时候讲解
结尾
哈哈,该篇就写到这里(一起体系化学习,一起成长)
Tips
更多精彩内容,请关注 ”Android热修技术“ 微信公众号