Why plug-in learning
In the process of project iteration, the business becomes more and more complex. Under the single-project development model, the coupling degree of business modules is extremely high, and the module boundary is fuzzy. Any modification to the project must be compiled into the whole project, and the team collaboration exposes many conflicts and inconvenience, so it has to develop to multi-project development model. Represents componentization and plug-in.
The core point of the multi-engineering development model is that it can flexibly assemble each module into a small project that can be compiled and run independently, and the final APP is made by the aggregation of these small projects. In this way, team members can focus on their own small projects without interfering with each other and with clear boundaries.
How to choose between componentization and plug-in
From the perspective of daily development, there is no difference between the two. For example, both business components can be hot-pluggable, and the problems encountered are the same, such as the need to solve layered problems, jump problems, cross-module communication problems and so on. However, from the point of view of empowerment and technical implementation, componentization can be regarded as a subset of plug-in. Pluginization contains all the advantages of componentization and makes it dynamic.
The core problems solved by plug-in are as follows:
- Dynamic release
- You can see dex subcontracting and resource subcontracting schemes
Therefore, if you’re dealing with a business that doesn’t have the need for rapid iteration and frequent releases, the power of plug-ins is largely reduced. Also, compatibility and stability issues are often associated with being dynamic, so it is important to measure the effort and output of the team to deal with these issues.
Purpose of the article
Although there are a variety of plug-in framework, and learn to know the principle of plug-in, but the paper come zhongjue shallow, must know this to practice. Understand the principle of plug-in by manually implementing it once. And in the daily learning of various Framework layer knowledge, can also be verified in this process.
Note: Article source code based on 8.0
How to implement plug-in
Plug-in dynamics actually solves three problems:
- Class loading
- Resource to load
- Management of the four components
Class loading
When using a class in a plug-in, you need to load the class before using it. Therefore, you need to know how class loading works on Android.
photo
The principle of class loading can be referred to: the fear of class loader
In simple terms, a class loader is used to load classes through the parental delegation model, with the following features:
- Each instantiation of a class loader requires passing in another class loader as the parent
- Whether a class is loaded is determined by ClassLoader + PackageName + ClassName
- When a class is loaded, the parent loader will load it first. If the parent loader does not load it, the parent loader will load it by itself. In this way, the upper class such as the Framework layer can be used directly, avoiding repeated loading, and can also isolate the core class library from being modified
- The PathClassLoader is responsible for loading the system classes and classes in the main dex
- DexClassLoader loads Classes from jar packages containing classes.dex or APK
Resource to load
The use of classes often involves the use of resources, images, layout files, and so on. The resources in the plug-in are not loaded, and when accessed, it will crash, so you need to load the resources of the plug-in.
For details about the resource loading principle, see: Analysis of the Android resource loading mechanism
The principle of resource loading can be described as follows:
- When APK is packaged, all resources are structured into ARSC files through AAPT. Arsc files contain all resource ids, resource types, file path information and all string information
- Through the AssetManager. AddAssetPath () to APK path, eventually trigger Nativie layer AssetMananger. CPP. AppendPathToResTable create ResTable ()
- When the Java layer accesses resources, the resource description can be obtained through the resource ID and ReTable, TypeValue, from which the key information of resources can be obtained and accessed
Management of the four components
For the four components, loading the corresponding classes does not reach the available state. After that, we use Activity to illustrate.
It is not possible to create an Activity in a new way. An Activity needs to be run in a context and registered in the AndroidManifest file. When APK is installed, PMS collects all Activity information from the AndroidManifest (and other information, of course, omitted here). When an Activity starts, AMS obtains Activity information, such as startup mode and process, through PMS, and then starts it.
In the plug-in scenario, three problems need to be solved when using an Activity: 1. Context is required for the start of the Activity; 2
What happens when the Activity starts
Manual implementation
Knowing the problems that need to be solved by plug-in, and also having a rough understanding of the principles involved in the corresponding problems, you can start to implement plug-ins simply by hand.
Step one, merge the DEX
On the basis of the principles of class loading, the Android this instantiation receives the APK file path, to resolve the Dex, coexist in BaseDexClassLoader dexPathList. DexElements, so need to get information when class loading. Source visible DexClassLoader () – > BaseDexClassLoader () – > DexPathList () – > DexPathList. MakeDexElements ()
private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader) { ...... Dex DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);if(dex ! DexElements elements[elementsPos++] = new Element(dex, null); }... }Copy the code
The idea is clear. By creating a new class loader, parsing out Element for the plug-in APK and inserting it into the host Element, you can provide the plug-in’s class information, as shown in the figure
The code implementation is as follows
public static void loadPluginDex(Application context, Throws Exception{// Get plug-in APK String apkPath = getPatchApkPath(context); File apkFile = new File(apkPath); DexClassLoader = new DexClassLoader(apkfile.getabsolutePath (), null, null,classLoader); / / get BaseDexClassLoader dexPathList Object pluginDexPatchList = ReflectUtil. GetField (dexClassLoader,"pathList"); // Get dexfile. dexElements Object pluginDexElements = reflectutil. getField(pluginDexPatchList,"dexElements"); HostDexPatchList = reflectutil. getField(classLoader,"pathList"); HostDexElements Object hostDexElements = reflectutil. getField(hostDexPatchList,"dexElements"); // Merge dexElements Object array = combineArray(hostDexElements, pluginDexElements); ReflectUtil.setField( hostDexPatchList,"dexElements", array); // Load mergePluginResources(context); }Copy the code
The second step is to load the plug-in resources
Context.getresources ().xxx(r.xx.xxx). The Resources object obtained by getResources() is stored in contextimpl.mResources. This object is also stored in Loadedapk.packageInfo.
Source visible ActivityThread. HandleLaunchActivity () – > ActivityThread. PerformLaunchActivity () – > LoadedApk. MakeApplication ()
// LoadedAPK public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { ...... ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); app = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); appContext.setOuterContext(app); . } // ContextImpl static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null); // Set mResource Context.setResources (packageInfo.getResources());return context;
}
Copy the code
Therefore, it is implemented by loading Resources from APK via AssetManaget to generate new Resources, and then replacing contextimpl. mResources with contextimpl.mpackageInfo. The implementation code is:
public static void loadPluginResources(Application application) throws Exception{ AssetManager assetManager = AssetManager.class.newInstance(); / / get AssetManager. AddAssetPath () Method addAssetPath = AssetManager. Class. GetMethod ("addAssetPath", String.class); Addassetpath. invoke(assetManager, getPatchApkPath(Application)); MerResource = new Resources(assetManager, application.getBaseContext().getResources().getDisplayMetrics(), application.getBaseContext().getResources().getConfiguration()); / / replace ContextImpl mResources ReflectUtil. SetField (application. GetBaseContext (),"mResources", merResource); // Get ContextImpl mPackageInfoField of type LoadedApk mPackageInfoField = application.getBaseContext().getClass().getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true); Object packageInfoO = mPackageInfoField.get(application.getBaseContext()); // Replace mpackageInfo. mResources reflectutil. setField(packageInfoO,"mResources", merResource); / / replace ContextImpl of Resources. The Theme Field themeField = application. GetBaseContext () getClass (). GetDeclaredField ("mTheme");
themeField.setAccessible(true);
themeField.set(application.getBaseContext(), null);
}
Copy the code
Step 3: Manage the Actvity plugin
The call stack can be briefly described in the following figure
1. Registration problems
An Activity needs to be registered in the AndroidManifest to be used, and dynamic registration is not possible. If you want to use an Activity that is not registered in the plugin, you cannot omit this step. The general approach is to use a surrogate StubActivity to register with AndroidManifest to achieve a placeholder effect, all plug-in activities through StubActivity together to cheat AMS.
2. Pass AMS verification
Since StubActivity has been registered normally, it must pass the verification of AMS. The problem is that using StubActivity instead of the actual Activity to pass THE AMS check requires that the actual Activity be dressed up as StubActivity at the appropriate time and restored at the appropriate time.
3. Context
Improve StubActivity to get the corresponding Context when working with AMS. During Activity startup, bind the Context when activity.attach () is attached. So make sure that the Activity that the ActivityThread builds is the Activity that it actually needs to get the Context.
In summary, the problem can be reduced to requiring a surrogate to pass the AMS detection and restore at the appropriate time. So there are dynamic replacement and static replacement. There are three subsequent implementations.
Method one, Hook Instrumentation
Instumentation. ExecStartActivity () can be regarded as the starting point of the Activity, can be used as a dress up practical Activity of the node. When ActivityThread loading Activity class, by Instumentation. NewActivity () to load, so here can be used as recovery nodes of real Activity.
Register StubActivity in AndroidManifest without creating a class file
<activity android:name=".StubActivity" />
Copy the code
Create their own Instrumentation class, implement, the key code is as follows
public class InstrumentationHook extends Instrumentation {
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
if(! Stubhelper.checkactivity (who, intent)){// Save information about the Activity to start, Intent.putextra (REAL_ACTIVITY_BANE, Intent.getComponent ().getClassName()); intent.setClassName(who, StubHelper.STUB_ACTIVITY); } try {// start with the actual mInstrumentationreturn (ActivityResult) startActivityMethod.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
}
return null;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
String startActivityName = intent.getStringExtra(REAL_ACTIVITY_BANE);
if(! Textutils. isEmpty(startActivityName) {// Restore the Activityreturn super.newActivity(cl, startActivityName, intent);
}
returnsuper.newActivity(cl, className, intent); }... }Copy the code
Then replace the Instrumentation, this part of the code is omitted.
Method two, agent AMS
Start the Activity process, but view the starting point as ams.startActivity (), and AMS finally creates the Activity by sending a message to ActivityThread, after activityThread.h receives the LAUNCH_ACTIVITY signal. It also means that the Activity passes the check. Select these two places as replacement and restore nodes.
In APP, AMS exists in the form of IActivityManager, and the purpose of AMS can be achieved by acting as IActivityManager. In 8.0 source code, agent IActivityManager, you need to follow the following steps:
- Get ActivityManager. GetService (), get IActivityManagerSingleton by this method
- IActivityManagerSingleton type for Singleton, Singleton auxiliary for the system to provide the Singleton implementation class, instance in Singleton. MInstance, therefore IActivityManager in mInstance
- Agent IActivityManager
The code implementation is as follows
Private static void replaceActivity(final Context Context) throws Exception{// Obtain AMS instance through ActivityManager,, Class amClass = class.forname ("android.app.ActivityManager");
Method getServiceMethod = amClass.getDeclaredMethod("getService");
final Object iActivityManagerObje = getServiceMethod.invoke(null);
Field iActivityManagerSingletonField = amClass.getDeclaredField("IActivityManagerSingleton");
Object iActivityManagerSingletonObj = ReflectUtil.getStaticField(amClass, "IActivityManagerSingleton"); // Obtain mInstance Class singleTonClass = class.forname ("android.util.Singleton");
Field mInstanceField = singleTonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
iActivityManagerSingletonField.setAccessible(true); / / ams instance final Object amsObj = ReflectUtil. GetField (iActivityManagerSingletonObj,"mInstance"); // Get IActivityManager Class<? > iamClass = Class.forName("android.app.IActivityManager"); Object proxy = proxy.newProxyInstance (thread.currentThread ().getContextClassLoader(), new Class<? >[]{iamClass}, newInvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// hook startActivity
if (!"startActivity".equals(method.getName())){
returnmethod.invoke(amsObj, args); IntentIndex = 0;for (int i=0; i<args.length; i++){
if (args[i] instanceof Intent){
intentIndex = i;
break; } } Intent realIntent = (Intent) args[intentIndex]; // Check whether the launched Activity is declared in the host Manifestif (StubHelper.checkActivity(context, realIntent)){
returnmethod.invoke(amsObj, args); Intent stubIntent = new Intent(); Stubinten.setcomponent (new ComponentName(StubHelper.self_pak, StubHelper.stub_activity)); stubIntent.putExtra(StubHelper.REAL_INTENT, realIntent); args[intentIndex] = stubIntent;returnmethod.invoke(amsObj, args); }}); / / agent ams mInstanceField. SetAccessible (true);
mInstanceField.set(iActivityManagerSingletonObj, proxy);
}
Copy the code
This completes the functionality disguised as StubActivity. On the node that triggers the Activity, the trigger is received via ActivityThread.H, and H is the Handler. Therefore, you can set the Handler Callback to revert to the actual Activity when it receives the LAUNCH_ACTIVITY signal. The code implementation is as follows:
Private static void restoreActivity() throws Exception{// Obtain ActivityThread Class atClass = class.forName ("android.app.ActivityThread");
Object curAtObj = ReflectUtil.getStaticField(atClass, "sCurrentActivityThread"); MHObj = (Handler) reflectutil. getField(curAtObj,"mH"); // Set Handler's mCallBack Class to handlerClass = handler.class; ReflectUtil.setField(mHObj,"mCallback", new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
try{
int LAUNCH_ACTIVITY = 0;
Class hClass = Class.forName("android.app.ActivityThread$H");
LAUNCH_ACTIVITY = (int) ReflectUtil.getStaticField(hClass, "LAUNCH_ACTIVITY");
ifIntent intent = (intent) Reflectutil. getField(msg.obj,"intent");
Intent realIntent = intent.getParcelableExtra(StubHelper.REAL_INTENT);
if(realIntent ! = null){ intent.setComponent(realIntent.getComponent()); } } } catch (Exception e){ } mHObj.handleMessage(msg);return true; }}); }Copy the code
APK plug-in for dynamic proxy
The way to manage Instrumentation or proxy AMS is a dynamic alternative, prepare APK plug-in for it. Note that the plugin APK inherits the Activity class. Instead of Resources, therefore, in the Activity plugin APK, you need to temporarily override the getResource method as follows
@Override
public Resources getResources() {
return(getApplication() ! = null && getApplication().getResources()! = null) ? getApplication().getResources() : super.getResources(); }Copy the code
The plug-in APK can come from the network, but for simplicity, it reads directly from the local. Simply place APK in a file path that the host has permission to read. My situation in the context. GetExternalCacheDir () getAbsolutePath () path, path for
/ storage/emulated / 0 / Android package name/data/your host/cache
Via ADB command
Adb push Local file path Mobile phone storage path
You can push the plug-in APK to your phone
Methods three
This method is static proxy, easy to understand, do not need to Hook any Framework layer code. It is carried out in three steps:
- Create a ProxyActivity in the host that acts as a proxy for the lifecycle
- Plugins create activities based on the BasePluginActivity class. Of course, these activities are not really activities, but just look like activities
- Start the Activity is actually start the ProxyActivity, and then bind the ProxyActivity to the plug-in Activity bidirectionally, and collect the callback method of the plug-in Activity. During lifecycle callbacks, the ProxyActivity communicates the callback information to the plug-in Activity, that is, by calling the specific methods of the plug-in Activity
BasePluginActivity is as follows:
Public abstract class BasePluginActivity {// Host protected Activity mHost; Public void proxy(Activity host){mHost = host; } public voidsetContentView(int layoutId){
mHost.setContentView(layoutId);
}
protected abstract void onCreate(Bundle savedInstanceState);
protected void onStart() {}; protected voidonRestart() {}; protected voidonResume() {}; protected voidonPause() {}; protected voidonStop() {}; protected voidonDestroy() {}; }Copy the code
ProxyAcitivty key codes are as follows:
Public class ProxyActivity extends Activity {private Object mPluginActivity; // Plugin Activity class name private String mPluginClassName; Private Map<String, Method> mLifecycleMethods = new HashMap<>(); public static final String PLUGIN_STUB ="plugin_stub"; Public static final String PLUGIN_CLASS_NAME ="com.bf.qinx.cosplayplugin.PluginActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MPluginClassName = getIntent().getStringExtra(PLUGIN_STUB); // proxyPluginActivity(); // Execute the plugin activity.onCreate () invokeLifecycleMethod()"onCreate", new Object[]{savedInstanceState}); } /** * proxy plugin Activity */ private voidproxyPluginActivity() {try{// Get the plug-in Activity Class<? > clazz = Class.forName(mPluginClassName); Constructor<? > con = clazz.getConstructor(new Class[]{}); mPluginActivity = con.newInstance(new Object[]{}); / / trigger a plugin hook point, establish links, namely call BasaPlauginActivity. The proxy () Method proxyMethod = clazz. GetMethod ("proxy", new Class[]{Activity.class});
proxyMethod.setAccessible(true); proxyMethod.invoke(mPluginActivity, new Object[]{ this }); ProxyLifecycle (clazz); } catch (Exception e){ e.printStackTrace(); }}... }Copy the code
After the connection between ProxyActivity and plug-in Activity is established and the corresponding method is collected and stored in mLifecycleMethods, when the ProxyActivity life cycle starts, the corresponding method of ProxyActivity can be triggered from mLifecycleMethods. For example onResume()
@Override
protected void onResume() {
super.onResume();
invokeLifecycleMethod("onResume", null);
}
private void invokeLifecycleMethod(String methodName, Object[] args){
try{
Object[] methodArgs = args;
if (methodArgs == null){
methodArgs = new Object[]{};
}
Method method = mLifecycleMethods.get(methodName);
if(method ! = null){ method.invoke(mPluginActivity, methodArgs); } } catch (Exception e){ Log.d("xx"."invokeLifcycleMethod: "+ e.getMessage()); }}Copy the code
Start the plug-in Activity
Hook AMS, Hook instrumentation, static replacement start Activity
private void startPatchActivityFormAMS(){
Intent intent = new Intent();
ComponentName componentName = new ComponentName("Here is the name of the plug-in package." , PATCH_ACTIVITY);
intent.setComponent(componentName);
startActivity(intent);
}
private void statPatchActivityFromInstrumentation(){
Intent intent = new Intent();
ComponentName componentName = new ComponentName(MainActivity.this , PATCH_ACTIVITY);
intent.setComponent(componentName);
startActivity(intent);
}
private void startCosplayActivity(){
Intent intent = new Intent();
intent.setClass(MainActivity.this, ProxyActivity.class);
intent.putExtra(ProxyActivity.PLUGIN_STUB, ProxyActivity.PLUGIN_CLASS_NAME);
startActivity(intent);
}
Copy the code
conclusion
The above dynamic scheme and static scheme as the direction, provides three ways to achieve. The advantage of the dynamic solution is to really manage the life cycle of the Activity (take the Activity as an example) and hook the key Framework code to make the plug-in Activity available. Disadvantages in compatibility and stability risk, need to deal with the differences between the major source code, such as the above proxy AMS, the actual source code scenario is more than 26. The static proxy approach, on the other hand, does not have any stability or compatibility problems. The problem is that it has certain limitations, such as when you want to simulate an Activity more realistically, you have to work hard.
Demo source address: Demo source address
The project structure is as follows
To realize plug-in, we need to solve three problems in the life cycle of four components: class loading, resource loading and management.
- Through the class loading mechanism, and class loading features in the Android, insert the plugin dex BaseDexClassLoader. DexPathList. DexElements can solve the problem of class loading
- The ARSC file in the plug-in dex is resolved into a ResTable needed to access the resource through AssertManager
- Through Hook AMS or Hook Instrumentation, StubActivity to achieve the purpose of AMS detection. You can also do this statically, using ProxyActivity to simulate an Activity
Of course, the above is only the simplest implementation, avoids a lot of problems, in the actual plug-in scheme implementation is much more complex.
Through manual implementation of a plug-in, not only have a deeper understanding of the implementation principle, but also support the Framework knowledge involved, so the principle of class loading, resource loading principle, Activity startup principle also have a more sincere understanding.
reference
Analysis of the principle of Android plug-in
Android plugins principles and practices (4) merge resources in plugins
Android Hook Start an Activity app without a list
Android plugins go from getting started to giving up
Android Development: From Modularity to Componentization