Tips: It takes 15-20 minutes to read this article (a lot of code)


Today, we are going to solve a problem:

Activity plug-in principle of the first scheme: Hook Instrumentation

All the problems of life, knowledge gives you the answer.


One of the fundamental problems that the plug-in solves is that the Activity in the plug-in is not registered in the host’s Androidmanifest.xml. That is to say, we need to start an unregistered Activity, so we need to understand how the Activity starts.

When an Activity is started, AMS is asked to create the Activity. In this case, AMS refers to ActivityManagerService. AMS belongs to a different process than the host (initiator), and AMS is located in the SystemServer process.

The communication between application processes and AMS is implemented by Binder. AMS manages all APP startup requests, so we cannot Hook the application process in SystemServer.

If we start an unregistered Activity, AMS will check whether the Activity is registered in the AndroidManifest and will report an error if it is not registered.

In order for AMS to pass the verification, we need to start an Activity registered in the AndroidManifest in advance, which is called the trap Activity. When we start the plug-in Activity, we replace it with the trap Activity to achieve a function of deceiving the top and hiding the lower part. When AMS passes the verification, You need to replace the pit Activity you started with a plug-in Activity.

To sum up, the Activity plug-in needs to do two things:

  • Replace the plug-in Activity that requests to start with a pit Activity.
  • After bypassing AMS validation, replace the pit Activity with a plug-in Activity.

When to replace a plug-in Activity with a pit Activity? When is the plug-in Activity restored? This requires an understanding of the Activity startup process.

We call the startActivity method in the Activity as follows:

    @Override
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }
    
        @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if(options ! =null) {
           startActivityForResult(intent, -1, options);
        } else {
           startActivityForResult(intent, -1); }}Copy the code

Call the startActivityForResult method:

    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            / / the Activity start
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if(ar ! =null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
            windows.
        } else {
            if(options ! =null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                mParent.startActivityFromChild(this, intent, requestCode); }}}Copy the code

The startActivityForResult method starts the Activity by calling the execStartActivity method on the mInstrumentation, which is a member variable of the Activity, The ActivityThread is passed in to the Activity attach method, and the Activity is created in the performLaunchActivity method. Through mInstrumentation newActivity.

//:/frameworks/base/core/java/android/app/ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {...try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if(r.state ! =null) { r.state.setClassLoader(cl); }}... activity.attach(appContext,this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); . }Copy the code

In summary, Instrumentation provides the execStartActivity method to start an Activity and the newActivity method to create an Activity. Therefore, the first solution is to replace the Instrumentation of activities with proxy Instrumentation, and in the proxy Instrumentation execStartActivity method replace pit Activity, Restore the plug-in Activity in the newActivity method.

Now we are based on the first scheme Hook Instrumentation to implement the Activity of the plug-in.

First create a pit Activity:

public class StubActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_stub); }}Copy the code

Create a plug-in Activity:

public class TargetActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_target); }}Copy the code

And register the pit Activity in androidmanifest.xml:

<?xml version="1.0" encoding="utf-8"? >
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.glh.haiproject01">

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="AllowBackup,GoogleAppIndexingWarning">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".StubActivity" />
    </application>

</manifest>
Copy the code

The plugin Activity is not registered in androidmanifest.xml and an error is reported if the plugin Activity is started.

Finally Hook Instrumentation, replace the member variables in ActivityThread Instrumentation with the Instrumentation of the agent.

Create the proxy Instrumentation class:

public class InstrumentationProxy extends Instrumentation {

    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;

    public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
        this.mInstrumentation = instrumentation;
        this.mPackageManager = packageManager;
    }

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

        List<ResolveInfo> resolveInfo = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        // Check whether the Activity is registered in androidmanifest.xml
        if (null == resolveInfo || resolveInfo.size() == 0) {
            // Save the target plug-in
            intent.putExtra(HookHelper.REQUEST_TARGET_INTENT_NAME, intent.getComponent().getClassName());
            // Set it to trap Activity
            intent.setClassName(who, "com.glh.haiproject01.StubActivity");
        }

        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String intentName=intent.getStringExtra(HookHelper.REQUEST_TARGET_INTENT_NAME);
        if(! TextUtils.isEmpty(intentName)){return super.newActivity(cl,intentName,intent);
        }
        return super.newActivity(cl,className,intent); }}Copy the code

The execStartActivity method used by the InstrumentationProxy to determine whether the plug-in Activity is registered in the Androidmanifest.xml file. Restore the plug-in Activity in the newActivity method.

When the proxy class InstrumentationProxy is written, the mInstrumentation member variable of the ActivityThread needs to be replaced.

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        hookActivityThreadInstrumentation();
    }


    private void hookActivityThreadInstrumentation(a){
        try{ Class<? > activityThreadClass=Class.forName("android.app.ActivityThread");
            Field activityThreadField=activityThreadClass.getDeclaredField("sCurrentActivityThread");
            activityThreadField.setAccessible(true);
            // Obtain ActivityThread object sCurrentActivityThread
            Object activityThread=activityThreadField.get(null);

            Field instrumentationField=activityThreadClass.getDeclaredField("mInstrumentation");
            instrumentationField.setAccessible(true);
            // Obtain the member variable mInstrumentation from the sCurrentActivityThread
            Instrumentation instrumentation= (Instrumentation) instrumentationField.get(activityThread);
            // Create a proxy object InstrumentationProxy
            InstrumentationProxy proxy=new InstrumentationProxy(instrumentation,getPackageManager());
            // Replace the mInstrumentation member variable in the sCurrentActivityThread with the proxy class InstrumentationProxy
            instrumentationField.set(activityThread,proxy);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(ClassNotFoundException e) { e.printStackTrace(); }}}Copy the code

At this point, we click the jump plug-in Activity on the main screen:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_startActivity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent=new Intent(MainActivity.this,TargetActivity.class); startActivity(intent); }}); }}Copy the code

Operation effect: