• An overview of the
    • The Activity support
      • Hook ActivityManagerService
      • Hook Instrumentation
      • Start the plug-in Activity
    • The Service support
    • ContentProvider support
    • Receiver support
    • summary

An overview of the

Didi Chuxing’s first external open source project – VirtualAPK. Address: github.com/didi/Virtua…

Didi developed this plug-in framework by itself, which is fully functional and compatible, and can also be applied to coupled business plug-ins, which is the meaning of the existence of VirtualAPK. VirtualAPK is considered the open source solution of choice when it comes to loading coupled plug-ins. It is said that Didi Taxi has been used, so it is necessary to find out

The VirtualAPK workflow looks like this:

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

The Activity support

Hook ActivityManagerService

The first thing to solve is that the Activity in the plug-in is not registered in the host application’s AndroidMainfest.xml. It is definitely not possible to directly start the Activity in the plug-in using conventional methods. For details about the main steps in the startup process, see

From the article, it is actually the Activity invoked the Instrumentation. ExecStartActivity this method. The source code is as follows:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,  
            Intent intent, int requestCode, Bundle options) {  
    IApplicationThread whoThread = (IApplicationThread) contextThread;  
    if(mActivityMonitors ! =null) {  
        synchronized (mSync) {  
            final int N = mActivityMonitors.size();  
            for (int i=0; i<N; i++) { // Check to see if the activity exists
                final ActivityMonitor am = mActivityMonitors.get(i);  
                if (am.match(who, null, intent)) {  
                    am.mHits++;  
                    if (am.isBlocking()) {  
                        return requestCode >= 0 ? am.getResult() : null;  
                    }  
                    break; }}}}try {  
        intent.migrateExtraStreamToClipData();  
        intent.prepareToLeaveProcess();  
        // This is where the activity really starts, and its core functionality is done in whoThread.
        intresult = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()),token, target ! =null ? target.mEmbeddedID : null,  
                    requestCode, 0.null, options);         
        checkStartActivityResult(result, intent); // Handle various exceptions such as ActivityNotFound
    } catch (RemoteException e) {  
    }  
    return null;  
}Copy the code

Visible, startActivity finally passed ActivityManagerNative. GetDefault () a remote call startActivity method of the AMS, ActivityManagerNative is actually a Binder proxy object for ActivityManagerService, a remote object, through which remote IPC calls are made every time you need to interact with AMS.

Android Interprocess Communication (IPC) with Binder


// ActivityManagerNative.getDefault()
static public IActivityManager getDefault() {
    return gDefault.get();
}

private static final Singleton<iactivitymanager> gDefault = 
    new Singleton<iactivitymanager>() {
           protected IActivityManager create() {
               IBinder b = ServiceManager.getService("activity");
               if (false) {
                   Log.v("ActivityManager"."default service binder = " + b);
               }
               IActivityManager am = asInterface(b);
               if (false) {
                   Log.v("ActivityManager"."default service = " + am);
               }
               returnam; }};Copy the code

From this we can know, ActivityManagerNative. GetDefault () is actually a return to a IActivityManager singleton.

So, the first thing VirtualApk does is save the AMS proxy object. First, we can have a look at the core repository VirtualApk com. Didi. VirtualApk. The PluginManager initialization of this class:


// constructor
private PluginManager(Context context) {
    Context app = context.getApplicationContext();
    if (app == null) {
        this.mContext = context;
    } else {
        this.mContext = ((Application)app).getBaseContext();
    }
    prepare();
}

/ / initialization
private void prepare() {
    Systems.sHostContext = getHostContext();
    this.hookInstrumentationAndHandler();
    this.hookSystemServices();
}

