A few months ago, Tencent opened source a new plug-in framework called Shadow. It’s important for the Android plug-in process, because most of the plug-in frameworks on the market will be obsolete as Google becomes more and more restrictive on the system API. Shadow approaches this problem from a new Angle.

The two most obvious advantages of Shadow over other plug-in frameworks on the market are:

  • Fully dynamic plug-in frameworks: It’s difficult to implement perfect plug-in frameworks all at once, but Shadow makes these implementations dynamic, making the plug-in framework code part of the plug-in. Iteration of a plug-in is no longer limited by the host packaging an older version of the plug-in framework.
  • Zero reflection without Hack implementation plug-in technology: theoretically it has been determined that there is no need to do compatible development for any system, nor any hidden API calls, and Google’s policy of restricting access to non-public SDK interfaces is completely not in conflict.

Frame full dynamic analysis

Before analyzing this, let’s take a look at the official explanation of the fully dynamic plug-in framework.

This is a problem we’ve been aware of since we’ve been around plug-in framework technology for a long time. This is a problem that we find all the known plug-in frameworks fail to solve. We call it the full dynamic plug-in framework. Fully dynamic means that in addition to the plug-in code, all the logic code of the plug-in framework itself is also dynamic. In fact, the code for the plug-in framework is packaged with the plug-in.

How important is this feature? It’s actually more important than a hack-free, zero-reflection implementation! Because with this feature, even if we use Hack solution, we need to be compatible with the system of various mobile phone manufacturers. We don’t have to wait for a host App update to fix the problem.

This feature is especially important for the new Shadow, because there are definitely a lot of imperfections. If it is packaged and distributed in a host, it will have to be tested very carefully and heavily designed to meet future plug-in requirements. However, Shadow does not need to do this now, and Shadow can only implement the functionality required by the business at the moment.

Full dynamic means that in addition to the plug-in code, all the logic code of the plug-in framework itself is also dynamic. Shadow divides the framework into four parts: Host, Manager, Loader, and Runtime. Except Host, Manager, Loader, and Runtime are all dynamic.

Host: The Host is packaged in the Host. It does two things: 1. Provides interfaces for Manger, Loader, and Runtime operations. 2. Load plug-ins such as Manager, Loader, and Runtime. It has two important classes DynamicPluginManager and PluginProcessService. The Enter () method of DynamicPluginManager is the entry point to the program and implements the logic for loading the Manager. PluginProcessService loads the Loader and Runtime.

Manager: Manages plug-ins, including the download logic, entry logic, and preloading logic of plug-ins. It’s all before the Loader. The open source part mainly includes the installation of plug-ins (APK storage specified path, odex optimization, decompression so library, etc.).

Loader: Loader is the core part of the framework. Mainly responsible for loading and inserting, managing the life cycle of the four components, Application life cycle and other functions. Many plug-in frameworks only have Loader functionality. In general, a Loader is a bridge between the host and the plug-in. For example, in the plug-in framework implemented by Hook system API, only the Loader code executed in the host can Hook some system classes, so that the plug-in can be successfully loaded. Or in the plug-in framework implemented by proxy, the plug-in must be loaded by Loader to complete the transfer of the four components.

Runtime: The Runtime section is mainly a few shellclasses registered in androidmanifest.xml. Shadow the author describes this part as forced dynamic. The reason is that the host is extremely strict about the deltas of the merged code, and the shell class introduces a lot of method deltas and is forced to make this part dynamic. This forced Shadow to introduce the only Hook API in the whole package. We’ll talk more about that later.

For more information about Manager, Loader, and Runtime, see Shadow’s full dynamic design principles.

Let’s take a look at how dynamic Manager, Loader, and Runtime is implemented. Go straight to the diagram above.

