上一篇文章裡面我們分析了一下Activity外掛化並提出了5個問題,然後有的問題給出瞭解決方案,有的問題沒有給出解決方案,不用擔心,會有一系列的文章來循序漸進的把Activity外掛化過程中遇到的問題慢慢講清楚。
本文程式碼:PluginDemo/activity_plugin
這篇文章只講一個問題,Activity外掛化佔坑實現方式
後續文章將要講到的,現階段不講的:
1 . 外部apk的解析
2 . ClassLoader問題
3 . 資原始檔問題
1.最簡單Activity完全外掛化實現方式
瞭解Activity的啟動過程就應該知道啟動Activity呼叫的是Instrumentation
的execStartActivity
方法完成的。等到AMS完成校驗,以及在需要的時候建立程式等等一系列的操作之後會回到App程式,最後依舊呼叫Instrumentation
的另外一個方法newActivity
。所以我們Hook掉ActivityThread
的Instrumentation
的例項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
,大概如下:
attachBaseContext
方法。
Manifest是這樣的,啟動Activity的程式碼就和平時一樣的啦。
效果如下:
是不是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