/** * Hook an IActivityManager, which is the AMS proxy object */
private void hookSystemServices() {
    try {
        / / reflection calls ActivityManagerNative. GetDefault (), in fact, this is a public static method in 6.0, reflex may be considering the version compatibility?
        Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null."gDefault");
        // Proxy objects are created using a dynamic proxy through which all methods in ActivityManagerNative are called
        IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());

        // Hook IActivityManager from ActivityManagerNative to replace ActivityManagerNative with activityManagerProxy
        ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);

        if (defaultSingleton.get() == activityManagerProxy) {
            // The same thing, save it
            this.mActivityManager = activityManagerProxy; }}catch(Exception e) { e.printStackTrace(); }}Copy the code

In fact, in addition to the fact that startActivity is the method that calls AMS, methods like startService, bindService, and so on, eventually call the method in AMS, The us in a dynamic proxy class com. Didi. Virtualapk. Delegate. ActivityManagerProxy can find:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("startService".equals(method.getName())) {
        try {
            // Execute the custom startService procedure, as described later
            return startService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Start service error", e); }}else if ("stopService".equals(method.getName())) {
        try {
            return stopService(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop Service error", e); }}else if ("stopServiceToken".equals(method.getName())) {
        try {
            return stopServiceToken(proxy, method, args);
        } catch (Throwable e) {
            Log.e(TAG, "Stop service token error", e); }}else if ("bindService".equals(method.getName())) {
        try {
            return bindService(proxy, method, args);
        } catch(Throwable e) { e.printStackTrace(); }}else if ("unbindService".equals(method.getName())) {
        try {
            return unbindService(proxy, method, args);
        } catch(Throwable e) { e.printStackTrace(); }}else if ("getIntentSender".equals(method.getName())) {
        try {
            getIntentSender(method, args);
        } catch(Exception e) { e.printStackTrace(); }}else if ("overridePendingTransition".equals(method.getName())){
        try {
            overridePendingTransition(method, args);
        } catch(Exception e){ e.printStackTrace(); }}try {
        // sometimes system binder has problems.
        return method.invoke(this.mActivityManager, args);
    } catch (Throwable th) {
        Throwable c = th.getCause();
        if(c ! =null && c instanceof DeadObjectException) {
            // retry connect to system binder
            IBinder ams = ServiceManager.getService(Context.ACTIVITY_SERVICE);
            if(ams ! =null) {
                IActivityManager am = ActivityManagerNative.asInterface(ams);
                mActivityManager = am;
            }
        }

        Throwable cause = th;
        do {
            if (cause instanceof RemoteException) {
                throwcause; }}while((cause = cause.getCause()) ! =null);

        throwc ! =null? c : th; }}Copy the code

This is essentially equivalent to rewriting some Activity and Service operations. What to do, will be mentioned later

Hook Instrumentation

Go back and look at Instrumentation. ExecStartActivity this method, at the last have so a code:

checkStartActivityResult(result, intent); // Handle various exceptions such as ActivityNotFoundCopy the code
static void checkStartActivityResult(int res, Object intent) {  
    if (res >= ActivityManager.START_SUCCESS) {  
        return;  
    }  

    switch (res) {  
        case ActivityManager.START_INTENT_NOT_RESOLVED:  
        case ActivityManager.START_CLASS_NOT_FOUND:  
            if (intent instanceofIntent && ((Intent)intent).getComponent() ! =null)  
                throw new ActivityNotFoundException(  
                        "Unable to find explicit activity class "  
                        + ((Intent)intent).getComponent().toShortString()  
                        + "; have you declared this activity in your AndroidManifest.xml?");  
            throw new ActivityNotFoundException(  
                    "No Activity found to handle " + intent);  
        case ActivityManager.START_PERMISSION_DENIED:  
            throw new SecurityException("Not allowed to start activity "  
                    + intent);  
        case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:  
            throw new AndroidRuntimeException(  
                    "FORWARD_RESULT_FLAG used while also requesting a result");  
        case ActivityManager.START_NOT_ACTIVITY:  
            throw new IllegalArgumentException(  
                    "PendingIntent is not an activity");  
        default:  
            throw new AndroidRuntimeException("Unknown error code "  
                    + res + " when starting "+ intent); }}Copy the code

Unable to find explicit Activity class (androidMainfest.xml) : Unable to find explicit Activity class (AndroidMainfest.xml) : Unable to find explicit Activity class (AndroidMainfest.xml)

The plugin’s Activity is not registered with the host application’s AndroidMainfest.xml. How can you bypass this layer of detection?

Mentioned before, com. Didi. Virtualapk. PluginManager class – this class initialization, in addition to Hook up with a AMS proxy object, also the hooks an Instrumentation object. The code is as follows:

private void hookInstrumentationAndHandler() {
    try {
        Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
        if (baseInstrumentation.getClass().getName().contains("lbe")) {
            // reject executing in paralell space, for example, lbe.
            System.exit(0);
        }

        // Create custom instrumentation, overriding some methods such as newActivity()
        // baseInstrumentation will be used after, also save
        final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);

        // Get an instance of ActivityThread
        Object activityThread = ReflectUtil.getActivityThread(this.mContext);

        // Replace the instrumentation in ActivityThread with custom instrumentation
        ReflectUtil.setInstrumentation(activityThread, instrumentation);
        ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
        this.mInstrumentation = instrumentation;
    } catch(Exception e) { e.printStackTrace(); }}Copy the code

Since the start of the Activity, the middle left Instrumentation. ExecStartActivity this method, then we can probably know, Hook out an Instrumentation object is used to do, This is essentially an Activity that helps launch the plug-in.

Start the plug-in Activity

Instead of Instrumentation, we Hook a VAInstrumentation, So when the system calls methods such as the newActivity() of its mInstrumentation member through the ActivityThread, it is actually calling the newActivity() of our VAInstrumentation.

In practice, plug-in Activity launches are pitted in the host manifest to bypass system validation before loading the actual Activity.

What is a pit grab? The idea is to build a series of fake Activity avatars, register them in AndroidMainfest.xml to bypass detection, and then turn them back to launch the actual target Activity when it’s time to do so. So how does this step work?

We can open androidMainfest.xml in the core library to see:

<application>
    <! -- Stub Activities -->
    <activity android:name=".A$1" android:launchMode="standard"/>
    <activity android:name=".A$2" android:launchMode="standard"
        android:theme="@android:style/Theme.Translucent" />

    <! -- Stub Activities -->
    <activity android:name=".B$1" android:launchMode="singleTop"/>
    <activity android:name=".B$2" android:launchMode="singleTop"/>
    <activity android:name=".B$3" android:launchMode="singleTop"/>
    <activity android:name=".B$4" android:launchMode="singleTop"/>
    <activity android:name=".B$5" android:launchMode="singleTop"/>
    <activity android:name=".B$6" android:launchMode="singleTop"/>
    <activity android:name=".B$7" android:launchMode="singleTop"/>
    <activity android:name=".B$8" android:launchMode="singleTop"/>

    <! -- Stub Activities -->
    <activity android:name=".C$1" android:launchMode="singleTask"/>
    <activity android:name=".C$2" android:launchMode="singleTask"/>
    <activity android:name=".C$3" android:launchMode="singleTask"/>
    <activity android:name=".C$4" android:launchMode="singleTask"/>
    <activity android:name=".C$5" android:launchMode="singleTask"/>
    <activity android:name=".C$6" android:launchMode="singleTask"/>
    <activity android:name=".C$7" android:launchMode="singleTask"/>
    <activity android:name=".C$8" android:launchMode="singleTask"/>

    <! -- Stub Activities -->
    <activity android:name=".D$1" android:launchMode="singleInstance"/>
    <activity android:name=".D$2" android:launchMode="singleInstance"/>
    <activity android:name=".D$3" android:launchMode="singleInstance"/>
    <activity android:name=".D$4" android:launchMode="singleInstance"/>
    <activity android:name=".D$5" android:launchMode="singleInstance"/>
    <activity android:name=".D$6" android:launchMode="singleInstance"/>
    <activity android:name=".D$7" android:launchMode="singleInstance"/>
    <activity android:name=".D$8" android:launchMode="singleInstance"/>

</application>Copy the code

You can see a bunch of fake StubActivities registered in the manifest. ABCD corresponds to different startup modes, so how do we change the plugin’s Activity name to the one registered in the listing?

Inside the VAInstrumentation, override the path to the startActivity () method: execStartActivity()

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

    ResolveInfo (ResolveInfo, ResolveInfo, ResolveInfo, ResolveInfo, ResolveInfo, ResolveInfo
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
    // null component is an implicitly intent
    if(intent.getComponent() ! =null) {
        Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                intent.getComponent().getClassName()));
        / /!!!!!! The big deal here is to replace the real Activity with those registered fake StubActivities to bypass detection!!
        this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }

    ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                intent, requestCode, options);

    return result;

}

