With the continuous growth of the project, even if the project adopts excellent architecture such as MVP or MVVM, it is difficult to keep up with the pace of iteration. When the functions of the APP end become bigger and more complicated, and the staff constantly join in, it often happens that the whole situation will be changed at one go, and the subsequent staff will feel like walking on thin ice to maintain the project. To this end, we must consider the development mode after the expansion of the team, isolate the business in advance, summarize the process of plug-in development, and improve the basic framework of the Android side.

In this article, the third and most frustrating part of my Android refactoring tour, I’ll talk about the concept of “plug-in” and our choices and problems with the “plug-in” framework.

Plug-in Hello World

Plug-in refers to the division of APK into host and plug-in parts. When the APP is running, we can dynamically load or replace the plug-in parts. Host: is the currently running APP. Plugins: As opposed to plugins, you load running APK class files.

Plug-in tumble for two kinds of forms, a plug-in APP no interaction with the host WeChat with WeChat small procedures, for example, a plug-in highly coupled with the host drops travel, for example, drops travel user information as a separate module, need data interaction with other modules, due to the usage scenario, this article only for plug-in data with frequent interaction with the host.

In our development process, often encounter many people collaborate on the development of modular, we expect to be able to run independently their own module and is not affected by other modules, there is a more common requirement, our product in rapid iteration, we often hope to be able to seamlessly to the new features to the user’s phone, Too many product iterations or too long development cycles, which can make us lose the edge when competing with unexpected products.

Above, a facial recognition product iteration record, due to launch in cities has the logic of nuances, prompted a BUG for each core business colleagues to Push one by one to explore various versions, promoter to download, then notify the cities at that time I just think, can make our application into a plug-in in the form of a dynamic distributed? This saved me from having to upgrade every time, and in the middle of a Push night, I decided I couldn’t do this anymore, I had to use plugins.

Choice of plug-in framework

The following figure shows the mainstream plug-in and componentized frameworks

features DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
Supports four major components Only support Activity Only support Activity Only support Activity Full support Full support
Components do not need to be pre-registered in the host manifest Square root x Square root Square root Square root
Plug-ins can depend on the host Square root Square root Square root x Square root
Support the PendingIntent x x x Square root Square root
Android Feature Support Most of the Most of the Most of the Almost all of the Almost all of the
Compatibility and adaptation general general medium high high
The plug-in build There is no The deployment of aapt Gradle plug-in There is no Gradle plug-in

After repeated deliberation, we finally decided to use Didi Chuxing’s VirtualAPK as our plug-in framework, which has the following advantages:

  • Can communicate with the host project
  • Strong compatibility
  • Using a simple
  • Easy to compile plug-ins
  • Through extensive use

If you’re loading a plug-in that doesn’t need any coupling or communication with the host, and you don’t want to repackage the plug-in, the DroidPlugin is recommended.

Principle of plug-in

VirtualAPK has no additional constraints on plug-ins, and native APK can be used as plug-ins. After the plug-in project is compiled and generated, the Apk can be loaded through the host App. After each plug-in Apk is loaded, a separate LoadedPlugin object will be created in the host. With these LoadedPlugin objects, as shown below, VirtualAPK can manage plug-ins and give them new meaning, making them behave like apps installed on your phone.

When we introduce a framework often can not only simply understand how to use, should go to in-depth understanding of how it works, especially plug-in this popular technology, very grateful to the open source project gave us a golden key to explore the Android world, the following will be simple and simple analysis of the principle of VirtualAPK.

The four components are familiar to Android personnel, we all know that the four components are required to register in the AndroidManifest, and for VirtualAPK is not possible to know the name in advance, registered in advance in the host Apk, so now basically adopt hack solution, The VirtualAPK scheme is as follows:

  • The Activity: Start the Activity in Apk by Hook Activity startup process, because the Activity has different LaunchMode and some special familiar. Therefore, multiple “ghost” activities are required to occupy the pit.
  • Service: distribution by proxy Service; VirtualAPK uses two proxy services for the master and other processes.
  • BroadcastReceiver: switch from static to dynamic.
  • ContentProvider: Distributed through a proxy Provider.

In this article, we will focus on the Activity capture process, if you want to learn more about VirtualAPK please click me

The Activity process

To enable VirtualAPK, we need to call pluginManager.loadPlugin(APk) to load the plug-in, and then we continue to call

LoadedPlugin plugin = LoadedPlugin. Create (this, this.mcontext, apk); // Load the plugin's Application plugin.invokeApplication();Copy the code

Loadedplugin.create is used to parse the Activity of the plugin. After that, the plugin is saved to mPlugins to facilitate the next call and unbinding of the plugin

