Framework Introduction

Shadow is a plug-in framework recently opened by Tencent. The idea is to implement the lifecycle of a component using a host agent. At present, most of the plug-in framework is done by hook system. There is basically no systematic framework for the use of proxy, only some small demo, Shadow framework of open source, in the trend of more and more strict system API control, it is a new direction. The two biggest highlights of Shadow are:

  1. Zero reflection
  2. The framework itself is dynamic

The following is a concrete analysis of the implementation of the framework.

Frame structure analysis

Frame structure drawing

Project directory structure

├ ─ ─ the projects │ ├ ─ ─ the sample / / sample code │ │ ├ ─ ─ the README. Md │ │ ├ ─ ─ maven │ │ ├ ─ ─ the sample - constant / / define some constants │ │ ├ ─ ─ the sample - the host / / host implementation │ │ ├ ─ ─ the sample - the manager / / PluginManager │ │ └ ─ ─ the sample - the plugin / / plug-in implementation │ ├ ─ ─ the SDK / / framework implementation code │ │ ├ ─ ─ coding / / Lint │ │ ├─ Core │ │ ├─ common │ │ ├─ Gradle Plugin │ │ ├─ Load - Parameters │ │ ├─ │ │ ├─ Heavy Metal Guitar School - │ │ ├─ Heavy metal Guitar School - Heavy metal Guitar School │ │ ├─ Transform // Used to replace the plug-in Activity and so on the parent class │ │ │ └ ─ ─ transform - kit │ │ └ ─ ─ the dynamic / / plug-in itself dynamic implementation, including some of the interface of the abstractCopy the code

Description of the framework’s main classes

PluginContainerActivity proxy Activity ShadowActivity plugin Activity unified parent class, During packaging, the corresponding relationship between ComponentManager and host agent is replaced by Transform

Sample Code androidmanifest.xml analysis

Registered sample MainActivity

Responsible for launching plug-ins

Register a proxy Activity

To register the three representative Activity, respectively is PluginDefaultProxyActivity, PluginSingleInstance1ProxyActivity, PluginSingleTask1ProxyActivity. As you can see, these three activities are all derived from pluginContainerActivities, but with different launchMode Settings.

Registering an agent Provider

PluginContainerContentProvider is also agent of the Provider.

The Activity implementation

For the implementation of the plug-in Activity, we mainly look at two places: 0. Replace the parent class of the plug-in Activity

  1. How do I start the plug-in Activity in the host
  2. How do I start a plug-in Activity in a plug-in

Replace the parent class of the plug-in Activity

There is a clever point in Shadow. During the development of the plug-in, the Activity of the plug-in still inherits from the Activity. During the packaging, it will replace its parent class with ShadowActivity by Transform. If you’re not familiar with Transform, check out the previous Gradle learning series. The projects/SDK/core/transform and the projects/SDK/core/kit two projects is the transform, transform – entry is ShadowTransform. There is a wrapper around Transform that provides a friendly way to develop it, but instead of analyzing it, we’ll focus on TransformManager.

class TransformManager(ctClassInputMap: Map<CtClass, InputClass>,
                       classPool: ClassPool,
                       useHostContext: () -> Array<String>
) : AbstractTransformManager(ctClassInputMap, classPool) {

    override val mTransformList: List<SpecificTransform> = listOf(
            ApplicationTransform(),
            ActivityTransform(),
            ServiceTransform(),
            InstrumentationTransform(),
            RemoteViewTransform(),
            FragmentTransform(ctClassInputMap),
            DialogTransform(),
            WebViewTransform(),
            ContentProviderTransform(),
            PackageManagerTransform(),
            KeepHostContextTransform(useHostContext())
    )
}
Copy the code

The mTransformList here is the Transform content that is executed in turn, which is the class map that needs to be replaced. Let’s take ApplicationTransform and ActivityTransform as examples.

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"
        )
)

class ActivityTransform : SimpleRenameTransform(
        mapOf(
                "android.app.Activity"
                        to "com.tencent.shadow.core.runtime.ShadowActivity"
        )
)
Copy the code

As you can see, during the packaging process, the Application of the plug-in will be replaced by ShadowApplication, and the Activity will be replaced by ShadowActivity. Here we mainly look at the inheritance relationship of ShadowActivity.

Why can plug-in Activities not inherit from Activities? Because the plug-in Activity is used as a normal class in the surrogate Activity way, it is responsible for executing the corresponding life cycle.