private ActivityResult realExecStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ActivityResult result = null;
    try {
        Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
        int.class, Bundle.class};
        result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
                "execStartActivity", parameterTypes,
                who, contextThread, token, target, intent, requestCode, options);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return result;
}Copy the code

So how do you replace StubActivity? Follow-up Code:

public void markIntentIfNeeded(Intent intent) {
    if (intent.getComponent() == null) {
        return;
    }

    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    // Determine whether to start the Activity of the plug-in
    if(! targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) ! =null) {
        / / marking
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        // Save the true intentionintent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName); dispatchStubActivity(intent); }}/** * The real conversion is here. Convert the corresponding StubActivity */ based on the startup mode
private void dispatchStubActivity(Intent intent) {
    ComponentName component = intent.getComponent();
    String targetClassName = intent.getComponent().getClassName();
    LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
    ActivityInfo info = loadedPlugin.getActivityInfo(component);
    if (info == null) {
        throw new RuntimeException("can not find " + component);
    }
    int launchMode = info.launchMode;
    // Temporarily replace the theme
    Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
    themeObj.applyStyle(info.theme, true);

    // Complete the conversion
    String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
    Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
    intent.setClassName(mContext, stubActivity);
}Copy the code

Follow up code:

class StubActivityInfo {
    public static final int MAX_COUNT_STANDARD = 1;
    public static final int MAX_COUNT_SINGLETOP = 8;
    public static final int MAX_COUNT_SINGLETASK = 8;
    public static final int MAX_COUNT_SINGLEINSTANCE = 8;

    public static final String corePackage = "com.didi.virtualapk.core";

    // This format is the name of those fake activities
    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 final int usedStandardStubActivity = 1;
    public int usedSingleTopStubActivity = 0;
    public int usedSingleTaskStubActivity = 0;
    public int usedSingleInstanceStubActivity = 0;

    private HashMap<String, String> mCachedStubActivity = new HashMap<>();

    /** * Construct StubActivity */ here based on the startup mode and theme
    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);
        returnstubActivity; }}Copy the code