This diagram describes the main interaction logic of Host, Manger, Loader, Runtime, and Plugin. I’ve simplified the whole process.

  • The DynamicPluginManager class, located in the Host module, is the entry point to launch the plug-in. It is responsible for loading the Manager plug-in, instantiating the PluginManagerImpl, and passing the processing logic to the PluginManagerImpl.
  • Manager installs Loader, Runtime, and Plugin (apK stores the specified path, odex optimization, and decompresses the SO library).
  • The PluginProcessService class is also in the Host module. Shadow uses a cross-process design that separates the host and plug-in processes. The plug-in process is created by launching PluginProceeServicew. The interaction between PluginManagerImpl and PluginProceeServicew is a typical two-way communication between Binder agents, PpsController in the host process and UuidManager in the plug-in process. The host controls the PluginProceeService to load Runtime and Loader through PpsController, and the plug-in obtains the installation information of Runtime, Loader and plug-in through UuidManager. For more on Shadow multi-process design, see the cross-process design and plug-in Service principles for Shadow.
  • PluginProcessService loads the Loader and instantiates PluginLoaderBinder. PluginLoaderBinder is a Binder object, so the Manager can retrieve the PluginLoader proxy for PluginLoaderBinder via PpsController. After that, the Manager communicates with PluginLoader and PluginLoaderBinder to control the loading of the plug-in, the life cycle of the four components, and the life cycle of the Application.
  • PluginLoaderBinder gives the specific processing logic to the PluginLoaderImpl.

Dynamic implementation of Manager

When I introduced Host, I mentioned that the entry point to the entire framework is the Enter () method of DynamicPluginManager. Let’s analyze the dynamics of the Manager starting with Enter (). The main logic of Enter () is as follows:

  1. Load the Manager plug-in.
  2. Instantiate the PluginManagerImpl within the Manager by reflection.
  3. Delegate the processing logic to the PluginMangerImpl enter().

Let’s look at the processing logic for the first two steps

final class ManagerImplLoader extends ImplLoader {
    private static final String MANAGER_FACTORY_CLASS_NAME = "com.tencent.shadow.dynamic.impl.ManagerFactoryImpl";
    PluginManagerImpl load(a) {
        //1. Load Manager plug-in. This ClassLoader is amazing. It doesn't follow parental delegation exactly. Whitelisted classes can only be loaded through parent.
        ApkClassLoader apkClassLoader = new ApkClassLoader(
                installedApk, //Manager plugin APK.
                getClass().getClassLoader(),
                loadWhiteList(installedApk),//ClassLoader whitelist
                1
        );
        // Support Resource dependency and ClassLoader, allowing Manager plug-ins to use resources.
        Context pluginManagerContext = new ChangeApkContextWrapper(
                applicationContext,  //Applicaton
                installedApk.apkFilePath,
                apkClassLoader
        );
        try {
          	/ / 2. The reflection of Manager class com. Tencent. Shadow. Dynamic. The impl. ManagerFactoryImpl instance, call buildManager PluginManagerImpl is generated.
            ManagerFactory managerFactory = apkClassLoader.getInterface(
                    ManagerFactory.class,
                    MANAGER_FACTORY_CLASS_NAME
            );
            return managerFactory.buildManager(pluginManagerContext);
        } catch (Exception e) {
            throw newRuntimeException(e); }}}Copy the code

The PluginManagerImpl enter() method is then called. So DynamicPluginManager is just a proxy class, and the real processing class is the PluginManagerImpl in the Manager plug-in.

Dynamic implementation of Runtime

The Runtime loading process is done in the loadRuntime of PluginProcessService. Let’s take a look at the pseudocode.

public class PluginProcessService extends Service {
  // The proxy object that communicates with the host
  private UuidManager mUuidManager;
  void loadRuntime(String uuid) throws FailedException {
    	/ /... Omit code
        // Get Runtime installation information
        InstalledApk installedRuntimeApk = mUuidManager.getRuntime(uuid);
        / / load the Runtime.
        boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
    	/ /... Omit code}}Copy the code

LoadRuntime does two things:

  1. Obtain the Runtime installation information from the mUuidManager. As mentioned earlier, the Installation of the Runtime is done in the Manager, which runs in the host process and therefore requires Binder communication.
  2. Loading the Runtime call DynamicRuntime. LoadRuntime (). Let’s move on.
public class DynamicRuntime{

    public static boolean loadRuntime(InstalledApk installedRuntimeApk) {
        ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();
        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
                return false;
            } else {
                // If the version is different, restore the normal classLoader structure before updating the RuntimerecoveryClassLoader(); }}try {
             // As normal, attach the Runtime to the pathclassLoader
            hackParentToRuntime(installedRuntimeApk, contextClassLoader);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return true;
    }

    private static void hackParentToRuntime(InstalledApk installedRuntimeApk, ClassLoader contextClassLoader) throws Exception {
      	//RuntimeClassLoader loads the Runtime plug-in.
        RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(installedRuntimeApk.apkFilePath, installedRuntimeApk.oDexPath,
                installedRuntimeApk.libraryPath, contextClassLoader.getParent());
      	// This is the only place in the framework where the system API is reflected. But this is the API provided by the Java layer, which is relatively low-risk.
        Field field = getParentField();
        if (field == null) {
            throw new RuntimeException("No parent domain of type ClassLoader found in classLoader. class");
        }
        field.setAccessible(true); field.set(contextClassLoader, runtimeClassLoader); }}Copy the code

LoadRuntime uses the system’s proprietary API for reflection calls, the only place in the framework where the RuntimeClassLoader that loads the Runtime is mounted to the system’s PathClassLoader. That is, the RuntimeClassLoader is the parent of the PathClassLoader. Why do you do that? Because Runtime is mainly a shell class, such as shellActivity. When the system starts the Activity in the plug-in, it actually starts the shell Activity. This ensures that the system’s PathClassLoader must be able to find the shellActivity. A simple way to do this is to use the parent delegate model and set the parent loader of the PathClassLoader to RuntimeClassLoader.

Dynamic implementation of Loader

The loading of the Loader is done in PluginProcessService, just like the Runtime. The main process is as follows:

  1. Obtain Loader installation information.
  2. Load the Loader plug-in in LoaderImplLoader.
public class PluginProcessService extends Service {
    
    private PluginLoaderImpl mPluginLoader;
    private UuidManager mUuidManager;

    void loadPluginLoader(String uuid) throws FailedException {
        // Obtain Loader installation information
        InstalledApk installedApk = mUuidManager.getPluginLoader(uuid);
        Loaderimplloader.load () to load loader.
        PluginLoaderImpl pluginLoader = new LoaderImplLoader().load(installedApk, uuid, getApplicationContext());
        pluginLoader.setUuidManager(mUuidManager);
        mPluginLoader = pluginLoader;
    }

    IBinder getPluginLoader(a) {
        returnmPluginLoader; }}Copy the code
