The introduction
In the last section, we learned sample-Manager (I) of Shadow source code parsing, mainly explaining the host plug-in script/sample-Manager. apk plug-in design concept/host to sample-Manager. apk plug-in entry process, etc
Next, we will learn a series of processes after being hosted in the sample-manager.apk plug-in entrance to help us understand the functions of the plug-in sample-Manager to dynamically manage other business plug-ins (such as loading other plug-ins/updating plug-in logic, etc.).
The code analysis
1. Review of the previous section
As you can see from the host code, mpluginManager.enter enters the plug-in
2. Location of plug-in entry code
After entering the plug-in, the implementation of Enter is shown in the figure above, where you can see the core implementation is onStartActivity
3. OnStartActivity implementation
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
//0) Prepare parameters
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) Plug-in optimization etc, then return the first plug-in list (default)
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null.true);
//2) Intent wrapper
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
if(extras ! =null) {
pluginIntent.replaceExtras(extras);
}
//3) Load the framework plug-in (such as loader/ Runtime) and service plug-in, and start the plug-in activity
startPluginActivity(installedPlugin, partKey, pluginIntent);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "SamplePluginManager, the plug-in is started, so I'm not going to expand it, I'm going to expand it in the next stage."); }}); }Copy the code
As can be seen from the code, the following things are mainly done:
1) Parameter analysis
2) Do something asynchronously
2.1) Plug-in optimization, etc. Then return the first (default) plug-in list. 2.2) Intent wrapper 2.3) Load the framework plug-in (e.g. loader/ Runtime) and business plug-in, and start the plug-in activity at the same timeCopy the code
3. Parameter analysis
Several parameters are parsed here:
1) pluginZipPath: pluginZipPath: pluginZipPath: pluginZipPath: pluginZipPath: pluginZipPath
2) partKey: The name of the plug-in to launch
3) className: The name of the activity to start the plug-in
4. Optimization of plug-ins for asynchronous processing of some things
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null.true);
Copy the code
Simple 1 sentence code call, completed the plug-in optimization and other series of work
You can see that the input parameter is pluginZipPath and the output is the InstalledPlugin object
InstalledPlugin is a pluginZipPath memory abstraction.
Let’s take a look at the implementation code:
public InstalledPlugin installPlugin(String zip, String hash, boolean odex)
throws IOException, JSONException, InterruptedException, ExecutionException {
//1) Convert zip to PluginConfig configuration
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
final String uuid = pluginConfig.UUID;
List<Future> futures = new LinkedList<>();
//2) Odex optimization for plugins runTime/pluginLoader
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) Business plug-in so decompression /odex optimization, etc
for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
final String partKey = plugin.getKey();
final File apkFile = plugin.getValue().file;
// Business plug-in, plug-in APk so decompression
Future extractSo = mFixedPool.submit((Callable) () -> {
// Plugin apk so decompress
extractSo(uuid, partKey, apkFile);
return null;
});
futures.add(extractSo);
// Business plug-in, odex optimization
if (odex) {
Future odexPlugin = mFixedPool.submit((Callable) () -> {
oDexPlugin(uuid, partKey, apkFile);
return null; }); futures.add(odexPlugin); }}//4) Task execution
for (Future future : futures) {
The /** * get () method can: * 1) return a result value when the task is finished, * 2) block the current thread until the task is finished ** / if the work is not finished
future.get();
}
//5) Persist the plug-in information to the database (such as soDir/oDexDir, etc.)
onInstallCompleted(pluginConfig);
//6) Get the installed plug-ins. The last one installed is at the top of the returned List
return getInstalledPlugins(1).get(0);
}
Copy the code
The code is longer and does the following:
1) Convert zip to PluginConfig configuration
2) Odex optimization of framework plug-in runTime/pluginLoader
3) Business plug-in so decompression /odex optimization, etc
4) Persistence of plug-in information to database (e.g. SoDir /oDexDir, etc.)
5) Return the first in the business plug-in
So how did each of these things happen? And then what about Android? The following will explain one by one
4.1 Zip conversion to PluginConfig configuration
//1) Convert zip to PluginConfig configuration
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
Copy the code
You can see that the input parameter is pluginZipPath and the output is the PluginConfig object
Here is to memory zip, the specific stamp here >>>
4.2 Odex optimization of framework plug-in runTime/pluginLoader
//2) Odex optimization for framework plug-in runTime/pluginLoader
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);
}
Copy the code
Here are the optimizations for the plugin framework runTime and pluginLoader. Let’s look at further implementations
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";
// Core implementation
ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, key));
} catch (InstallPluginException e) {
throwe; }}Copy the code
Create some directories above to prepare for odex optimization later
Let’s look at the specific optimization implementation
public static void oDexPlugin(File apkFile, File oDexDir, File copiedTagFile) throws InstallPluginException {
String key = apkFile.getAbsolutePath();
//key: sample-loader-debug.apk/sample-Run-time debug.apk etc
/ / value: the Object
Object lock = sLocks.get(key);
if (lock == null) {
lock = new Object();
sLocks.put(key, lock);
}
synchronized (lock) {
if (copiedTagFile.exists()) {
return;
}
// If the odex directory exists but is a file, not a directory, that is unexpected. It doesn't always work if you delete it.
if (oDexDir.exists() && oDexDir.isFile()) {
throw new InstallPluginException("oDexDir=" + oDexDir.getAbsolutePath() + "It already exists, but it's a file. I'm afraid to delete it.");
}
// Create the oDex directory
oDexDir.mkdirs();
new DexClassLoader(
apkFile.getAbsolutePath(),//dexPath
oDexDir.getAbsolutePath(),//optimizedDirectory
null.//librarySearchPath
ODexBloc.class.getClassLoader());//ClassLoader parent
// Execute successfully, create tag file
try {
copiedTagFile.createNewFile();
} catch (IOException e) {
throw new InstallPluginException("ODexPlugin failed to create tag file:"+ copiedTagFile.getAbsolutePath(), e); }}}Copy the code
The core operation here is to instantiate DexClassLoader to optimize dex
How do you instantiate it?
Here’s a primer on loading android Dex:
The odex file structure will be generated in /dalvik/dalvik-cache when the application starts the app for the first time
However, our plug-in APK has not been installed, and the startup is not system startup, so the odex optimization of the plug-in needs to be done by ourselves, that is, the new DexClassLoader object can be directly used.
The specific reason why the system needs to do odex involves the knowledge of the system. Here is a passage from the Internet:
Then why can dex be optimized to Odex by directly new DexClassLoader? The details of the process can be found on the Internet.
4.3 Business plug-in so decompression/Odex optimization, etc
for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
final String partKey = plugin.getKey();
final File apkFile = plugin.getValue().file;
// Business plug-in, plug-in APk so decompression
Future extractSo = mFixedPool.submit((Callable) () -> {
// Plugin apk so decompress
extractSo(uuid, partKey, apkFile);
return null;
});
futures.add(extractSo);
// Business plug-in, odex optimization
if (odex) {
Future odexPlugin = mFixedPool.submit((Callable) () -> {
oDexPlugin(uuid, partKey, apkFile);
return null; }); futures.add(odexPlugin); }}Copy the code
The first is so decompression
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) {
throwe; }}Copy the code
The code is relatively simple, copy so to the specified directory folder
Then there is the optimization of 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) {
throwe; }}Copy the code
Same optimization principle as above, so no longer redundant
4.4 Persisting Plug-in Information to the Database (for example, soDir/oDexDir)
//5) Persist the plug-in information to the database (such as soDir/oDexDir, etc.)
onInstallCompleted(pluginConfig);
Copy the code
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);
}
Copy the code
This is simply to persist some information about the plug-in package ZIP database for future business use
4.5 Return the first business plug-in
//6) Get the installed plug-ins. The last one installed is at the top of the returned List
return getInstalledPlugins(1).get(0);
Copy the code
/** * gets the installed plug-ins, with the last installed at the top of the return List **@paramLimit Maximum number of entries */
public final List<InstalledPlugin> getInstalledPlugins(int limit) {
return mInstalledDao.getLastPlugins(limit);
}
Copy the code
This one is also relatively simple, in the persistent database just remove the first business plug-in information return load
5. Intent wrappers that handle something asynchronously
//2) Intent wrapper
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
if(extras ! =null) {
pluginIntent.replaceExtras(extras);
}
Copy the code
The name of the activity class of the plug-in to launch is given to the intent and so on
6. Asynchronous processing of some things to load/start series of plug-ins, etc
//3) Load the framework plug-in (such as loader/ Runtime) and service plug-in, and start the plug-in activity
startPluginActivity(installedPlugin, partKey, pluginIntent);
Copy the code
public void startPluginActivity(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
//1) Intent wrapper
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
/ / 2)
mPluginLoader.startActivityInPluginProcess(intent);
}
Copy the code
There are two things to do in this link:
1) The intent is wrapped twice
2) Start the activity of the plug-in
6.1 Wrapping intEnts twice
So let’s look at the first point, wrapper around intent 2
//1) Intent wrapper
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Copy the code
public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
//1) Load framework plug-ins (such as loader/ Runtime) and service plug-ins
loadPlugin(installedPlugin.UUID, partKey);
//2) Wrap the intent of the plugin
return mPluginLoader.convertActivityIntent(pluginIntent);
}
Copy the code
As you can see from the code, the framework plug-in is loaded before the intent is wrapped and then wrapped
Since an intent is intended to start a business activity with a business plug-in, the framework plug-in and the business plug-in must be prepared first. Then, information in the plug-in must be extracted and encapsulated in the intent to start the activity successfully
Let’s take a closer look at the implementation
A) Load framework plug-ins (such as Loader/Runtime) and service plug-ins
private void loadPlugin(String uuid, String partKey) {
//1) Load framework plug-ins: loader and Runtime
loadPluginLoaderAndRuntime(uuid, partKey);
//2) Load the service plug-in: load the plug-in through the frame loader;
// Example: partKey = sample-plugin-app
mPluginLoader.loadPlugin(partKey);
}
Copy the code
Load the frame plug-in Loader and Runtime first, and then use the frame loader to load the service plug-in
Now let’s look at how to load the framework plug-in
private void loadPluginLoaderAndRuntime(String uuid, String partKey) {
/*** * sample-runtime-release.apk * */
loadRunTime(uuid);
/** *sample-loader-release.apk * */
loadPluginLoader(uuid);
}
Copy the code
To load the framework plugin runtime, you need to load sample-Runtimerelease.apk from the plugin zip.
public final void loadRunTime(String uuid) {
InstalledApk installedApk;
// 1) Loader plug-in memory
installedApk = getRuntime(uuid);
InstalledApk installedRuntimeApk = new InstalledApk(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath);
//2) Load the frame plug-in loader according to the memorized object
boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
if(loaded) { DynamicRuntime.saveLastRuntimeInfo(mHostContext, installedRuntimeApk); }}Copy the code
This implementation is emasculated because the original version is called remotely through IPC BInder, which omits IPC calls to avoid long links
Here is mainly “loader plug-in memory” and “according to the memory object loading framework plug-in loader”
Loader plug-in memory, this section is not good to say, so omit
Load the framework plug-in loader based on the memorized object
public static boolean loadRuntime(InstalledApk installedRuntimeApk) {
// Host ClassLoader
ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();
HackParentToRuntime (installedRuntimeApk, contextClassLoader);
RuntimeClassLoader runtimeClassLoader = getRuntimeClassLoader();
if(runtimeClassLoader ! =null) {
String apkPath = runtimeClassLoader.apkPath;
if (TextUtils.equals(apkPath, installedRuntimeApk.apkFilePath)) {
// The runtime of the same version is already loaded, no need to load
Log.i(TAG, "DynamicRuntime has been loaded with the same apkPath runtime, do not need to load");
return false;
} else {
// If the version is different, restore the normal classLoader structure before updating the RuntimerecoveryClassLoader(); }}// Attach runtime to pathclassLoader
try {
Log.i(TAG, DynamicRuntime, normal processing, attach Runtime to pathclassLoader);
hackParentToRuntime(installedRuntimeApk, contextClassLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
return true;
}
Copy the code
/* * contextClassLoader is the loader of the host */
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);
}
Copy the code
static void hackParentClassLoader(ClassLoader classLoader, ClassLoader newParentClassLoader) throws Exception {
Field field = getParentField();
if (field == null) {
throw new RuntimeException("No parent domain of type ClassLoader found in classLoader. class");
}
field.setAccessible(true);
field.set(classLoader, newParentClassLoader);
}
Copy the code
This looks like a lot of code, but it’s actually pretty simple
Build a Classloader to load the plugin, using the “replace the parent PathClassloader” solution.
So we’re done loading the framework 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() +"File does not exist");
}
LoaderImplLoader implLoader = new LoaderImplLoader();
mPluginLoader = implLoader.load(installedApk, uuid, mHostContext);
}
Copy the code
public PluginLoaderImpl load(InstalledApk installedApk, String uuid, Context appContext) {
ApkClassLoader pluginLoaderClassLoader = new ApkClassLoader(
installedApk,
LoaderImplLoader.class.getClassLoader(),
loadWhiteList(installedApk),
1
);
// From apK, read the implementation of the interface
//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);
}
Copy the code
Loader is implemented in sample-loader-release.apk
The plug-in loading framework uses the loader
class ApkClassLoader extends DexClassLoader {
static final String TAG = "daviAndroid";
private ClassLoader mGrandParent;
private final String[] mInterfacePackageNames;
ApkClassLoader(InstalledApk installedApk,
ClassLoader parent,////parent = host ClassLoader
String[] mInterfacePackageNames,
int grandTimes) {
super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);
/ / by default
ClassLoader grand = parent;//parent = host ClassLoader
// How many generations are there
for (int i = 0; i < grandTimes; i++) {
grand = grand.getParent();
}
mGrandParent = grand;
this.mInterfacePackageNames = mInterfacePackageNames;
}
@Override
protectedClass<? > 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) {
// Case 1: The plugin can load the host class implementation:
return super.loadClass(className, resolve);
} else {
// Case 2: The plug-in does not need to load the host class implementationClass<? > clazz = findLoadedClass(className);//1) System search
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// If not, look in your dexPath first
clazz = findClass(className);//2) find in the dexPath of your own
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
// If no parent ClassLoader is found, check the parent ClassLoader.
//BootClassLoader
try {
clazz = mGrandParent.loadClass(className);/ / to find his father
} catch (ClassNotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
e.addSuppressed(suppressed);
}
throwe; }}}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 newException(e); }}}Copy the code
The principle of this loader is actually described in the past.
After loading the framework plug-in Runtime and loader, the framework plug-in Loader is used to load the business plug-in
//2) Load the service plug-in: load the plug-in through the frame loader;
// Example: partKey = sample-plugin-app
mPluginLoader.loadPlugin(partKey);
Copy the code
Some people may wonder why the business plug-in loader is used as a frame plug-in to load the business plug-in.
This is the dynamic design of shadow. All modules are dynamic (e.g. business plug-in download logic/business plug-in load etc.).
Because the explanation at the present stage is about the source code parsing of the sample-Manager module, that is, the sample-Manager. apk in the framework plug-in, how to load the service plug-in in the framework loader is not expanded here, and will be explained later when the module is parsed to loader.apk. Just be aware of it
B) Wrapped intents of plug-ins, etc
//2) Wrap the intent of the plugin
return mPluginLoader.convertActivityIntent(pluginIntent);
Copy the code
This implementation is also in the loader, in fact, the content of the business plug-in information wrapped in the intent
The specific implementation will not be expanded for the time being, and will be explained later in the loader.apk module
6.2 Starting the Activity of the Plug-in
/ / 2)
mPluginLoader.startActivityInPluginProcess(intent);
Copy the code
This implementation is also in loader. The specific implementation will not be expanded for the time being, and will be explained later in the loader.apk module
At the end
Haha, that’s all for this article (systematic learning and growing together)
Tips
More exciting content, please pay attention to “Android hot repair technology” wechat public number