手把手帶你實現最簡單的插件化[二](Activity)

背景

上文在手把手帶你實現最簡單的插件化[一]咱們實現了最簡單的插件化,也介紹了插件化實現過程當中須要用到的知識點,最後咱們實現了從app中加載sd卡中的dex文件,調用dex中Test類的方法。今天咱們將實現Activity的插件化,Activity是須要在清單文件中註冊的,插件中的Activity沒有在宿主的清單文件中註冊,那麼咱們如何啓動它呢?java

Activity的插件化思路

Activity是四大組件用的最頻繁的組件,Activity的插件化也是各大插件化框架必須實現的功能。Activity插件化與Activity的啓動有着密切的關係。android

Activity的啓動過程須要由應用進程與AMS共同完成,當要啓動一個Activity時,應用進程會向AMS發起請求,AMS收到這個包含要啓動的Activity信息的請求後會進行一些列的處理以及權限校驗,處理校驗完成後回調到應用進程,由Activity所屬的應用進程完成Activity的啓動。數組

所以現有的插件化框架都會有一套越過AndroidMainfest.xml註冊而啓動Activity的機制,本文就帶大家實現和分析這一套機制性能優化

由於AMS會進行Activity的處理和權限校驗(是否註冊),處理校驗完會回到應用進程,由Activity所屬的應用進程完成Activity的啓動。那麼思路就來了,咱們能夠在宿主App中建立一個ProxyActivity繼承自Activity,而且在清單中註冊,當啓動插件中的Activity的時候,在系統檢測前,找到一個Hook點,而後經過Hook將插件Activity替換成ProxyActivity,等到AMS檢測完以後,再找一個Hook點將它們換回來,這樣就實現了插件Activity的啓動。思路雖然簡單,可是須要熟悉Activity啓動流程,動態代理,反射,Handler等原理,因此其實並不簡單,須要很深的功力markdown

先來看一下Activity的啓動流程app

經過這張圖咱們能夠肯定Hook點的大體位置。框架

  1. 在進入AMS以前,找到一個Hook點,用來將插件Activity替換爲ProxyActivity。
  2. 從AMS出來後,再找一個Hook點,用來將ProxyActivity替換爲插件Activity。

Hook Activity啓動入口

咱們在啓動Activity通常經過Intent包裝後調用startActivity來啓動,咱們能夠在AMS檢查以前將Intent中的要啓動的Activity替換爲咱們本地已經註冊過的ProxyActivity,同時把咱們要啓動的插件Activity保存在Intent中,而後在通過AMS校驗以後,再把Intent中的ProxyActivity再替換爲插件中的Activity並啓動,也就是說可以修改Intent的地方就能夠做爲Hook點ide

這裏要強調一下,查找Hook點應該儘可能找靜態變量或者單例對象,儘可能Hook public的對象和方法。爲何呢?由於靜態變量好獲取,不容易被改變,並且靜態變量只要找一個,不是靜態變量就可能有多個對象,須要進一步的判斷;爲何要找public方法呢,由於private方法可能被內部調用,影響該類的多個方法,固然這不是主要緣由(public也有可能),主要是public是提供給外部使用的,通常是不容易改變。oop

下面咱們進入源碼post

//Activity.java
@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 {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
						···
    }
複製代碼

而後咱們進入Instrumentation的execStartActivity方法中

public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
		···
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
      	//這裏就是咱們的Hook點,替換傳入startActivity方法中的Intent參數
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}
複製代碼

爲何就能直接看出這是個Hook點呢,由於ActivityManager.getService().startActivity這個調用中含有參數Intent,同時getService()方法是一個靜態public方法,方便hook

public static IActivityManager getService() {
    return IActivityManagerSingleton.get();
}
複製代碼

找到該Hook點,經過動態代理(IActivityManager是個接口),咱們要生成一個代理對象,咱們要代理的是ActivityManager.getService()返回的對象,同時替換它的參數Intent

//建立動態代理對象
Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");

Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
        new Class[]{iActivityManagerClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] args) throws Throwable {
                // do something
                // Intent的修改 -- 過濾
                /** * IActivityManager類的方法 * startActivity(whoThread, who.getBasePackageName(), intent, * intent.resolveTypeIfNeeded(who.getContentResolver()), * token, target != null ? target.mEmbeddedID : null, * requestCode, 0, null, options) */
                //過濾
                if ("startActivity".equals(method.getName())) {
                    int index = -1;
                    //獲取Intent參數在args數組中的index值
                    for (int i = 0; i < args.length; i++) {
                        if (args[i] instanceof Intent) {
                            index = i;
                            break;
                        }
                    }
                    //獲得原始的Intent對象
                    Intent intent = (Intent) args[index];

                    //生成代理proxyIntent
                    Intent proxyIntent = new Intent();
                    proxyIntent.setClassName("com.jackie.plugingodemo", ProxyActivity.class.getName());
                    //保存原始的Intent對象
                    proxyIntent.putExtra(TARGET_INTENT, intent);
                    //使用proxyIntent替換數組中的Intent
                    args[index] = proxyIntent;
                }

                //args method須要的參數 --不改變原有的執行流程
                //mInstance 系統的IActivityManager對象
                return method.invoke(mInstance, args);
            }
        });
