Activity的外掛化(二)

渣渣008發表於2017-12-13

上一篇文章裡面我們分析了一下Activity外掛化並提出了5個問題,然後有的問題給出瞭解決方案,有的問題沒有給出解決方案,不用擔心,會有一系列的文章來循序漸進的把Activity外掛化過程中遇到的問題慢慢講清楚。

本文程式碼:PluginDemo/activity_plugin

這篇文章只講一個問題,Activity外掛化佔坑實現方式

後續文章將要講到的,現階段不講的:

1 . 外部apk的解析

2 . ClassLoader問題

3 . 資原始檔問題

1.最簡單Activity完全外掛化實現方式

瞭解Activity的啟動過程就應該知道啟動Activity呼叫的是InstrumentationexecStartActivity方法完成的。等到AMS完成校驗,以及在需要的時候建立程式等等一系列的操作之後會回到App程式,最後依舊呼叫Instrumentation的另外一個方法newActivity。所以我們Hook掉ActivityThreadInstrumentation的例項mInstrumentation即可。

public class HookManager {

    private static volatile HookManager sManager;

    private HookManager() {
    }

    public static HookManager getManager() {
        if (sManager == null) {
            synchronized (HookManager.class) {
                if (sManager == null) {
                    sManager = new HookManager();
                }
            }
        }
        return sManager;
    }

    /**
    * {@link android.app.ActivityThread} Class
    */
    private Class<?> mActivityThreadClass = null;
    /**
    * {@link android.app.ActivityThread}物件
    */
    private Object mActivityThread = null;
    /**
    * {@link Instrumentation}物件
    */
    private Instrumentation mInstrumentation = null;
    /**
    * {@link Instrumentation} Field
    */
    private Field mInstrumentationField = null;