// copy Resources this.mResources = createResources(context, apk); This. mClassLoader = createClassLoader(Context, apk, this.mNativeLibDir, context.getClassLoader()); // If it is initialized, it is not parsedif(pluginManager.getLoadedPlugin(mPackageInfo.packageName) ! = null) { throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
        }
        // 解析APK
        this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
        // 拷贝插件中的So
        tryToCopyNativeLib(apk);
        // 保存插件中的 Activity 参数
        Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity activity : this.mPackage.activities) {
            activityInfos.put(activity.getComponentName(), activity.info);
        }
        this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
        this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);

Copy the code

LoadedPlugin incorporates resources from our plugin into the host App, so the loading process of the plugin App has been completed. Here we must have some confusion.

This involves the start process of the Activity. After startActivity, the system will finally call the execStartActivity method of the Instrumentation. You then interact with AMS through ActivityManagerProxy.

The verification of whether an Activity is registered in the Manifest is performed by AMS, so we can replace the ComponentName submitted to AMS by ActivityManagerProxy with our ComponentName before AMS interacts with it. Usually we can choose Hook Instrumentation or Hook ActivityManagerProxy can achieve the goal, VirtualAPK select Hook Instrumentation.

 private void hookInstrumentationAndHandler() {
        try {
            Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
            if (baseInstrumentation.getClass().getName().contains("lbe")) {
                // reject executing in paralell space, forexample, lbe. System.exit(0); } // The name used to handle the replacement Activity final VAInstrumentation = new VAInstrumentation(this, baseInstrumentation); Object activityThread = ReflectUtil.getActivityThread(this.mContext); / / Hook Instrumentation. Replace the Activity name ReflectUtil setInstrumentation (activityThread, Instrumentation); // Hook handleLaunchActivity ReflectUtil.setHandlerCallback(this.mContext, instrumentation); this.mInstrumentation = instrumentation; } catch (Exception e) { e.printStackTrace(); }}Copy the code

Above we have successfully Hook the Instrumentation, the next is the need for our Ghost play

    public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); // Only activities in plug-ins are replacedif(intent.getComponent() ! = null) { Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName())); / / use"Phoney"To replace this. MPluginManager. GetComponentsHandler () markIntentIfNeeded (intent); } ActivityResult result = realExecStartActivity(who, contextThread, token, target, intent, requestCode, options);return result;
    }

Copy the code

Let’s take a look at markIntentIfNeeded; What did he do

    public void markIntentIfNeeded(Intent intent) {
        if (intent.getComponent() == null) {
            return; } String targetPackageName = intent.getComponent().getPackageName(); String targetClassName = intent.getComponent().getClassName(); // Save our original dataif(! targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) ! = null) { intent.putExtra(Constants.KEY_IS_PLUGIN,true); intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName); dispatchStubActivity(intent); } } private void dispatchStubActivity(Intent intent) { ComponentName component = intent.getComponent(); String targetClassName = intent.getComponent().getClassName(); LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent); ActivityInfo info = loadedPlugin.getActivityInfo(component); // Determine if it is an Activity in a plug-inif (info == null) {
            throw new RuntimeException("can not find "+ component); } int launchMode = info.launchMode; / / into the Theme of the Resources. The Theme themeObj = loadedPlugin. GetResources (). NewTheme (); themeObj.applyStyle(info.theme,true); / / the Activity from the plug-in is replaced by the Activity of pit String stubActivity = mStubActivityInfo. GetStubActivity (targetClassName launchMode, themeObj); Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
        intent.setClassName(mContext, stubActivity);
    }

Copy the code