複製代碼

接着咱們再使用反射將系統中的IActivityManager對象替換爲咱們的代理對象proxyInstance,如何替換?咱們來看一下源碼。

//ActivityManager.class
    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };
複製代碼

再來看看SingleTon的源碼

public abstract class Singleton<T> {
    private T mInstance;

    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}
複製代碼

能夠看到,IActivityManagerSingleton.get()實際上返回的就是mInstance對象,接下來咱們要替換的就是這個對象,代碼以下:

//獲取Singleton<T>對象
Field singletonField = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { //8.0
    Class<?> clazz = Class.forName("android.app.ActivityManagerNative");
    singletonField = clazz.getDeclaredField("gDefault");
} else {
    Class<?> clazz = Class.forName("android.app.ActivityManager");
    singletonField = clazz.getDeclaredField("IActivityManagerSingleton");
}
singletonField.setAccessible(true);
Object singleton = singletonField.get(null); //靜態的能夠直接獲取,傳入null

//獲取mInstance對象,mInstance是非靜態的,mInstance對象是系統的IActivityManager對象,也就是ActivityManager.getService()
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);

final Object mInstance = mInstanceField.get(singleton);


//建立動態代理對象
···
//替換
mInstanceField.set(singleton, proxyInstance);
複製代碼

到此爲止,咱們的第一次Hook就已經實現了,下面咱們來看第二次Hook點。

Hook Activity啓動出口

從前面的那張圖咱們能夠看到在出來的時候,會調用H(handler)的handleMessage方法,在handleMessage方法中(注意這裏是android 7.0,和8.0/9.0的源碼不一樣)