At this point, it’s pretty clear. Also, since StubActivity has been changed, it will have to be changed back when it is actually started. Take a look at the rewritten newActivity() method:

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
    } catch (ClassNotFoundException e) {
        // According to the intent type, to obtain the corresponding plug-in
        LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
        // In the Intent, we retrieve the actual Intent we just saved
        String targetClassName = PluginUtil.getTargetActivity(intent);

        Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));

        if(targetClassName ! =null) {
            // mBase is the original Instrumentation object, so this is actually handed over to the original Instrumentation object of the system, so this mode can also be understood as the dynamic proxy
            Plugin.getclassloader () is a DexClassLoader created by plugin.getClassLoader()
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);

            try {
                / / for 4.1 +
                ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
            } catch (Exception ignored) {
                // ignored.
            }

            returnactivity; }}return mBase.newActivity(cl, className, intent);
}Copy the code

At this point, the analysis of the plug-in’s Activity launch process is basically over. Details, can not step in place, but also need to look at the source code while understanding, so as to see more thoroughly.

The Service support

For Service support, dynamic proxy AMS intercepts service-related requests and forwards them to Service Runtime, which takes over all operations of the system.

For our dynamic proxy AMS, which was introduced in the previous Activity support section, let’s take a quick look at how ActivityManagerProxy starts a Service.