GetStubActivity (targetClassName, launchMode, themeObj); Student: The substitution


    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public String getStubActivity(String className, int launchMode, Theme theme) {
        String stubActivity= mCachedStubActivity.get(className);
        if(stubActivity ! = null) {return stubActivity;
        }

        TypedArray array = theme.obtainStyledAttributes(new int[]{
                android.R.attr.windowIsTranslucent,
                android.R.attr.windowBackground
        });
        boolean windowIsTranslucent = array.getBoolean(0, false);
        array.recycle();
        if (Constants.DEBUG) {
            Log.d("StubActivityInfo"."getStubActivity, is transparent theme ? " + windowIsTranslucent);
        }
        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
        switch (launchMode) {
            case ActivityInfo.LAUNCH_MULTIPLE: {
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) {
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                }
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TOP: {
                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TASK: {
                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                break;
            }

            default:break;
        }

        mCachedStubActivity.put(className, stubActivity);
        return stubActivity;
    }

Copy the code
<! -- Stub Activities --> <activity android:name=".BThe $1" android:launchMode="singleTop"/>
       <activity android:name=".CThe $1" android:launchMode="singleTask"/>
       <activity android:name=".DThe $1" android:launchMode="singleInstance"/> < span style = "box-sizing: border-box! ImportantCopy the code

StubActivityInfo starts the corresponding “ghost” Activity based on the same launchMode. So far, we have successfully tricked AMS into launching our spoiling Activity, but only half of it is successful. After AMS is executed, the final Activity to be started is not the Activity that spoils the pit, so we need to be able to start the target Activity correctly.

We Hook the handleLaunchActivity at the same time in the Instrumentation, so between us to the newActivity method of the Instrumentation to view the process of starting the Activity.

@Override public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {try {// Whether the Activity cl.loadClass(className) can be loaded directly; } catch (ClassNotFoundException e) {// Get the correct Activity LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); String targetClassName = PluginUtil.getTargetActivity(intent); Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName)); // Check whether VirtualApk started the plug-in Activityif(targetClassName ! = null) { Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); // Start the plug-in Activity activity.setintent (intent); try { //for4.1 + ReflectUtil. SetField (ContextThemeWrapper. Class, the activity,"mResources", plugin.getResources());
                } catch (Exception ignored) {
                    // ignored.
                }
                returnactivity; }} // The host Activity starts directlyreturn mBase.newActivity(cl, className, intent);
    }

Copy the code

Now, the Activity can start normally.

summary

VritualApk sort out the idea is very clear, here we only introduce the way to start the Activity, interested students can go to the Internet to understand the other three forms of proxy. However, if you want to use a plug-in framework, it is important to understand how it is implemented. The documentation does not cover all the details, many attributes, and features that are not supported due to the way they are implemented.

Introducing the pain of plugins

As a result of the project host interactions with the plug-in needs to be more closely, at the same time of pluggable need to modular of project, but the modularity doesn’t happen overnight, often occurs in the process of modular, highly interconnected problems, after all night after night, I summarized the modular several principles.

VirtualAPK itself is not difficult to use, but the difficulty is that it needs to gradually organize the modules of the project. During this period, there are many problems. Because I have no relevant experience, I read a lot of articles about modularity on the Internet.

There are several guidelines to follow when modularizing a project

  • Determine business logic boundaries
  • Exercise restraint on module changes
  • Timely extraction of public resources

Before determining the business logic boundary, we should first analyze the business logic in detail. App is the end of the business chain, and due to the limitations of its role, developers’ understanding of the business is less than that of the back end. The so-called haste makes waste.

In modular, we need the business module in isolation, business module cannot depend on each other can exist between data transmission, can only be one-way dependent on host project, in order to achieve this effect We need to borrow the routing scheme ARouter on market, due to the space, here I am, do not do too much introduction, interested students can search by oneself.

After the project transformation, the host only left the simplest common base logic, and the other parts were loaded in the form of plug-ins, which gave us great freedom in the process of version update. From the project structure, we looked like all plug-ins depended on the code of the host App. But VirtualAPK actually helps eliminate duplicate resources during the packaging process.

Module on the changes of restraint in modular, don’t be the pursuit of the perfect goal, simple and crude, the follow-up to gradually improve, a lot of business logic and other business logic often produce, both of them will be a relatively ambiguous relationship, this time we don’t go to forcibly divided their business boundary, Excessive segmentation will often lead to the collapse of the project transformation because the coder is not clear about the module.

The timely extraction of VirtualAPK from public resources will help us eliminate duplicate resources. For some ambiguous resources, we can simply put them into the host project. If too many resources are stored in the plug-in project, it will cause our plug-in to lose the flexibility and reuse of resources.

conclusion

Initial internal promotion in the company of plug-in, colleagues outcry is a most question of the plug-in, here I want to thank my leadership, at a critical moment to give my support to help me to resist the sound of people questioned, and, after more than ten days and nights of modified reconstruction after the plug-in, the first online version of the plugin flexible advantage manifests incisively and vividly, Each plug-in is only 60 KB in size, and there is almost no pressure on the bandwidth of the server side, which helps us to quickly carry out product iteration and Bug repair. In this article, only my own experience and ideas in the project plug-in, not in-depth introduction to how to use VirtualAPK interested students can read the VirtualAPK WiKi, hope this article design ideas can bring you some help.

Link: https://www.jianshu.com/p/c6f2a516b182, reprint please indicate the original

To read more

My Android Refactoring Journey: The Framework

My Android Refactoring Journey: Architecture

An overview of how to keep Android processes alive

NDK project actual combat – high imitation 360 mobile phone assistant uninstall monitoring