public void handleMessage(Message msg) {
1452            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
1453            switch (msg.what) {
1454                case LAUNCH_ACTIVITY: {
1455                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
1456                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
1457
1458                    r.packageInfo = getPackageInfoNoCheck(
1459                            r.activityInfo.applicationInfo, r.compatInfo);
1460                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
1461                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1462                } break;
複製代碼

在這裏咱們並無看到咱們的Intent,繼續玩下看handleLaunchActivity

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
2688        // If we are getting ready to gc after going to the background, well
2689        // we are back active so skip it.
2690        unscheduleGcIdler();
2691        mSomeActivitiesChanged = true;
2692
2693        if (r.profilerInfo != null) {
2694            mProfiler.setProfiler(r.profilerInfo);
2695            mProfiler.startProfiling();
2696        }
2697
2698        // Make sure we are running with the most recent config.
2699        handleConfigurationChanged(null, null);
2700
2701        if (localLOGV) Slog.v(
2702            TAG, "Handling launch of " + r);
2703
2704        // Initialize before creating the activity
2705        WindowManagerGlobal.initialize();
2706
2707        Activity a = performLaunchActivity(r, customIntent);
  					···
複製代碼

注意這個方法的參數customIntent並非咱們想要的Intent,由於上面該參數傳的是null。繼續看performLaunchActivity

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
2515        // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
2516
2517        ActivityInfo aInfo = r.activityInfo;
2518        if (r.packageInfo == null) {
2519            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
2520                    Context.CONTEXT_INCLUDE_CODE);
2521        }
2522
2523        ComponentName component = r.intent.getComponent();
2524        if (component == null) {
2525            component = r.intent.resolveActivity(
2526                mInitialApplication.getPackageManager());
2527            r.intent.setComponent(component);
2528        }
複製代碼

能夠看到該方法中有(ActivityClientRecord)r.intent方法,注意,不是說有看到Intent的能夠Hook,也要看這個intent所屬的是什麼對象,也就是說你要熟悉系統中的一些常見類,ActivityRecord和ActivityClientRecord都是保存Activity信息的對象。只不過,ActivityRecord歸system_server進程使用,ActivityClientRecord歸App進程使用

因此這裏能夠對ActivityClientRecord的intent進行hook,ActivityClientRecord方法中的intent(非靜態)

static final class ActivityClientRecord {
310        IBinder token;
311        int ident;
312        Intent intent;
複製代碼

要獲取非靜態的intent,首先咱們要獲取ActivityClientRecord對象,那麼若是獲取該對象呢?倒推回去,performLaunchActivity被handleLaunchActivity調用,而後handleLaunchActivity在處理LAUNCH_ACTIVITY消息時被調用

final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
複製代碼

能夠看到,咱們的這個r(ActivityClientRecord)其實是個msg.obj,也就是說能拿到msg(Message)就能夠拿到r對象了,那怎麼拿到msg呢,也就是咱們上面說的mCallback,將mCallback做爲hook點,替換或建立整個mCallback,咱們就能夠拿到該消息了

下面咱們來看一下Handler的源碼:

/** * Handle system messages here. */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
複製代碼

若是不瞭解Handler的源碼,能夠看看我以前寫的文章Handler的初級、中級、高級問法,你都掌握了嗎?,消息的發送最終會調用dispatchMessage方法,而後分發給handleMessage方法(若是該方法有被調用的話),仔細看該方法,若是Handle.mCallback不爲空的話,會首先執行mCallback.handleMessage(msg)方法,同時只有在mCallback.handleMessage(msg)返回爲false的時候,纔會繼續執行下面的handleMessage方法,這個很是重要。咱們再來看系統的H(Handler)

//ActivityThread.java
final H mH = new H();

//Handler.java
113    public Handler() {  //第一個參數是callback
114        this(null, false);
115    }
複製代碼

也就是說系統的這個Handler在傳callback參數時是空的,沒有Callback,那麼咱們須要本身建立一個Callback

// 建立的 callback
Handler.Callback callback = new Handler.Callback() {

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        // 經過msg 能夠拿到 Intent,能夠換回執行插件的Intent

        // 找到 Intent的方便替換的地方 --- 在這個類裏面 ActivityClientRecord --- Intent intent 非靜態
        // msg.obj == ActivityClientRecord
        switch (msg.what) {
            case 100:
                try {
                    Field intentField = msg.obj.getClass().getDeclaredField("intent");
                    intentField.setAccessible(true);
                    // 啓動代理Intent
                    Intent proxyIntent = (Intent) intentField.get(msg.obj);
                    // 啓動插件的 Intent
                    Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                    if (intent != null) {
                        intentField.set(msg.obj, intent);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            case 159:
                try {
                    // 獲取 mActivityCallbacks 對象
                    Field mActivityCallbacksField = msg.obj.getClass()
                            .getDeclaredField("mActivityCallbacks");

                    mActivityCallbacksField.setAccessible(true);
                    List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);

                    for (int i = 0; i < mActivityCallbacks.size(); i++) {
                        if (mActivityCallbacks.get(i).getClass().getName()
                                .equals("android.app.servertransaction.LaunchActivityItem")) {
                            Object launchActivityItem = mActivityCallbacks.get(i);

                            // 獲取啓動代理的 Intent
                            Field mIntentField = launchActivityItem.getClass()
                                    .getDeclaredField("mIntent");
                            mIntentField.setAccessible(true);
                            Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);

                            // 目標 intent 替換 proxyIntent
                            Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                            if (intent != null) {
                                mIntentField.set(launchActivityItem, intent);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
        }
        // 必須 return false
        return false;
    }
};
複製代碼

同時在該方法中從Intent中拿出插件的Activity,最終啓動該Activity。而後經過反射給系統的H(Handler)設置一個Callback

// 獲取 ActivityThread 類的 Class 對象
            Class<?> clazz = Class.forName("android.app.ActivityThread");

            // 獲取 ActivityThread 對象
            Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
            activityThreadField.setAccessible(true);
            Object activityThread = activityThreadField.get(null);

            // 獲取 mH 對象
            Field mHField = clazz.getDeclaredField("mH");
            mHField.setAccessible(true);
            final Handler mH = (Handler) mHField.get(activityThread);

            Field mCallbackField = Handler.class.getDeclaredField("mCallback");
            mCallbackField.setAccessible(true);

            // 建立的 callback
						···
            // 替換系統的 callBack
            mCallbackField.set(mH, callback);
複製代碼

到這來咱們就實現了Activity的插件化,固然Hook點不止這些,有興趣的讀者能夠本身尋找,同時在不一樣版本上源碼的實現方式也不一樣,須要進行適配。在Android10上,系統對源碼作了較大的修改,有興趣的能夠本身實現一波。

最後你可能會碰到這麼一個異常

2020-11-29 12:27:33.247 19124-19124/com.jackie.plugingodemo D/AppCompatDelegate: Exception while getting ActivityInfo
    android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{com.jackie.plugingodemo/com.jackie.plugin.PluginActivity}
        at android.app.ApplicationPackageManager.getActivityInfo(ApplicationPackageManager.java:435)
        at androidx.appcompat.app.AppCompatDelegateImpl.isActivityManifestHandlingUiMode(AppCompatDelegateImpl.java:2649)
        at androidx.appcompat.app.AppCompatDelegateImpl.updateForNightMode(AppCompatDelegateImpl.java:2499)
        at androidx.appcompat.app.AppCompatDelegateImpl.applyDayNight(AppCompatDelegateImpl.java:2374)
        at androidx.appcompat.app.AppCompatDelegateImpl.onCreate(AppCompatDelegateImpl.java:494)
        at androidx.appcompat.app.AppCompatActivity.onCreate(AppCompatActivity.java:114)
        at com.jackie.plugin.PluginActivity.onCreate(PluginActivity.java:12)
        at android.app.Activity.performCreate(Activity.java:7136)
        at android.app.Activity.performCreate(Activity.java:7127)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
複製代碼

該異常提示咱們找不到ActivityInfo,而後咱們到拋出異常的方法看一下

private boolean isActivityManifestHandlingUiMode() {
    if (!mActivityHandlesUiModeChecked && mHost instanceof Activity) {
        final PackageManager pm = mContext.getPackageManager();
        if (pm == null) {
            // If we don't have a PackageManager, return false. Don't set
            // the checked flag though so we still check again later
            return false;
        }
        try {
            int flags = 0;
            // On newer versions of the OS we need to pass direct boot
            // flags so that getActivityInfo doesn't crash under strict
            // mode checks
            if (Build.VERSION.SDK_INT >= 29) {
                flags = PackageManager.MATCH_DIRECT_BOOT_AUTO
                        | PackageManager.MATCH_DIRECT_BOOT_AWARE
                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
            } else if (Build.VERSION.SDK_INT >= 24) {
                flags = PackageManager.MATCH_DIRECT_BOOT_AWARE
                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
            }
            final ActivityInfo info = pm.getActivityInfo(
                    new ComponentName(mContext, mHost.getClass()), flags);
            mActivityHandlesUiMode = info != null
                    && (info.configChanges & ActivityInfo.CONFIG_UI_MODE) != 0;
        } catch (PackageManager.NameNotFoundException e) {
            // This shouldn't happen but let's not crash because of it, we'll just log and
            // return false (since most apps won't be handling it)
            Log.d(TAG, "Exception while getting ActivityInfo", e);
            mActivityHandlesUiMode = false;
        }
    }
    // Flip the checked flag so we don't check again
    mActivityHandlesUiModeChecked = true;

    return mActivityHandlesUiMode;
}
複製代碼

而後進如pm.getActivityInfo方法

/** * Retrieve all of the information we know about a particular activity * class. * * @param component The full component name (i.e. * com.google.apps.contacts/com.google.apps.contacts. * ContactsList) of an Activity class. * @param flags Additional option flags to modify the data returned. * @return An {@link ActivityInfo} containing information about the * activity. * @throws NameNotFoundException if a package with the given name cannot be * found on the system. */
public abstract ActivityInfo getActivityInfo(ComponentName component, @ComponentInfoFlags int flags) throws NameNotFoundException;
複製代碼

經過查看該方法註釋,能夠看到當找不到該Activity的包名,也就是在系統中找不到,就會拋出異常了,由於咱們的插件包名和宿主App的包名不一致致使的,不過系統也爲咱們捕獲了該異常了。

咱們爲何要學插件化

Activity插件化的實現很重要的一點是尋找Hook點,如何尋找Hook點須要咱們對Activity啓動流程很是熟悉。插件化涉及到的技術其實不少,四大組件的啓動流程,AMS/PKMS等系統服務啓動流程,Handler,反射,動態代理等等,裏面運用到不少Android自身的知識,而不只僅是Java的知識,有點像Android技術的「集大成者」。因此若是你想成爲一個高級開發,就應該懂得像插件化,熱修復這樣的技術難點。最後,看完本文喜歡的點個贊和關注吧。

近期的一些文章:

手把手帶你實現最簡單的插件化[一]

Activity的初級,中級,高級問法,你都掌握了嗎?

Handler的初級、中級、高級問法,你都掌握了嗎?(深度好文)

性能優化:爲何要使用SparseArray和ArrayMap替代HashMap?

參考文章:

zhuanlan.zhihu.com/p/109157321

相關文章
相關標籤/搜索