    /**
    * 獲取ActivityThread Class物件
    *
    * @return
    */
    public Class<?> getActivityThreadClass() {
        if (mActivityThreadClass != null) {
            return mActivityThreadClass;
        }
        try {
            mActivityThreadClass = Class.forName("android.app.ActivityThread");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return mActivityThreadClass;
    }

    /**
    * 獲取ActivityThread
    *
    * @return
    */
    public Object getActivityThread() {
        if (mActivityThread != null) {
            return mActivityThread;
        }
        Class<?> activityThreadClass = getActivityThreadClass();
        if (activityThreadClass != null) {
            try {
                // 通過反射呼叫 ActivityThread 的靜態方法, 獲取 currentActivityThread
                Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
                currentActivityThreadMethod.setAccessible(true);
                mActivityThread = currentActivityThreadMethod.invoke(null);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return mActivityThread;
    }

    /**
    * 獲取 Instrumentation 的 Field
    *
    * @return
    */
    public Field getInstrumentationField() {
        if (mInstrumentationField != null) {
            return mInstrumentationField;
        }
        Class<?> activityThreadClass = getActivityThreadClass();
        if (activityThreadClass != null) {
            // 拿到原始的 mInstrumentation欄位
            if (mInstrumentationField == null) {
                try {
                    mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
            }
        }
        return mInstrumentationField;
    }

    /**
    * 獲取Instrumentation
    *
    * @return
    */
    public Instrumentation getInstrumentation() {
        if (mInstrumentation != null) {
            return mInstrumentation;
        }
        Class<?> activityThreadClass = getActivityThreadClass();
        if (activityThreadClass != null) {
            try {
                if (getInstrumentationField() != null) {
                    // 拿到原始的 mInstrumentation 欄位
                    getInstrumentationField().setAccessible(true);
                    if (getActivityThread() != null) {
                        mInstrumentation = (Instrumentation) getInstrumentationField().get(getActivityThread());
                    }
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return mInstrumentation;
    }

    /**
    * 替換{@link android.app.ActivityThread#mInstrumentation}物件為指定物件
    */
    public void replaceInstrumentation() {
        if (getInstrumentation() != null && !(getInstrumentation() instanceof PluginInstrumentation)) {
            PluginInstrumentation instrumentation = new PluginInstrumentation(getInstrumentation());
            if (getInstrumentationField() != null && getActivityThread() != null) {
                // 拿到原始的 mInstrumentation 欄位
                getInstrumentationField().setAccessible(true);
                try {
                    getInstrumentationField().set(getActivityThread(), instrumentation);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製程式碼

一個類就搞定了,這裡我們就把整個App的Instrumentation物件換成了我們計己的PluginInstrumentation啦。

public class PluginInstrumentation extends Instrumentation {

    private Instrumentation mOrigin;

    private static final String RESOURCES_PACKAGE_NAME = "com.example.activityplugin";

    public PluginInstrumentation(Instrumentation origin) {
        this.mOrigin = origin;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        // 由於這個方法是隱藏的,因此需要使用反射呼叫;首先找到這個方法
        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity", Context.class, IBinder.class, IBinder.class,
                    Activity.class, Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            // TODO: 2017/10/27 這裡把intent裡面的class替換成Manifest裡面註冊的
            reWarpIntent(who, intent);
            return (ActivityResult) execStartActivity.invoke(mOrigin,
                    who, contextThread, token, target, intent, requestCode, options);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
    * 對Intent做一些修改
    *
    * @param context
    * @param intent
    */
    private void reWarpIntent(Context context, Intent intent) {
        // TODO: 2017/10/27 判斷
        if (!intent.getComponent().getClassName().contains("MainActivity")) {
            intent.setClassName(context.getPackageName(), RESOURCES_PACKAGE_NAME + ".A$1");
        }
    }

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        // TODO: 2017/10/27 替換className為真正需要啟動的class
        String targetClass = className;
        if (!className.contains("MainActivity")) {
            targetClass = RESOURCES_PACKAGE_NAME + ".PluginSampleActivity";
        }
        return mOrigin.newActivity(cl, targetClass, intent);
    }

}
複製程式碼

這裡說了是最簡單的Demo,只是實現了可以使用預註冊的Activity代替真實的Activity的目的。

這裡我的場景是這樣的,定義了一個普通的PluginSampleActivity,然後在主介面啟動,然後我們不在Manifest裡面註冊這個,使用預註冊好的一個Activity,大概如下:

81755721.png
當然第一步是Hook,我們需要在一個儘可能早的時機Hook。所以我們選擇Application的attachBaseContext方法。
82038718.png

Manifest是這樣的,啟動Activity的程式碼就和平時一樣的啦。

82107540.png

效果如下:

3867f4d8-f689-486b-ab49-401ae3c89ffe.gif
是不是so easy的。

2.Manifest 預留 Activity 佔坑說明

先看看現階段各大外掛化框架是怎樣佔坑滴。 DroidPlugin Android-Plugin-Framework VirtualAPK Small

1.1.standard

對於standard模式的Activity可以註冊一個即可,因為這個模式的Activity每次啟動都會生成新的Activity的例項。所以stub並不需要真實存在,只是佔個位置,standard的launchmode只需全透明和非全透明各註冊1個即可。如果在實際中遇到特別的需求可以再調整的。

當然這個涉及到程式的問題,程式的問題在我看來是這樣的,一般的應用外掛化開發涉及不到程式的問題,或者說你的外掛全都執行在一個新的程式,如果有需要支援的話,可以後續增加,沒有必要一開始就整的那麼的全。

<activity android:name=".A$1" android:launchMode="standard"/>
<activity android:name=".A$2" android:launchMode="standard"
    android:theme="@android:style/Theme.Translucent" />
複製程式碼
1.2.singleTop

需要註冊的stub數量只需 >= 可能 同時 處於執行狀態的 singleTop 模式的Activity的數量,最糟糕的情況是所有的 singleTop 模式的Activity都 同時 處於執行狀態,那麼這種情況下 需要註冊的stub數量即為所有外掛所有 singleTop 模式的Activity的總和。一般情況,我們應用設計的話應該不會同時有那麼多的 singleTop 型別的Activity同時執行的。這裡只預註冊四個,如果需要更多可自行判斷。

<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"/>
複製程式碼
1.3.singleTask

需要註冊的stub數量只需 >= 可能 同時 處於執行狀態的 singleTask 模式的Activity的數量,最糟糕的情況是所有的 singleTask 模式的Activity都 同時 處於執行狀態,那麼這種情況下 需要註冊的stub數量即為所有外掛所有 singleTask 模式的Activity的總和。一般情況,我們應用設計的話應該不會同時有那麼多的 singleTask 型別的Activity同時執行的。這裡只預註冊四個,如果需要更多可自行判斷。

<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"/>
複製程式碼
1.4.singleInstance

需要註冊的stub數量只需 >= 可能 同時 處於執行狀態的 singleInstance 模式的Activity的數量,最糟糕的情況是所有的 singleInstance 模式的Activity都 同時 處於執行狀態,那麼這種情況下 需要註冊的stub數量即為所有外掛所有 singleInstance 模式的Activity的總和。一般情況,我們應用設計的話應該不會同時有那麼多的 singleInstance 型別的Activity同時執行的。這裡只預註冊四個,如果需要更多可自行判斷。

<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"/>
複製程式碼

3.啟動 Activity Intent解析

Intent物件裡面有一個ComponentName物件,這個用來描述一個元件的資訊,可以用來描述四大元件,裡面有兩個物件。

private final String mPackage;
private final String mClass;
複製程式碼

其中mPackage表示包名,mClass表示要啟動的物件的類名,一般我們啟動一個Activity,如果不是隱式的啟動,那麼Intent裡面的ComponentName物件都是不為空的,所以針對隱式的啟動需要做一些處理,但是這個處理不一定是完全的,因為通過action這種隱式啟動,很隨意,如果使用者自定義了action,那麼也很難處理的。

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    // 隱式Intent的轉換
    mHookManager.getComponentResolver().implicitToExplicit(intent);
    // Intent的重新解析
    if (intent.getComponent() != null) {
        mHookManager.getComponentResolver().reMakeIntent(intent);
    }
    // 由於這個方法是隱藏的,因此需要使用反射呼叫,首先找到這個方法
    try {
        Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                "execStartActivity", Context.class, IBinder.class, IBinder.class,
                Activity.class, Intent.class, int.class, Bundle.class);
        execStartActivity.setAccessible(true);
        return (ActivityResult) execStartActivity.invoke(mOrigin,
                who, contextThread, token, target, intent, requestCode, options);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

複製程式碼

我們要在啟動Activity的時候替換找到合適的預註冊的Activity用來替換外掛中的,當然這裡只是模擬,啟動的和真實的Activity在同一個apk裡面。然後我們來想想reMakeIntent方法的實現,我們需要根據類名,包名(對應的外掛包),啟動模式來確定一個外掛Activity,這裡就不涉及到包名,因為是在同一個裡面的。

/**
    * Intent重新解析,可能需要加上標記,標記是一個需要替換的Activity
    *
    * @param intent
    */
public void reMakeIntent(Intent intent) {
    if (intent.getComponent() == null) {
        return;
    }
    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();
    // 包名不是宿主的,並且能在外掛列表找到對應包名的外掛就打上是外掛Activity的標記
    // 2017/11/7 外掛Activity判斷,這裡做最簡單的判斷,如果不是主介面就認為是外掛Activity
    if (!targetClassName.contains("MainActivity")) {
        intent.putExtra(Constants.KEY_IS_PLUGIN, true);
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    }
}

/**
    * 找出最合適的可以替換的Activity
    *
    * @param intent
    */
private void dispatchStubActivity(Intent intent) {
    PackageManager pm = mContext.getPackageManager();
    ComponentName component = intent.getComponent();
    if (component == null) {
        return;
    }
    try {
        String targetClassName = component.getClassName();
        ActivityInfo activityInfo = pm.getActivityInfo(intent.getComponent(), 0);
        int launchMode = activityInfo.launchMode;
        String stubActivity = getStubActivity(targetClassName, launchMode);
        if (!TextUtils.isEmpty(stubActivity)) {
            Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
            intent.setClassName(mContext, stubActivity);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複製程式碼

首先我們對是外掛的Activity打上標記,表示是外掛Activity,這裡註釋裡面寫的比較清楚,然後根據啟動模式找到對應的Activity。

/**
 * 獲取對應的stub Activity,這裡用了一種很巧妙的處理方式,框架一般認為不存在說超過8個特殊
 * 啟動模式的Activity在執行,所以這裡使用了%8的方式,8個用完就又從第一個開始
 *
 * @param className  要啟動的origin Activity
 * @param launchMode 啟動模式
 * @param theme      主題,需要靠這個來判斷是不是透明的
 * @return 合適的stub Activity
 */
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();
    Log.i("StubActivityInfo", "getStubActivity, is transparent theme : " + windowIsTranslucent);
    stubActivity = format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
    switch (launchMode) {
        case ActivityInfo.LAUNCH_MULTIPLE: {
            stubActivity = format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
            if (windowIsTranslucent) {
                stubActivity = format(STUB_ACTIVITY_STANDARD, corePackage, 2);
            }
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_TOP: {
            usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
            stubActivity = format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_TASK: {
            usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
            stubActivity = format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
            break;
        }
        case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
            usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
            stubActivity = format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
            break;
        }
        default:
            break;
    }
    mCachedStubActivity.put(className, stubActivity);
    return stubActivity;
}

複製程式碼

這個類其實很簡單,就是根據啟動模式,來匹配預註冊的Activity,但是有一個比較巧妙的地方,註釋裡面也說了,獲取對應的stub Activity,這裡用了一種很巧妙的處理方式,框架一般認為不存在說超過8個特殊啟動模式的Activity在執行,所以這裡使用了%8的方式,8個用完就又從第一個開始。

4.啟動真實Activity

啟動完成從AMS回到App的時候會呼叫newActivity方法,我們要在這個方法裡面去啟動真正要啟動的Activity。實現很簡單了,因為前面已經做了標記了的。

public Activity newActivity(ClassLoader cl, String className, Intent intent)
        throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    // 2017/10/27 替換className為真正需要啟動的class
    String realClass = intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY);
    if (!TextUtils.isEmpty(realClass)) {
        Log.i(TAG, String.format("newActivity[%s : %s]", className, realClass));
        Activity activity = mOrigin.newActivity(cl, realClass, intent);
        activity.setIntent(intent);
        return activity;
    }
    return mOrigin.newActivity(cl, className, intent);
}
複製程式碼

5.結束語

以上就是啟動Activity的Intent的匹配過程,相對來說比較簡單的。再次說明一下上面的一個比較巧妙的地方,我們認為一般App裡面不存在同時有超過我們預想個數的特殊啟動模式的Activity同時執行,如果你覺得你的App可能的話,可以增加這個數量即可,然後我們在啟動特殊的啟動模式的Activity的時候,使用這個最大的數當一個輪迴,使用完這個數目的Activity之後,又從第一個開始,這樣就很巧妙的解決了,我們需要去判斷,當前有多少個特殊模式的Activity在執行了。

本文程式碼PluginDemo

相關文章