【Android插件化】啓動沒有在Manifest中註冊的Activity

1 概述

如果要啓動沒有在Manifest中註冊的Activity,應該從startActivity着手。一般啓動Activity的方式有兩種,一種是startActivity,一種是startActivityForResult。其實startActivity最終調用的也是startActivityForResult,如下所示:

    //Activity.java
    @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

當我們啓動一個Activity時,AMS會對這個Activity是否在AndroidManifest中聲明註冊進行檢查,如果沒有註冊,則會報錯。要想啓動沒有在AndroidManifest中註冊的Activity,則需要“欺騙AMS”,讓其以爲我們已經在AndroidManifest中註冊了。

基本思路是:

  1. 發送要啓動的Activity信息給AMS之前,把這個Activity替換爲一個在AndroidManifest中聲明的StubActivity,這樣就能繞過AMS的檢查,在替換過程中,要把原來的Activity信息保存起來。
  2. AMS通知App啓動StubActivity時,我們再將保存的Activity替換StubActivity,這樣就能啓動沒有在AndroidManifest中註冊的Activity。

修改Activity啓動流程如圖所示(圖片來自網絡)

image.png

2 代碼實現

AndroidManifest.xml

    <application
        android:name=".MainApplication"
        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">
        <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=".SubActivity"/>
    </application>

一共兩個Activity,分別是MainActivity和SubActivity,一個MainApplication,SubActivity是一個殼Activity,用於欺騙AMS的。

MainActivity.java

    private View.OnClickListener mListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent();
            intent.setClass(MainActivity.this, PluginTestActivity.class);
            startActivity(intent);
        }
    };

PluginTestActivity並沒有在AndroidManifest中註冊,正常情況下會報錯,所以我們需要欺騙AMS。

欺騙AMS的方式有幾種,這裏我們選擇對ActivityThread的mInstrumentation進行hook。代碼如下:

public class HookHelper {
    public static final String PLUGIN_INTENT = "plugin_intent";
    public static void hookInstrumentation(Context context,String className) throws Exception{
        //獲取ActivityThread的字節碼對象
        Class<?> clazz = Class.forName("android.app.ActivityThread");
        //獲取ActivityThread中的sCurrentActivityThread字段
        Field sCurrentActivityThreadField = ReflectUtils.getField(clazz,"sCurrentActivityThread");
        //獲取ActivityThread中的mInstrumentation字段
        Field mInstrumentationField = ReflectUtils.getField(clazz,"mInstrumentation");
        //獲取ActivityThread中的sCurrentActivityThread的值並賦值給currentActivityThread
        Object currentActivityThread = sCurrentActivityThreadField.get(clazz);
        //獲取ActivityThread中的mInstrumentation的值並賦值給instrumentation
        Instrumentation instrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
        //創建一個PackageManager對象packageManager
        PackageManager packageManager = context.getPackageManager();
        //創建InstrumentationProxy對象,InstrumentationProxy是繼承Instrumentation的類,下面詳細介紹
        InstrumentationProxy instrumentationProxy = new InstrumentationProxy(instrumentation,packageManager,className);
        //將ActivityThread中的mInstrumentation賦值爲instrumentationProxy
        ReflectUtils.setFieldObject(clazz,currentActivityThread,"mInstrumentation",instrumentationProxy);
    }
}

InstrumentationProxy類如下

package com.example.plugintest;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.IBinder;
import android.text.TextUtils;

import java.lang.reflect.Method;
import java.util.List;

/**
 * Created by yds
 * on 2020/2/14.
 */
public class InstrumentationProxy extends Instrumentation{
    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;
    private String className;

    public InstrumentationProxy(Instrumentation mInstrumentation, PackageManager mPackageManager, String className) {
        this.mInstrumentation = mInstrumentation;
        this.mPackageManager = mPackageManager;
        this.className = className;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        if (infos == null || infos.size() == 0) {
            intent.putExtra(HookHelper.PLUGIN_INTENT,intent.getComponent().getClassName());
            intent.setClassName(who,className);
        }
        try {
            Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class,IBinder.class,IBinder.class,Activity.class,Intent.class,int.class,Bundle.class);
            return (ActivityResult) execMethod.invoke(mInstrumentation,who,contextThread,token,target,intent,requestCode,options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

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

只要將InstrumentationProxy賦值給ActivityThread的mInstrumentation,調用startActivity時,最終會調用InstrumentationProxy的execStartActivity和newActivity。我們只要在execStartActivity中將未註冊的Activity保存起來,並使用已註冊的SubActivity,然後在newActivity中將保存起來未註冊的Activity替換SubActivity就可以了。

源碼地址:https://github.com/Yedongsheng/PluginTest01

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章