How do I start the plug-in Activity in the host

The principle of starting the plug-in Activity in the host is shown as follows:

Let’s start with MainActivity in sample. Sample – host/SRC/main/Java/com/tencent/shadow/sample/host/MainActivity is the main entry demo. The plug-in can be launched by:

startPluginButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // ...
        Intent intent = new Intent(MainActivity.this, PluginLoadActivity.class);
        intent.putExtra(Constant.KEY_PLUGIN_PART_KEY, partKey);
        intent.putExtra(Constant.KEY_ACTIVITY_CLASSNAME, "com.tencent.shadow.sample.plugin.app.lib.gallery.splash.SplashActivity");
        // ...startActivity(intent); }});Copy the code

As you can see, the PluginLoadActivity is started using PluginLoadActivity, which is called SplashActivity, and then the PluginLoadActivity is started using the PluginLoadActivity.

class PluginLoadActivity extends Activity {
    public void startPlugin(a) {
        PluginHelper.getInstance().singlePool.execute(new Runnable() {
            @Override
            public void run(a) {
                HostApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);
                // ...
                bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, getIntent().getStringExtra(Constant.KEY_ACTIVITY_CLASSNAME));
                HostApplication.getApp().getPluginManager()
                        .enter(PluginLoadActivity.this, Constant.FROM_ID_START_ACTIVITY, bundle, new EnterCallback() {
                            @Override
                            public void onShowLoadingView(final View view) {
                                // Set the loading style
                                mHandler.post(new Runnable() {
                                    @Override
                                    public void run(a) { mViewGroup.addView(view); }}); }// ...}); }}); }}Copy the code

As you can see, you get the PluginManager via HostApplication and then call its Enter method to enter the plug-in. Let’s take a look at what the PluginManager returned is.

class HostApplication extends Application {
    public void loadPluginManager(File apk) {
        if (mPluginManager == null) {
            / / create the PluginManagermPluginManager = Shadow.getPluginManager(apk); }}public PluginManager getPluginManager(a) {
        returnmPluginManager; }}public class Shadow {
    public static PluginManager getPluginManager(File apk){
        final FixedPathPmUpdater fixedPathPmUpdater = new FixedPathPmUpdater(apk);
        File tempPm = fixedPathPmUpdater.getLatest();
        if(tempPm ! =null) {
            / / create DynamicPluginManager
            return new DynamicPluginManager(fixedPathPmUpdater);
        }
        return null; }}Copy the code

As you can see, the HostApplication returns a DynamicPluginManager instance, so the DynamicPluginManager Enter method is the next step.

class DynamicPluginManager implements PluginManager {
    @Override
    public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {
        // Load the mManagerImpl implementation, which involves the framework's own dynamics, as discussed later, as long as you know that mManagerImpl is ultimately an instance of SamplePluginManager
        updateManagerImpl(context);
        // mManagerImpl is an instance of SamplePluginManager whose implementation is invokedmManagerImpl.enter(context, fromId, bundle, callback); mUpdater.update(); }}Copy the code

Through the above code we know, call DynamicPluginManager. Enter will be forwarded to the SamplePluginManager. To enter, then take a look at this.

class SamplePluginManager extends FastPluginManager {
    public void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {
        // ...
        / / start the Activity
        onStartActivity(context, bundle, callback);
        // ...
    }

    private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
        // ...
        final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
        // ...
        final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
        if(callback ! =null) {
            // Create loading View
            final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
            callback.onShowLoadingView(view);
        }
        executorService.execute(new Runnable() {
            @Override
            public void run(a) {
                // ...
                // Load the plug-in
                InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null.true);
                // Create the Intent
                Intent pluginIntent = new Intent();
                pluginIntent.setClassName(
                        context.getPackageName(),
                        className
                );
                if(extras ! =null) {
                    pluginIntent.replaceExtras(extras);
                }
                // Start the plug-in Activity
                startPluginActivity(context, installedPlugin, partKey, pluginIntent);
                // ...}}); }}Copy the code

In SamplePluginManager. Enter the call onStartActivity startup plug-ins Activity, including thread to load the plug-ins, and then call startPluginActivity. StartPluginActivity is implemented in its parent FastPluginManager class.

class FastPluginManager {
    public void startPluginActivity(Context context, InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) throws RemoteException, TimeoutException, FailedException {
        Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
        if(! (contextinstanceofActivity)) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); }}Copy the code

ConvertActivityIntent converts the plugin intent into the host intent, and then calls the context.startActivity to start the plugin. The context here is pluginloadActivity. this, passed directly in from its Enter method. ConvertActivityIntent is implemented with convertActivityIntent.

class FastPluginManager {
    public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) throws RemoteException, TimeoutException, FailedException {
        / / create mPluginLoader
        loadPlugin(installedPlugin.UUID, partKey);
        // Call the Application onCreate method
        mPluginLoader.callApplicationOnCreate(partKey);
        // Convert the intent to a proxy Activity intent
        returnmPluginLoader.convertActivityIntent(pluginIntent); }}Copy the code

This is a bit more complicated because mPluginLoader calls methods with Binder. As the use of Binder is involved here, readers are required to know Binder related knowledge, and the code is rather complicated, so the specific analysis of code implementation will not be made here, but the corresponding relationship will be straightened out with a picture:

According to the Binder diagram above, we can simply understand that calling methods in mPluginLoader is calling methods in DynamicPluginLoader and calling methods of mPpsController. Call the method in PluginProcessService. So here mPluginLoader. ConvertActivityIntent equivalent to invoke the DynamicPluginLoader. ConvertActivityIntent.

internal class DynamicPluginLoader(hostContext: Context, uuid: String) {
    fun convertActivityIntent(pluginActivityIntent: Intent): Intent? {
        return mPluginLoader.mComponentManager.convertPluginActivityIntent(pluginActivityIntent)
    }
}
Copy the code

Calls to the ComponentManager. ConvertPluginActivityIntent method.

abstract class ComponentManager : PluginComponentLauncher {
    override fun convertPluginActivityIntent(pluginIntent: Intent): Intent {
        return if (pluginIntent.isPluginComponent()) {
            pluginIntent.toActivityContainerIntent()
        } else {
            pluginIntent
        }
    }

    private fun Intent.toActivityContainerIntent(a): Intent {
        // ...
        return toContainerIntent(bundleForPluginLoader)
    }

    private fun Intent.toContainerIntent(bundleForPluginLoader: Bundle): Intent {
        val className = component.className!!
        val packageName = packageNameMap[className]!!
        component = ComponentName(packageName, className)
        val containerComponent = componentMap[component]!!
        valbusinessName = pluginInfoMap[component]!! .businessNamevalpartKey = pluginInfoMap[component]!! .partKeyval pluginExtras: Bundle? = extras
        replaceExtras(null as Bundle?)

        val containerIntent = Intent(this)
        containerIntent.component = containerComponent

        bundleForPluginLoader.putString(CM_CLASS_NAME_KEY, className)
        bundleForPluginLoader.putString(CM_PACKAGE_NAME_KEY, packageName)

        containerIntent.putExtra(CM_EXTRAS_BUNDLE_KEY, pluginExtras)
        containerIntent.putExtra(CM_BUSINESS_NAME_KEY, businessName)
        containerIntent.putExtra(CM_PART_KEY, partKey)
        containerIntent.putExtra(CM_LOADER_BUNDLE_KEY, bundleForPluginLoader)
        containerIntent.putExtra(LOADER_VERSION_KEY, BuildConfig.VERSION_NAME)
        containerIntent.putExtra(PROCESS_ID_KEY, DelegateProviderHolder.sCustomPid)
        return containerIntent
    }
}
Copy the code

So we finally call the toContainerIntent method, and we’re done. In the toContainerIntent, we create an intent that hosts the new proxy Activity, ContainerComponent corresponding here is the front PluginDefaultProxyActivity registered in the Manifest, return agent activity after the intent, Calling context.startActivity(intent) launches the proxy Activity. PluginDefaultProxyActivity inherited from PluginContainerActivity, this also is the agent of the Activity of the framework, in PluginContainerActivity is the regular distribution of the life cycle. This is pretty much what we introduced in plugins. The lifecycle is distributed through a HostActivityDelegate.

class ShadowActivityDelegate(private val mDI: DI) : HostActivityDelegate, ShadowDelegate() {
    // ...
    override fun onCreate(savedInstanceState: Bundle?). {
        // ...
        // Set application, Resources, etc
        mDI.inject(this, partKey)
        // Create a plug-in resource
        mMixResources = MixResources(mHostActivityDelegator.superGetResources(), mPluginResources)
        // Set the plugin theme
        mHostActivityDelegator.setTheme(pluginActivityInfo.themeResource)
        try {
            val aClass = mPluginClassLoader.loadClass(pluginActivityClassName)
            // Create a plug-in activity
            val pluginActivity = PluginActivity::class.java.cast(aClass.newInstance())
            // Initialize the plug-in activity
            initPluginActivity(pluginActivity)
            mPluginActivity = pluginActivity
            // Set the WindowSoftInputMode registered in the plugin Androidmanifest.xml
            mHostActivityDelegator.window.setSoftInputMode(pluginActivityInfo.activityInfo.softInputMode)
            / / get savedInstanceState
            valpluginSavedInstanceState: Bundle? = savedInstanceState? .getBundle(PLUGIN_OUT_STATE_KEY) pluginSavedInstanceState? .classLoader = mPluginClassLoader// Invoke the plug-in activity onCreate
            pluginActivity.onCreate(pluginSavedInstanceState)
            mPluginActivityCreated = true
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }

    // Get the plugin resource
    override fun getResources(a): Resources {
        if (mDependenciesInjected) {
            return mMixResources;
        } else {
            return Resources.getSystem()
        }
    }
}
Copy the code

This is the whole process of starting the plug-in Activity in the host. Let’s look at how to start the Activity in the plug-in.

How do I start a plug-in Activity in a plug-in

The principle of starting an Activity in the plug-in is shown as follows:

As we mentioned above, the plug-in Activity replaces its parent class with ShadowActivity during the packaging process. Obviously, starting the Activity in the plug-in calls startActivity, StartActivity is the startActivity of ShadowActivity. StartActivity is implemented in its parent ShadowContext, so let’s look at it in detail.

class ShadowContext extends SubDirContextThemeWrapper {
    public void startActivity(Intent intent) {
        final Intent pluginIntent = new Intent(intent);
        // ...
        final boolean success = mPluginComponentLauncher.startActivity(this, pluginIntent);
        // ...}}Copy the code

As you can see, is through mPluginComponentLauncher. Continue to call startActivity, mPluginComponentLauncher is ComponentManager an instance, This is set during the initialization of the plug-in Activity mentioned earlier. The internal implementation is simpler.

abstract class ComponentManager : PluginComponentLauncher {
    override fun startActivity(shadowContext: ShadowContext, pluginIntent: Intent): Boolean {
        return if (pluginIntent.isPluginComponent()) {
            shadowContext.superStartActivity(pluginIntent.toActivityContainerIntent())
            true
        } else {
            false}}}public class ShadowContext extends SubDirContextThemeWrapper {
    public void superStartActivity(Intent intent) {
        // Call the system startActivity
        super.startActivity(intent); }}Copy the code

By calling the toActivityContainerIntent transformation intent intent for agency Activity, then call startActivity system startup agency Activity, The remaining steps are the same as described above in the host to start the Activity plug-in.

At this point, we have a basic understanding of how an Activity starts in the framework.

The Service implementation

Service implementation, we can directly look at the plug-in how to start. Take a look at the startService implementation in ShadowContext:

public class ShadowContext extends SubDirContextThemeWrapper {
    public ComponentName startService(Intent service) {
        if (service.getComponent() == null) {
            return super.startService(service);
        }
        Pair<Boolean, ComponentName> ret = mPluginComponentLauncher.startService(this, service);
        if(! ret.first)return super.startService(service);
        returnret.second; }}Copy the code

Also called mPluginComponentLauncher startService, here we are familiar with, is ComponentManager. StartService

abstract class ComponentManager : PluginComponentLauncher {
    override fun startService(context: ShadowContext, service: Intent): Pair<Boolean, ComponentName> {
        if (service.isPluginComponent()) {
            // Service intents do not need to be converted into Container service intents
            valcomponent = mPluginServiceManager!! .startPluginService(service)// ...
        }
        return Pair(false, service.component)
    }
}
Copy the code

Direct call here PluginServiceManager startPluginService.

class PluginServiceManager(private val mPluginLoader: ShadowPluginLoader, private val mHostContext: Context) {
    fun startPluginService(intent: Intent): ComponentName? {
        val componentName = intent.component
        // Check whether the requested service already exists
        if(! mAliveServicesMap.containsKey(componentName)) {// Create the Service instance and call the onCreate method
            val service = createServiceAndCallOnCreate(intent)
            mAliveServicesMap[componentName] = service
            // Start the collection with startServicemServiceStartByStartServiceSet.add(componentName) } mAliveServicesMap[componentName]? .onStartCommand(intent,0, getNewStartId())
        return componentName
    }
    
    private fun createServiceAndCallOnCreate(intent: Intent): ShadowService {
        val service = newServiceInstance(intent.component)
        service.onCreate()
        return service
    }
}
Copy the code

As you can see, the handling of a Service in Shadow is very simple, calling its lifecycle methods directly, but this implementation may introduce some timing issues.

BroadcastReceiver implementation

The implementation of broadcasting is also more conventional, registering and sending broadcasts dynamically in plug-ins, directly calling system methods, because broadcasting does not involve complex content such as life cycle. What you need to deal with is the broadcasts that are statically registered in the Manifest. This theory is basically the same as when we explained the principle of plug-in before, parsing the Manifest and then dynamically registering. However, in the Shadow demo, there is no parsing, is directly written in the code.

// AndroidManifest.xml
        <receiver android:name="com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.MyReceiver">
            <intent-filter>
                <action android:name="com.tencent.test.action" />
            </intent-filter>
        </receiver>

// SampleComponentManager
public class SampleComponentManager extends ComponentManager {
    public List<BroadcastInfo> getBroadcastInfoList(String partKey) {
        List<ComponentManager.BroadcastInfo> broadcastInfos = new ArrayList<>();
        if (partKey.equals(Constant.PART_KEY_PLUGIN_MAIN_APP)) {
            broadcastInfos.add(
                    new ComponentManager.BroadcastInfo(
                            "com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.MyReceiver",
                            new String[]{"com.tencent.test.action"}
                    )
            );
        }
        return broadcastInfos;
    }
}
Copy the code

ContentProvider implementation

About the implementation of ContentProvider, in fact, and the principle of plug-in before the idea is the same, is also through the registration of proxy ContentProvider and then distributed to the plug-in Provider, here will not do more introduction.

The framework itself is dynamic

Another feature of the Shadow framework is that the framework itself is dynamic. There are three main steps:

  1. Abstract interface class
  2. Implement the factory class in the plug-in
  3. Dynamically create an implementation of the interface through a factory class

Take PluginLoaderImpl as an example. Earlier in the Activity startup process, Have said mPluginLoader convertActivityIntent agent used to convert the plugin intent to the intent of the Activity, mPluginLoader is dynamically created here. Let’s look at the creation process. Create a gateway PluginProcessService. LoadPluginLoader

public class PluginProcessService extends Service {
    void loadPluginLoader(String uuid) throws FailedException {
        // ...
        PluginLoaderImpl pluginLoader = new LoaderImplLoader().load(installedApk, uuid, getApplicationContext());
        // ...}}Copy the code

Next we need to look at the concrete implementation of LoaderImplLoader

final class LoaderImplLoader extends ImplLoader {
    // Create the PluginLoaderImpl factory class
    private final static String sLoaderFactoryImplClassName
            = "com.tencent.shadow.dynamic.loader.impl.LoaderFactoryImpl";

    // Dynamically create PluginLoaderImpl
    PluginLoaderImpl load(InstalledApk installedApk, String uuid, Context appContext) throws Exception {
        // Create a plug-in ClassLoader
        ApkClassLoader pluginLoaderClassLoader = new ApkClassLoader(
                installedApk,
                LoaderImplLoader.class.getClassLoader(),
                loadWhiteList(installedApk),
                1
        );
        // Get the factory class in the plug-in
        LoaderFactory loaderFactory = pluginLoaderClassLoader.getInterface(
                LoaderFactory.class,
                sLoaderFactoryImplClassName
        );
        // Call the factory class method to create a PluginLoaderImpl instance
        returnloaderFactory.buildLoader(uuid, appContext); }}Copy the code

From the code and comments above, it is quite simple to create a ClassLoader for the plug-in, create an instance of the factory class from the ClassLoader, and then call the factory class method to generate PluginLoaderImpl. The factory class and PluginLoaderImpl implementations are both in the plug-in, making the framework itself dynamic. PluginManagerImpl is the same reason, in DynamicPluginManager updateManagerImpl by ManagerImplLoader. The load is loaded.

conclusion

In fact, the whole framework, there is no black technology, is the principle of proxy Activity plus the use of design mode. In fact, at present, several major plug-in frameworks are mainly hook systems. For example, using the principle of proxy Activity, Shadow should be the first framework with relatively complete implementation in all aspects. The advantage is that there is no need to call system limited API, which is more stable. Under the trend of more and more strict system control, it is also a better choice. The principle is simple, and the design ideas can be learned

About me