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:
- Zero reflection
- 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
- How do I start the plug-in Activity in the host
- 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:
- Abstract interface class
- Implement the factory class in the plug-in
- 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