When a method such as startService is executed, the AMS proxy object executes the following methods accordingly:

private Object startService(Object proxy, Method method, Object[] args) throws Throwable {
    IApplicationThread appThread = (IApplicationThread) args[0];
    Intent target = (Intent) args[1];
    ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
    if (null == resolveInfo || null == resolveInfo.serviceInfo) {
        // is host service
        return method.invoke(this.mActivityManager, args);
    }

    return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}

private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
    return mPluginManager.getHostContext().startService(wrapperIntent);
}

private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
    // fill in service with ComponentName
    target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
    String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

    RemoteService/LocalService/RemoteService
    // LocalService and RemoteService correspond to whether to start the Activity in a new process
    boolean local = PluginUtil.isLocalService(serviceInfo);
    Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
    Intent intent = new Intent();
    intent.setClass(mPluginManager.getHostContext(), delegate);
    intent.putExtra(RemoteService.EXTRA_TARGET, target);

    // Save the Command to perform different operations
    intent.putExtra(RemoteService.EXTRA_COMMAND, command);
    intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
    if(extras ! =null) {
        intent.putExtras(extras);
    }

    return intent;
}Copy the code

Actually including our calling stopService (), AMS proxy objects after transformation of intentions, is also the end of the code above two methods startDelegateServiceForTarget and wrapperTargetIntent (), Only command is different.

So essentially AMS acts as a proxy, calling LocalService or RemoteService’s startService method whenever you start or close a Service in the plug-in. In LocalService or RemoteService onStartCommand(), perform the corresponding operation according to command. So let’s look at the onStartCommand() method of LocalService:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (null== intent || ! intent.hasExtra(EXTRA_TARGET) || ! intent.hasExtra(EXTRA_COMMAND)) {return START_STICKY;
    }

    Intent target = intent.getParcelableExtra(EXTRA_TARGET);
    int command = intent.getIntExtra(EXTRA_COMMAND, 0);
    if (null == target || command <= 0) {
        return START_STICKY;
    }

    ComponentName component = target.getComponent();
    LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);

    switch (command) {
        case EXTRA_COMMAND_START_SERVICE: {
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                service = this.mPluginManager.getComponentsHandler().getService(component);
            } else {
                try {
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                } catch (Throwable t) {
                    return START_STICKY;
                }
            }

            service.onStartCommand(target, 0.this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
            break;
        }
        case EXTRA_COMMAND_BIND_SERVICE: {
            ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
            IApplicationThread appThread = mainThread.getApplicationThread();
            Service service = null;

            if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                service = this.mPluginManager.getComponentsHandler().getService(component);
            } else {
                try {
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();

                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                } catch(Throwable t) { t.printStackTrace(); }}try {
                IBinder binder = service.onBind(target);
                IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");
                IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);
                iServiceConnection.connected(component, binder);
            } catch (Exception e) {
                e.printStackTrace();
            }
            break;
        }
        case EXTRA_COMMAND_STOP_SERVICE: {
            Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
            if (null! = service) {try {
                    service.onDestroy();
                } catch (Exception e) {
                    Log.e(TAG, "Unable to stop service " + service + ":"+ e.toString()); }}else {
                Log.i(TAG, component + " not found");
            }
            break;
        }
        case EXTRA_COMMAND_UNBIND_SERVICE: {
            Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
            if (null! = service) {try {
                    service.onUnbind(target);
                    service.onDestroy();
                } catch (Exception e) {
                    Log.e(TAG, "Unable to unbind service " + service + ":"+ e.toString()); }}else {
                Log.i(TAG, component + " not found");
            }
            break; }}return START_STICKY;
}Copy the code