public class PpsController {
    public IBinder getPluginLoader(a) throws RemoteException {
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        IBinder _result;
        try {
            _data.writeInterfaceToken(PpsBinder.DESCRIPTOR);
            mRemote.transact(PpsBinder.TRANSACTION_getPluginLoader, _data, _reply, 0);
            _reply.readException();
            _result = _reply.readStrongBinder();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
        return_result; }}Copy the code

The logic in loaderimplLoader.load () is similar to the previous ManagerImplLoader logic, which I won’t analyze. The PluginLoaderImpl returned by loaderImplloader.load () is a Binder object. Our Manager can through PpsController. GetPluginLoader () get their agents.

Installing a plug-in

Installation package structure

Because of the dynamic nature of the Loader and Runtime, we also publish the Loader and Runtime when we publish the plug-in. Shadow does this by packing the plug-in, Loader, and Runtime into a ZIP file. The directory structure looks like this.

In addition to the above files, we found a config.json file that shadow automatically generated at compile time. It is a description of the entire installation package.

{"compact_version": [1, 2, 3], "pluginLoader": {//Loader description "apkName": "sample-loader-debug.apk", // Specify which APK is Loader. "Hash" : "FF05A9CC0A80D9D0950B0EA9CB431B35" / / file hash value, shadow according to the value and has been installed/load the Loader plugin contrast, whether to need to install/load. }, "plugins": [// plugins description {"apkName": "sample-plugin-app-debug.apk", "businessName": [// plugins description {"apkName": "sample-plugin-app-debug.apk", "businessName": [// plugins description {"apkName": "sample-plugin-app-debug.apk", "businessName": "The sample - the plugin - app", "hash" : "FD6FF462B95540C1184A64370FCA090E}]," "runtime" : {/ / the description of the runtime "apkName" : "sample-runtime-debug.apk", "hash": "AC2E39021DDFCBED5471B8739DDB3643" }, "UUID": "E822e493-2e29-41bb-b1e6-28d58bebb8ab ", "version": 4, "UUID_NickName": "1.1.5"}Copy the code

The installation process

I’ve broken down the installation process into the following steps:

  1. Decompress the installation package to a specified directory, obtain the installation package information together with config.json, and insert the information into the database for future use.
  2. Loader and Runtiem installation, mainly oDex optimization, update database oDex file path.
  3. Plug-in installation. Extract so library and oDex optimization, update database so file path and oDex file path.

The whole process is relatively simple, do not analyze the source code. ODex optimization is to hand over APK to DexClassLoader. So library extraction is the use of ZIP file stream, find the so file inside, copy to the corresponding directory.

Load the plug-in

When a host interacts with a plug-in, such as starting a plug-in Activity, it loads the plug-in first. The plug-in is loaded by sending the PluginLoader in the Manager a message to the PluginLoaderBinder in the Loader via Binder communication. PluginLoadersBinder receives the message and delegates it to PluginLoaderImpl for processing.

Plug-in loading is all about preparing the environment for the four components of a plug-in. The whole process is divided into:

  1. Use ClassLoader to load plug-in APK.
  2. Use PackageManager to get plug-in information.
  3. Create PluginPackageManager that stores plug-in information to obtain ActivityInfo and PackageInfo information.
  4. Create Resources that look for plug-in Resources.
  5. Create ShadowApplication for the current plug-in to prepare for the plug-in Application lifecycle.
  6. Cache the above content.

Let’s look directly at the implementation in Loader. The implementation logic is in LoadPluginBloc.

object LoadPluginBloc {
    fun loadPlugin(
            executorService: ExecutorService,
            abi: String,
            commonPluginPackageManager: CommonPluginPackageManager,
            componentManager: ComponentManager,
            lock: ReentrantLock,
            pluginPartsMap: MutableMap<String, PluginParts>,
            hostAppContext: Context,
            installedApk: InstalledApk,
            loadParameters: LoadParameters,
            remoteViewCreatorProvider: ShadowRemoteViewCreatorProvider?).: Future<*> {
        if (installedApk.apkFilePath == null) {
            throw LoadPluginException("apkFilePath==null")}else {
             // Use ClassLoader to load APk.
            val buildClassLoader = executorService.submit(Callable {
                lock.withLock {                   
                    LoadApkBloc.loadPlugin(installedApk, loadParameters, pluginPartsMap)
                }
            })

            // Use packageManager to get plug-in information.
            val getPackageInfo = executorService.submit(Callable {
                val archiveFilePath = installedApk.apkFilePath
                val packageManager = hostAppContext.packageManager               
                valpackageArchiveInfo = packageManager.getPackageArchiveInfo( archiveFilePath, PackageManager.GET_ACTIVITIES or PackageManager.GET_META_DATA or PackageManager.GET_SERVICES or PackageManager.GET_PROVIDERS or PackageManager.GET_SIGNATURES ) ? :throw NullPointerException("getPackageArchiveInfo return null.archiveFilePath==$archiveFilePath")
                packageArchiveInfo
            })

            // Create PluginPackageManager to store plug-in information.
            val buildPackageManager = executorService.submit(Callable {
                val packageInfo = getPackageInfo.get(a)val pluginInfo = ParsePluginApkBloc.parse(packageInfo, loadParameters, hostAppContext)
                PluginPackageManager(commonPluginPackageManager, pluginInfo)
            })
              
            // Create Resources that look for plug-in Resources.
            val buildResources = executorService.submit(Callable {
                val packageInfo = getPackageInfo.get()
                CreateResourceBloc.create(packageInfo, installedApk.apkFilePath, hostAppContext)
            })
              
            // Create the Application for the current plug-in.
            val buildApplication = executorService.submit(Callable {
                val pluginClassLoader = buildClassLoader.get(a)val pluginPackageManager = buildPackageManager.get(a)val resources = buildResources.get(a)val pluginInfo = pluginPackageManager.pluginInfo

                CreateApplicationBloc.createShadowApplication(
                        pluginClassLoader,
                        pluginInfo.applicationClassName,
                        pluginPackageManager,
                        resources,
                        hostAppContext,
                        componentManager,
                        remoteViewCreatorProvider
                )
            })
            // Save the above content in the cache.
            val buildRunningPlugin = executorService.submit {
                if (File(installedApk.apkFilePath).exists().not()) {
                    throw LoadPluginException("Plug-in file not found. PluginFile ==" + installedApk.apkFilePath)
                }
                val pluginPackageManager = buildPackageManager.get(a)val pluginClassLoader = buildClassLoader.get(a)val resources = buildResources.get(a)val pluginInfo = pluginPackageManager.pluginInfo
                val shadowApplication = buildApplication.get()
                lock.withLock {
                    componentManager.addPluginApkInfo(pluginInfo)
                    pluginPartsMap[pluginInfo.partKey] = PluginParts(
                            shadowApplication,
                            pluginClassLoader,
                            resources,
                            pluginInfo.businessName
                    )
                    PluginPartInfoManager.addPluginInfo(pluginClassLoader, PluginPartInfo(shadowApplication, resources,
                            pluginClassLoader, pluginPackageManager))
                }
            }
            return buildRunningPlugin
        }
    }
}
Copy the code

Plug-in ClassLoader implementation

A ClassLoader in a plug-in must be able to load classes from both the host and other plug-ins so that the plug-in can interact with the host or other plug-ins.

Loads the classes in the host

In Shadow, plug-ins can only load host classes configured in the whitelist. So let’s see how this works.

class PluginClassLoader(
        private valdexPath: String, optimizedDirectory: File? .private vallibrarySearchPath: String? , parent: ClassLoader,private valspecialClassLoader: ClassLoader? , hostWhiteList: Array<String>? ) : BaseDexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) { }Copy the code

We need to focus on three parameters: parent, specialClassLoader, and hostWhiteList. SpecialClassLoader is the parent of the host PathClassLoader when only the host and plug-in interactions are considered. Parent is the host PathClassLoader. HostWhiteList indicates that the plug-in can load a whitelist of host classes. The specific loading logic is as follows:

  1. In the whitelist, go straight to the parent delegate logic. Load directly to the host’s PathClassLoader.
  2. Not in the whitelist, try to load yourself first. The parent of the host PathClassLoader fails to load itself. This process skips the host PathClassLoader.
@Throws(ClassNotFoundException::class)
    override fun loadClass(className: String, resolve: Boolean): Class<*> {
        // Classes in the whitelist go directly to parent delegate
        if (specialClassLoader == null || className.startWith(allHostWhiteList))  {
            return super.loadClass(className, resolve)
        } else {
            var clazz: Class<*>? = findLoadedClass(className)
            if (clazz == null) {
                clazz = findClass(className)!!
                if (clazz == null) { clazz = specialClassLoader.loadClass(className)!! }}return clazz
        }
    }
Copy the code

Load classes in other plug-ins.

In introducing the installation process, I mentioned that shadow’s installation package includes a config.json file. The file’s description of the plug-in also has a dependsOn field for other plug-ins that it dependsOn. When shadow loads a plug-in, it determines whether the plug-in it depends on is already loaded. If the dependent plug-ins are all loaded, assemble the PathClassLoader that loads them into a CombineClassLoader. This CombineClassLoader is the parent loader of the current plug-in PathClassLoader. If a dependent plug-in is not loaded, an exception is thrown. Therefore, this requires us to be familiar with the call relationship between plug-ins, and before loading plug-ins, load their dependent plug-ins first.

    @Throws(LoadApkException::class)
    fun loadPlugin(installedApk: InstalledApk, loadParameters: LoadParameters, pluginPartsMap: MutableMap<String, PluginParts>): PluginClassLoader {
        val apk = File(installedApk.apkFilePath)
        val odexDir = if (installedApk.oDexPath == null) null else File(installedApk.oDexPath)
        val dependsOn = loadParameters.dependsOn
        // The Logger class must be packaged in the host, which is the classLoader that loads the host
        val hostClassLoader: ClassLoader = Logger::class.java.classLoader!!!!!
        val hostParentClassLoader = hostClassLoader.parent
        if (dependsOn == null || dependsOn.isEmpty()) {
            return PluginClassLoader(
                    apk.absolutePath,
                    odexDir,
                    installedApk.libraryPath,
                    hostClassLoader,
                    hostParentClassLoader,
                    loadParameters.hostWhiteList
            )
        } else if (dependsOn.size == 1) {
            val partKey = dependsOn[0]
            val pluginParts = pluginPartsMap[partKey]
            if (pluginParts == null) {
                throw LoadApkException("Load" + loadParameters.partKey + "When it depends on" + partKey + "Not loaded yet.")}else {
                return PluginClassLoader(
                        apk.absolutePath,
                        odexDir,
                        installedApk.libraryPath,
                        pluginParts.classLoader,
                        null,
                        loadParameters.hostWhiteList
                )
            }
        } else {
            val dependsOnClassLoaders = dependsOn.map {
                val pluginParts = pluginPartsMap[it]
                if (pluginParts == null) {
                    throw LoadApkException("Load" + loadParameters.partKey + "When it depends on" + it + "Not loaded yet.")}else {
                    pluginParts.classLoader
                }
            }.toTypedArray()
            val combineClassLoader = CombineClassLoader(dependsOnClassLoaders, hostParentClassLoader)
            return PluginClassLoader(
                    apk.absolutePath,
                    odexDir,
                    installedApk.libraryPath,
                    combineClassLoader,
                    null,
                    loadParameters.hostWhiteList
            )
        }
    }
Copy the code

PackageManager obtains plug-in information

val getPackageInfo = executorService.submit(Callable {
                val archiveFilePath = installedApk.apkFilePath
                val packageManager = hostAppContext.packageManager               
                valpackageArchiveInfo = packageManager.getPackageArchiveInfo( archiveFilePath, PackageManager.GET_ACTIVITIES or PackageManager.GET_META_DATA or PackageManager.GET_SERVICES or PackageManager.GET_PROVIDERS or PackageManager.GET_SIGNATURES ) ? :throw NullPointerException("getPackageArchiveInfo return null.archiveFilePath==$archiveFilePath")
                packageArchiveInfo
            })
Copy the code

Here is called the PackageManager. GetPackageArchiveInfo (), to save the plugin APK path in the past.

Generate PluginPackageManager

 val buildPackageManager = executorService.submit(Callable {
     // Get plug-in information
     val packageInfo = getPackageInfo.get(a)// Parse to our own PluginInfo.
     val pluginInfo = ParsePluginApkBloc.parse(packageInfo, loadParameters, hostAppContext)
     / / build PluginPackManager
     PluginPackageManager(commonPluginPackageManager, pluginInfo)
 })
Copy the code

The whole process is simple, look directly at PluginPackageManager.

class PluginPackageManager(val commonPluginPackageManager: CommonPluginPackageManager,
                           val pluginInfo: PluginInfo) : PackageManager() {
    override fun getApplicationInfo(packageName: String? , flags:Int): ApplicationInfo {
        val applicationInfo = ApplicationInfo()
        applicationInfo.metaData = pluginInfo.metaData
        applicationInfo.className = pluginInfo.applicationClassName
        return applicationInfo
    }

    override fun getPackageInfo(packageName: String? , flags:Int): PackageInfo? {
        if (pluginInfo.packageName == packageName) {
            valinfo = PackageInfo() info.versionCode = pluginInfo.versionCode info.versionName = pluginInfo.versionName info.signatures  = pluginInfo.signaturesreturn info;
        }
        return null;
    }

    override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo {
        val find = pluginInfo.mActivities.find {
            it.className == component.className
        }
        if (find == null) {
            return commonPluginPackageManager.getActivityInfo(component, flags)
        } else {
            return find.activityInfo
        }
    }
  
  	override fun getProviderInfo(component: ComponentName? , flags:Int): ProviderInfo {
        ImplementLater()
    }

    override fun getReceiverInfo(component: ComponentName? , flags:Int): ActivityInfo {
        ImplementLater()
    }
}
Copy the code

PluginPackageManager inherits from PackageManager and returns information about the current plug-in. For this part, see the author’s blog Shadow’s approach to PackageManager.

Create Resources that look for plug-in Resources

object CreateResourceBloc {
    fun create(packageArchiveInfo: PackageInfo, archiveFilePath: String, hostAppContext: Context): Resources {
        val packageManager = hostAppContext.packageManager
        packageArchiveInfo.applicationInfo.publicSourceDir = archiveFilePath
        packageArchiveInfo.applicationInfo.sourceDir = archiveFilePath
        try {
            return packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo)
        } catch (e: PackageManager.NameNotFoundException) {
            throw RuntimeException(e)
        }
    }
}
Copy the code

To the path of the plugin apk assignment to publicSourceDir and sourceDir, using the PackageManager. GetResourcesForApplication () to create a new Resources.

ShadowApplication

Bytecode substitution

Shadow uses bytecode technology when processing plug-in Application, and replaces the inherited system Application with ShadowApplication at compile time. ShadowApplication is a normal class that inherits Context.

class ApplicationTransform : SimpleRenameTransform(
        mapOf(
                "android.app.Application"
                        to "com.tencent.shadow.core.runtime.ShadowApplication"
                ,
                "android.app.Application\$ActivityLifecycleCallbacks"
                        to "com.tencent.shadow.core.runtime.ShadowActivityLifecycleCallbacks"
        )
)
Copy the code

Create ShadowApplication

object CreateApplicationBloc {
    @Throws(CreateApplicationException::class)
    fun createShadowApplication(
            pluginClassLoader: PluginClassLoader,
            appClassName: String? , pluginPackageManager:PluginPackageManager,
            resources: Resources,
            hostAppContext: Context,
            componentManager: ComponentManager,
            remoteViewCreatorProvider: ShadowRemoteViewCreatorProvider?).: ShadowApplication {
        try {
            val shadowApplication : ShadowApplication;
            shadowApplication = if(appClassName ! =null) {// If appClassName exists, the plugin declares Application, and reflection generates ShadowApplication.
                val appClass = pluginClassLoader.loadClass(appClassName)
                ShadowApplication::class.java.cast(appClass.newInstance())
            } else {
                object : ShadowApplication(){}// The plugin does not declare Application, just new a ShadowApplication.
            }
            val partKey = pluginPackageManager.pluginInfo.partKey
            shadowApplication.setPluginResources(resources) / / add the Resources
            shadowApplication.setPluginClassLoader(pluginClassLoader) / / add this
            shadowApplication.setPluginComponentLauncher(componentManager) // Add ComponentManager to manage four components.
            shadowApplication.setHostApplicationContextAsBase(hostAppContext) // Add the host Application.
            shadowApplication.setBroadcasts(componentManager.getBroadcastsByPartKey(partKey)) // Add BroadcastReceiver.
            shadowApplication.setLibrarySearchPath(pluginClassLoader.getLibrarySearchPath())
            shadowApplication.setDexPath(pluginClassLoader.getDexPath())// Plugin APK path.
            shadowApplication.setBusinessName(pluginPackageManager.pluginInfo.businessName)
            shadowApplication.setPluginPartKey(partKey)
            shadowApplication.remoteViewCreatorProvider = remoteViewCreatorProvider
            return shadowApplication
        } catch (e: Exception) {
            throw CreateApplicationException(e)
        }
    }
}
Copy the code

So let’s just look at the comments.