最近把Activity啓動流程整體看了一遍,估摸着弄個啥來鞏固下,發現插件化正好是這塊技術的實踐,而說道插件化其實有好幾種實現方式,這裏我用的是hook的方式實現,主要目的呢是爲了對activity啓動流程有個整體的認識,當然了本篇的插件化也只是一個demo版本並沒有任何兼容適配,重在流程和原理的理解。
概述
插件化顧名思義,就是將一個APK拆成多個,當需要的時候下載對應插件APK加載的技術。本文demo中除了下載是通過adb命令,其他都是模擬真實環境的,這裏先理下流程。
- 將插件工程打包爲APK,然後通過adb push命令發送到宿主APK目錄(模擬下載流程)。
- 利用ClassLoader加載插件APK中的類文件。
- hook Activity啓動流程中部分類,利用佔坑Activity幫助PluginActivity繞過AMS驗證,在真正啓動的時候又替換回PluginActivity。
- 創建插件Apk的Resources對象,完成插件資源的加載。
對整體流程有個大概認識後,下面將結合源碼和Demo來詳細講解,本文貼出的源碼基於API27。
初始化插件APK類文件
既然插件APK是通過網絡下載下來的,那麼APK中的類文件就需要我們自己加載了,這裏我們要用到DexClassLoader去加載插件APK中的類文件,然後將DexClassLoader中的Element數組和宿主應用的PathClassLoader的Element數組合並再設置回PathClassLoader,完成插件APK中類的加載。對ClassLoader不太熟悉的可以看下我另篇Android ClassLoader淺析
public class InjectUtil {
private static final String TAG = "InjectUtil";
private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
private static final String FIELD_PATH_LIST = "pathList";
private static final String FIELD_DEX_ELEMENTS = "dexElements";
public static void inject(Context context, ClassLoader origin) throws Exception {
File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/Android/data/$packageName/files/plugin
if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0) {
Log.i(TAG, "插件文件不存在");
return;
}
pluginFile = pluginFile.listFiles()[0];//獲取插件apk文件
File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
if (!optimizeFile.exists()) {
optimizeFile.mkdirs();
}
DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到插件Elements
Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements
Object array = combineArray(originElements, pluginElements);//合併數組
FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//設置回PathClassLoader
Log.i(TAG, "插件文件加載成功");
}
private static Object combineArray(Object pathElements, Object dexElements) {//合併數組
Class<?> componentType = pathElements.getClass().getComponentType();
int i = Array.getLength(pathElements);
int j = Array.getLength(dexElements);
int k = i + j;
Object result = Array.newInstance(componentType, k);
System.arraycopy(dexElements, 0, result, 0, j);
System.arraycopy(pathElements, 0, result, j, i);
return result;
}
}
這裏我們約定將插件APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目錄,然後爲了儘早加載所以在Application中執行加載邏輯。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類文件
} catch (Exception e) {
e.printStackTrace();
}
}
}
Hook啓動流程
在說之前我們得先了解下Activity的啓動流程。
上圖抽象的給出了Acticity的啓動過程。在應用程序進程中的Activity向AMS請求創建Activity(步驟1),AMS會對這個Activty的生命週期棧進行管理,校驗Activity等等。如果Activity滿足AMS的校驗,AMS就會請求應用程序進程中的ActivityThread去創建並啓動Activity。
那麼在上一步我們已經將插件Apk的類文件加載進來了,但是我們並不能通過startActivity的方式去啓動PluginActivity,因爲PluginActivity並沒有在AndroidManifest中註冊過不了AMS的驗證,既然這樣我們換一個思路。
- 在宿主項目中提前弄一個SubActivity佔坑,在啓動PluginActivity的時候替換爲啓動這個SubActivity繞過驗證。
- 在AMS處理完相應驗證通知我們ActivityThread創建Activty的時候在替換爲PluginActivity。
佔坑SubActivity非常簡單
public class SubActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
然後在AndroidManifest註冊好即可
<activity android:name=".SubActivity"/>
對於startActivity()最終都會調到ActivityManagerService的startActivity()方法。
ActivityManager.getService()//獲取AMS
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
那麼我們可以通過動態代理hook ActivityManagerService,然後在startActivity()的時候將PluginActivity替換爲SubActivity,不過對於ActivityManagerService的獲取不同版本方式有所不同。
在Android7.0以下會調用ActivityManagerNative的getDefault方法獲取,如下所示。
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");//獲取ams
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);//拿到ams代理對象
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
getDefault()返回的是IActivityManager,而gDefault是一個單例對象Singleton並且是靜態的是非常容易用反射獲取。
Android8.0會調用ActivityManager的getService方法獲取,如下所示。
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);//拿到ams
final IActivityManager am = IActivityManager.Stub.asInterface(b);//拿到ams代理對象
return am;
}
};
返回一個IActivityManager,而IActivityManagerSingleton是一個單例對象Singleton並且是靜態非常容易獲取。
在看下上面提到的Singleton等會hook會用到
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;
}
}
}
到這裏會發現其實返回的都是AMS的接口IActivityManager,那麼我們只要能通過反射拿到,然後通過動態代理去Hook這個接口在啓動的時候把PluginActivity替換爲SubActivity即可繞過AMS的驗證。
public class IActivityManagerProxy implements InvocationHandler {//動態代理
private final Object am;
public IActivityManagerProxy(Object am) {//傳入代理的AMS對象
this.am = am;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {//startActivity方法
Intent oldIntent = null;
int i = 0;
for (; i < args.length - 1; i++) {//獲取startActivity Intent參數
if (args[i] instanceof Intent) {
oldIntent = (Intent) args[i];
break;
}
}
Intent newIntent = new Intent();//創建新的Intent
newIntent.setClassName("rocketly.demo", "rocketly.demo.SubActivity");//啓動目標SubActivity
newIntent.putExtra(HookHelper.TRANSFER_INTENT, oldIntent);//保留原始intent
args[i] = newIntent;//把插件Intent替換爲佔坑Intent
}
return method.invoke(am, args);
}
}
動態代理寫好後,我們還需要通過反射去hook住原始AMS。因爲會用到反射弄了一個簡單的工具類
public class FieldUtil {
public static Object getField(Class clazz, Object target, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
public static Field getField(Class clazz, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
}
public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
}
接下來是hook代碼
public class HookHelper {
public static final String TRANSFER_INTENT = "transfer_intent";
public static void hookAMS() throws Exception {
Object singleton = null;
if (Build.VERSION.SDK_INT >= 26) {//大於等於8.0
Class<?> clazz = Class.forName("android.app.ActivityManager");
singleton = FieldUtil.getField(clazz, null, "IActivityManagerSingleton");//拿到靜態字段
} else {//8.0以下
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
singleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");//拿到靜態字段
}
Class<?> singleClazz = Class.forName("android.util.Singleton");
Method getMethod = singleClazz.getMethod("get");
Object iActivityManager = getMethod.invoke(singleton);//拿到AMS
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));//生成動態代理
FieldUtil.setField(singleClazz, singleton, "mInstance", proxy);//將代理後的對象設置回去
}
}
接下來我們需要在Application去執行hook
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類文件
HookHelper.hookAMS();//hookAMS
} catch (Exception e) {
e.printStackTrace();
}
}
}
那麼這裏我們已經實現了第一步
在宿主項目中提前弄一個SubActivity佔坑,在啓動PluginActivity的時候替換爲啓動這個SubActivity繞過驗證。
接下來我們在看如何在收到AMS創建Activity的通知時替換回PluginActivity。
AMS創建Activity的通知會先發送到ApplicationThread,然後ApplicationThread會通過Handler去執行對應邏輯。
private class ApplicationThread extends IApplicationThread.Stub {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {//收到AMS啓動Activity事件
ActivityClientRecord r = new ActivityClientRecord();
r.intent = intent;//給r賦上要啓動的intent
...//省略很多r屬性初始化
sendMessage(H.LAUNCH_ACTIVITY, r);//發送r到Handler
}
private void sendMessage(int what, Object obj) {
sendMessage(what, obj, 0, 0, false);
}
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);//發送到mH
}
}
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//執行啓動activity
} break;
}
}
}
既然是通過sendMessage()方式通知Handler去執行對應的方法,那麼在調用handleMessage()之前會通過dispatchMessage()分發事件。
public class Handler {
final Callback mCallback;
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
public interface Callback {
public boolean handleMessage(Message msg);
}
}
可以發現一個很好的hook點就是mCallback這個接口,可以讓我們在handleMessage方法之前將ActivityClientRecord中的SubActivity Intent替換回PluginActivity Intent。
public class HCallback implements Handler.Callback {//實現Callback接口
public static final int LAUNCH_ACTIVITY = 100;
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY://啓動事件
Object obj = msg.obj;
try {
Intent intent = (Intent) FieldUtil.getField(obj.getClass(), obj, "intent");//拿到ActivityClientRecord的intent字段
Intent targetIntent = intent.getParcelableExtra(HookHelper.TRANSFER_INTENT);//拿到我們要啓動PluginActivity的Intent
intent.setComponent(targetIntent.getComponent());//替換爲啓動PluginActivity
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
}
接下來就是我們需要將這個Callback設置給Handler,而剛剛說的Handler是ActivityThread的成員變量mH,ActivityThread實例則可以通過他的靜態字段sCurrentActivityThread獲取。
public final class ActivityThread {
private static volatile ActivityThread sCurrentActivityThread;
final H mH = new H();
}
然後我們通過反射給mH設置Callback
public class HookHelper {
...//省略前面的hookAMS()方法
public static void hookH() throws Exception {
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
Object activityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");//拿到activityThread
Object mH = FieldUtil.getField(activityThreadClazz, activityThread, "mH");//拿到mH
FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback());//給mH設置callback
}
}
依舊是在Application初始化這段hook邏輯
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類文件
HookHelper.hookAMS();
HookHelper.hookH();
} catch (Exception e) {
e.printStackTrace();
}
}
}
到這裏完成了我們上面說的第二步,並且成功啓動了PluginActivity
在AMS處理完相應驗證通知我們ActivityThread創建Activty的時候在替換爲PluginActivity。
不過這裏肯定會有人問啓動是啓動了但是沒有生命週期,對於AMS那邊他只知道我們啓動的是SubActivity,那麼接下來我們解釋生命週期如何處理。
插件Activity生命週期
其實不用做任何處理就已經有生命週期了,那麼我們看看是爲何。
先回顧下啓動的流程
- AMS通知ApplicationThread啓動Activity
- ApplicationThread發送事件到Handler
- Handler調用handleLaunchActivity去執行啓動邏輯
- 然後在handleLaunchActivity方法中創建對應的Activity
對你會發現Activity是在應用進程創建的,AMS是沒有該Activity的引用的,那麼AMS必須得有一個唯一標識來標識該Activity,然後應用進程存儲這個標識和Activity的對應關係,這樣當AMS通知應用進程生命週期事件的時候只需要告訴應用進程需要執行該事件的Activity標識就可以了,然後應用進程通過標識找到Activity具體執行即可。
那我們先看下創建Activity的時候是如何存儲這個關係的。
private class ApplicationThread extends IApplicationThread.Stub {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {//AMS通知啓動Activity
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;//這個token正是Activity的唯一標示
...//省略很多r屬性初始化
sendMessage(H.LAUNCH_ACTIVITY, r);//發送到Handler
}
}
private class H extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {//啓動Activity事件
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//執行啓動的方法
} break;
}
}
}
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
Activity a = performLaunchActivity(r, customIntent);//真正執行啓動的方法
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ComponentName component = r.intent.getComponent();//拿到intent中的組件
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);//通過反射創建Activity
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
r.activity = activity;//將創建的好的Activity存儲在ActivityClientRecord對象中
mActivities.put(r.token, r);//然後用一個Map存儲token和ActivityClientRecord的對應關係
return activity;
}
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();//存儲對應關係的Map
從代碼中可以看出,在創建Activity之後將Activity存儲到了ActivityClientRecord對象中,然後用AMS傳來的token作爲鍵ActivityClientRecord作爲值存儲到Map中。
而在ActivityThread中執行生命週期的方法一般命名爲perform$事件名Activity()
,那麼直接看該方法
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide, String reason) {
ActivityClientRecord r = mActivities.get(token);//通過AMS token拿到ActivityClientRecord
r.activity.performResume();//執行Resume事件
}
private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = mActivities.get(token);//通過AMS token拿到ActivityClientRecord
mInstrumentation.callActivityOnDestroy(r.activity);//執行Destroy事件
}
隨便找了兩個執行生命週期事件的方法,都是通過AMS的token找到ActivityClientRecord然後拿到裏面的Activity執行生命週期方法。
那麼在分析下爲啥,創建的PluginActivity會有生命呢,因爲我們是在Handler將StubActivity替換爲PluginActivity,然後在performLaunchActivity方法中,會將PluginActivity創建並且添加到ActivityClientRecord然後用AMS傳來的token作爲鍵ActivityClientRecord作爲值存儲到Map中,那麼在接下來的生命週期方法AMS是通過token來通知應用進程執行生命週期方法,而這個token所對應的Activity就是PluginActivity,所以PluginActivity就有了生命。
初始化插件資源
前面我們已經完成了PluginActivity的啓動和生命週期事件,但是PluginActivity沒法setContentView()這種方式通過id去操作佈局,因爲凡是通過id去獲取資源的方式都是通過Resource去獲取的,但是宿主APK並不知道插件APK的存在,所以宿主Resource也沒法加載插件APK的資源。
那麼這裏我們可以給插件APK創建一個Resources,然後插件APK中都通過這個Resource去獲取資源。這裏看下Resources構造方法
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
有三個參數
- AssetManager真正加載資源的(根據插件APK路徑創建AssetManager加載資源)
- DisplayMetrics顯示配置(直接用宿主的Resources的配置即可)
- Configuration配置(直接用宿主的Resources的配置即可)
接下來看AssetManager如何創建
public final class AssetManager implements AutoCloseable {
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init(false);
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {//傳入需要加載資源的路徑
return addAssetPathInternal(path, false);
}
}
直接通過空參構造方法創建,然後調用addAssetPath()去加載對應路徑的資源。
接下來我們在Application中創建插件的Resources,之所以在這裏創建也是有原因的,方便插件APK中獲取到這個Resources。
public class MyApplication extends Application {
private Resources pluginResource;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類文件
HookHelper.hookAMS();
HookHelper.hookH();
initPluginResource();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initPluginResource() throws Exception {
Class<AssetManager> clazz = AssetManager.class;
AssetManager assetManager = clazz.newInstance();//創建AssetManager
Method method = clazz.getMethod("addAssetPath", String.class);//拿到addAssetPath方法
method.invoke(assetManager, getExternalFilesDir("plugin").listFiles()[0].getAbsolutePath());//調用addAssetPath傳入插件APk路徑
pluginResource = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());//生成插件Resource
}
@Override
public Resources getResources() {
return pluginResource == null ? super.getResources() : pluginResource;
}
}
這裏我們解釋下爲啥插件Resources在Application初始化插件APK方便獲取,因爲插件APK中的四大組件實際都是在宿主APK創建的,那麼他們拿到的Application實際上都是宿主的,所以他們只需要通過getApplication().getResources()
就可以非常方便的拿到插件Resource。
插件工程
插件工程比較簡單,就是一個Activity,不過有點需要注意的是重寫了getResources()
方法,因爲我們需要通過插件Resources才能用id去操作資源文件。
public class PluginActivity extends Activity {//這裏需要注意繼承的是Activity不是AppCompatActivity,因爲AppCompatActivity做了很多檢查用它的話還需要多hook幾個類,而我們主要是流程和原理的掌握就沒有進行適配了。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin);
}
@Override
public Resources getResources() {//重寫getResources()是因爲對於activity中通過id獲取資源的Resources都是通過該方法獲取
return getApplication() != null && getApplication().getResources() != null ? getApplication().getResources() : super.getResources();//拿到插件Resources
}
}
測試流程
測試流程這裏說明下
- 將插件項目打包成APK
- 然後通過adb命令
adb push <local> <remote>
將APK推到內存卡中 - 宿主應用加載插件APK,能顯示插件Activity佈局即爲成功