Obviously, this is where the plug-in Service life cycle is controlled. The specific code is left to our analysis ~~

ContentProvider support

The dynamic agent IContentProvider intercepts provider-related requests and forwards them to the Provider Runtime, which takes over all operations of the system.

Let’s take a look at the com. Didi. Virtualapk. Internal. PluginContentResolver this class:

public class PluginContentResolver extends ContentResolver {
    private ContentResolver mBase;
    private PluginManager mPluginManager;
    private static Method sAcquireProvider;
    private static Method sAcquireExistingProvider;
    private static Method sAcquireUnstableProvider;

    static {
        try {
            sAcquireProvider = ContentResolver.class.getDeclaredMethod("acquireProvider".new Class[]{Context.class, String.class});
            sAcquireProvider.setAccessible(true);
            sAcquireExistingProvider = ContentResolver.class.getDeclaredMethod("acquireExistingProvider".new Class[]{Context.class, String.class});
            sAcquireExistingProvider.setAccessible(true);
            sAcquireUnstableProvider = ContentResolver.class.getDeclaredMethod("acquireUnstableProvider".new Class[]{Context.class, String.class});
            sAcquireUnstableProvider.setAccessible(true);
        } catch (Exception e) {
            //ignored}}public PluginContentResolver(Context context) {
        super(context);
        mBase = context.getContentResolver();
        mPluginManager = PluginManager.getInstance(context);
    }

    protected IContentProvider acquireProvider(Context context, String auth) {
        try {
            if (mPluginManager.resolveContentProvider(auth, 0) != null) {
                // In this case, hook an IContentProvider proxy object
                return mPluginManager.getIContentProvider();
            }

            return (IContentProvider) sAcquireProvider.invoke(mBase, context, auth);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    // ...
}Copy the code

This class is created in the PluginContext object getContentResolver() that was created when the LoadedPlugin was constructed.

class PluginContext extends ContextWrapper {

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }

    @Override
    public ContentResolver getContentResolver() {
        // Create proxy support
        return newPluginContentResolver(getHostContext()); }}Copy the code

So, the IContentProvider proxy object for the Hook above is actually made in PluginManager.

private void hookIContentProviderAsNeeded() {
    Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
    mContext.getContentResolver().call(uri, "wakeup".null.null);
    try {
        Field authority = null;
        Field mProvider = null;
        ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
        Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
        Iterator iter = mProviderMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object val = entry.getValue();
            String auth;
            if (key instanceof String) {
                auth = (String) key;
            } else {
                if (authority == null) {
                    authority = key.getClass().getDeclaredField("authority");
                    authority.setAccessible(true);
                }
                auth = (String) authority.get(key);
            }
            if (auth.equals(PluginContentResolver.getAuthority(mContext))) {
                if (mProvider == null) {
                    mProvider = val.getClass().getDeclaredField("mProvider");
                    mProvider.setAccessible(true);
                }
                IContentProvider rawProvider = (IContentProvider) mProvider.get(val);
                IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                mIContentProvider = proxy;
                Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                break; }}}catch(Exception e) { e.printStackTrace(); }}Copy the code

The content of this piece is best according to the Demo provided by Didi, and then look at it, it is easier to understand.

Uri bookUri = Uri.parse("content://com.ryg.chapter_2.book.provider/book");
ContentValues values = new ContentValues();
values.put("_id".6);
values.put("name"."The Art of Programming");Copy the code

Ha ha, the author is a little lazy, using Ren Yugang’s “Android development art Exploration” to change, was found

Receiver support

The receiver registered statically in the plugin is re-registered. It seems that there is no corresponding support in the code, nor in the Demo, maybe this part is not finished?

summary

This article focuses on the analysis of the plug-in Activity start process, including other topics, resources, not detailed analysis, because it is a little too much to say the details of the content, mainly to let people read the code, have a general direction. Have a question welcome to discuss yo ~~

Thanks for reading!