外掛化之啟動沒有註冊的Activity
啟動沒有在AndroidManifest中註冊的Activity是安卓外掛化中一個很重要的知識點,只有這樣你才能把Activity中分離出來,放到外掛中.
啟動沒有在AndroidManifest中註冊的Activity,會涉及到Activity啟動流程、反射、動態代理的知識,我覺得就算不學外掛化,掌握這些知識也是很有用的.
Activity的啟動流程
為了達到啟動沒有在AndroidManifest中註冊的Activity的目的,我們先來分析下Activity的啟動流程,看看有沒有什麼突破口.
這部分的知識我在 ofollow,noindex">《從原始碼看Activity生命週期》 這篇部落格裡面其實也有講過,這裡只做大概的講解,然後做一些補充,感興趣的同學可以將兩篇部落格結合起來看看.
丟擲ActivityNotFoundException的原因
如果使用startActivity去啟動一個沒有在AndroidManifest中註冊的Activity,正常情況下是會丟擲ActivityNotFoundException的,那這個異常是怎麼丟擲來的呢?
我們知道呼叫Activity.startActivity方法,實際上最後是呼叫了Instrumentation.execStartActivity:
public class Instrumentation { ... public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ... int result = ActivityManagerNative.getDefault() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, null, options); checkStartActivityResult(result, intent); ... } ... public static void checkStartActivityResult(int res, Object intent) { ... switch (res) { case ActivityManager.START_INTENT_NOT_RESOLVED: case ActivityManager.START_CLASS_NOT_FOUND: if (intent instanceof Intent && ((Intent)intent).getComponent() != null) throw new ActivityNotFoundException( "Unable to find explicit activity class " + ((Intent)intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?"); throw new ActivityNotFoundException( "No Activity found to handle " + intent); ... } ... } ... }
可以看到Instrumentation又是通過ActivityManagerNative.getDefault()拿到一個IActivityManager去呼叫其startActivity來啟動Activity的.
這個IActivityManager內部實際是通過Binder機制將處理轉發給ActivityManagerService:
public abstract class ActivityManagerNative extends Binder implements IActivityManager ... static public IActivityManager getDefault() { return gDefault.get(); } ... private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() { protected IActivityManager create() { //實際上是用Binder機制與AMS進行互動 IBinder b = ServiceManager.getService("activity"); IActivityManager am = asInterface(b); return am; } }; ... }
所以可以看到通過ActivityManagerService去startActivity之後會有個返回值.
ActivityManagerService內部會使用PackageManagerService查詢這個Activity是否在AndroidManifest中註冊.如果沒有,就會返回START_CLASS_NOT_FOUND或者START_INTENT_NOT_RESOLVED,這個時候Instrumentation就會丟擲ActivityNotFoundException.
所以ActivityNotFoundException就是這樣被丟擲的.
Activity是怎樣被建立的
我們都知道兩個不同的程序直接是不能直接訪問記憶體的,所以處於應用程序的Activity肯定還是應用程序去建立,而不是被AMS建立的.
這塊的程式碼在ActivityThread中實現:
public final class ActivityThread { ... final H mH = new H(); ... @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) { ... sendMessage(H.LAUNCH_ACTIVITY, r); } ... private class H extends Handler { public static final int LAUNCH_ACTIVITY= 100; ... public void handleMessage(Message msg) { switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; ... } ... } ... }
AMS會呼叫ActivityThread的scheduleLaunchActivity,在這個方法中會使用一個Hander同步到主執行緒中再去建立Activity.
Activity啟動的原理圖

1.png
怎樣欺騙ActivityManagerService
從上面的Activity啟動的原理圖可以看到大概的流程是:
應用將要啟動的Activity告訴AMS->AMS檢查Activity是否註冊->AMS讓ActivityThread去建立Activity.
那是不是可以這樣呢?
- 新建一個StubActivity並且在AndroidManifest中註冊
- 將想要啟動的Activity換成StubActivity,而將真正想要啟動的Activity儲存到Extra中
- 騙過AMS
- 在ActivityThread中拿出真正想要建立的Activity換回來去建立
修改後的原理如下:

2.png
將要啟動的Activity替換成StubActivity
第一步是將要啟動的Activity替換成StubActivity,我們回顧下上一節看到的ActivityManagerNative程式碼:
public abstract class ActivityManagerNative extends Binder implements IActivityManager ... static public IActivityManager getDefault() { return gDefault.get(); } ... private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() { protected IActivityManager create() { //實際上是用Binder機制與AMS進行互動 IBinder b = ServiceManager.getService("activity"); IActivityManager am = asInterface(b); return am; } }; ... }
可以看到這個gDefault其實是個靜態的私有成員變數.
那我們是不是可以通過反射,將它替換成我們寫的Singleton<IActivityManager>,然後儲存好原來的gDefault,在替換的程式碼裡面先將要啟動的Activity替換成StubActivity,然後再將Intent傳給原來的gDefault?
大概的做法如下:
class MyActivityManager implements IActivityManager { private IActivityManager mOrigin; public MyActivityManager(IActivityManager origin) { mOrigin = origin; } public int startActivity(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException { // TODO 將要啟動的activity替換成StubActivity return mOrigin. startActivity(caller, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options); } ... } Class c = Class.forName("android.app.ActivityManagerNative"); final Field field =c.getDeclaredField("gDefault"); field.setAccessible(true); Singleton<IActivityManager> proxy = new Singleton<IActivityManager>() { protected IActivityManager create() { return new MyActivityManager(field.get(null)); } }; field.set(null, proxy);
但是這個做法問題很大,首先我們要將IActivityManager的所有方法都實現一遍轉發給mOrigin。而且最大的問題是IActivityManager和Singleton被隱藏了,我們在應用層是找不到定義的!
那怎麼辦呢?別急,我們先來看看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; } } }
其實最終的IActivityManager是儲存在mInstance這個變數裡面的,我們只需要替換這個變數就好,於是就繞過了Singleton沒有定義的問題。但是還有這個IActivityManager的定義問題擺在我們面前。
怎麼辦呢?答案就是我們可以用動態代理的方法去建立IActivityManager。關於動態代理我之前寫過一篇部落格 Java%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3%E5%92%8C%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/" target="_blank" rel="nofollow,noindex">《Java自定義註解和動態代理》 ,大家感興趣的話可以去看看。這裡就直接把程式碼貼上了:
// 獲取gDefault Class activityManagerClass = Class.forName("android.app.ActivityManagerNative"); Field gDefaultField = activityManagerClass.getDeclaredField("gDefault"); gDefaultField.setAccessible(true); Object gDefault = gDefaultField.get(null); // 獲取mIntance Class singletonClass = Class.forName("android.util.Singleton"); Field mInstanceField = singletonClass.getDeclaredField("mInstance"); mInstanceField.setAccessible(true); Object mInstance = mInstanceField.get(gDefault); // 替換mIntance Object proxy = Proxy.newProxyInstance( mInstance.getClass().getClassLoader(), new Class[]{Class.forName("android.app.IActivityManager")}, new IActivityManagerHandler(mInstance)); mInstanceField.set(gDefault, proxy); public static class IActivityManagerHandler implements InvocationHandler { private Object mOrigin; IActivityManagerHandler(Object origin) { mOrigin = origin; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("startActivity".equals(method.getName())) { int index = 0; for (int i = 0; i < args.length; i++) { if (args[i] instanceof Intent) { index = i; break; } } Intent raw = (Intent) args[index]; Intent intent = new Intent(); intent.setClassName(raw.getComponent().getPackageName(), StubActivity.class.getName()); intent.putExtra("RawIntent", raw); args[index] = intent; } return method.invoke(mOrigin, args); } }
上面的程式碼的功能就是建立一個IActivityManager的代理,代理startActivity方法,將啟動的Activity的Intent換成啟動StubActivity的Intent,並且將原來的Intent儲存起來放到RawIntent這個Extra裡。
然後用它去替換ActivityManagerNative.gDefault的mInstance成員變數。
將StubActivity替換會要啟動的Activity
在上面我們已經將要啟動的Activity替換成了已經註冊了的StubActivity,這樣在AMS檢查的時候就能在AndroidManifest查到,不會報ActivityNotFoundException了.
然後AMS會讓ActivityThread去建立Activity,這個時候就要將StubActivity替換會真正要啟動的Activity了.
再回顧下這部分的程式碼:
public final class ActivityThread { ... final H mH = new H(); ... @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) { ... sendMessage(H.LAUNCH_ACTIVITY, r); } ... private class H extends Handler { public static final int LAUNCH_ACTIVITY= 100; ... public void handleMessage(Message msg) { switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; ... } ... } ... }
ActivityThread的scheduleLaunchActivity方法會被調到,然後會向mH傳送LAUNCH_ACTIVITY訊息.
所以關鍵點就是將這個mH變數替換成我們的代理物件,將Intent替換回之前儲存的RawIntent.
但是這裡有個問題,H是個內部類,我們是沒有辦法用動態代理的方式建立內部類的,也就是說我們沒有辦法替換掉mH這個物件.
於是只好繼續挖一挖Handler內部有沒有機會了,其實在Handler.dispatchMessage裡面是會先判斷mCallback是不是有賦值的,如果有就會將訊息交給它去處理.
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); } } ... }
所以我們可以從這個mCallback入手,將mH的mCallback設定成我們的代理物件:
// 獲取ActivityThread例項 Class activityThreadClass = Class.forName("android.app.ActivityThread"); Field threadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); threadField.setAccessible(true); Object sCurrentActivityThread = threadField.get(null); // 獲取mH變數 Field mHField = activityThreadClass.getDeclaredField("mH"); mHField.setAccessible(true); Object mH = mHField.get(sCurrentActivityThread); // 設定mCallback變數 Field mCallbackField = Handler.class.getDeclaredField("mCallback"); mCallbackField.setAccessible(true); Handler.Callback callback = new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == 100) { try { Field intentField = msg.obj.getClass().getDeclaredField("intent"); intentField.setAccessible(true); Intent intent = (Intent) intentField.get(msg.obj); Intent raw = intent.getParcelableExtra("RawIntent"); intent.setComponent(raw.getComponent()); } catch (Exception e) { Log.e("hook", "get intent err", e); } } return false; } }; mCallbackField.set(mH, callback);
ActivityThread的例項儲存在sCurrentActivityThread這個靜態成員變數裡,程式碼我就不貼了,然後我們在mCallback這裡將要啟動的Activity設定回來.
處理Android 8.0的情況
上面的程式碼執行在8.0的系統上會崩潰,原因是8.0對Activity的啟動這塊做了些改動,不再使用ActivityManagerNative.getDefault()了,改成了ActivityManager.getService():
public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ... 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); ... }
ActivityManager其實和ActivityManagerNative很像:
public class ActivityManager { ... 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; } }; ... }
所以我們類似的去替換IActivityManagerSingleton就好了:
// 獲取IActivityManagerSingleton Class activityManagerClass = Class.forName("android.app.ActivityManager"); Field singletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton"); singletonField.setAccessible(true); Object gDefault = singletonField.get(null); // 獲取mIntance Class singletonClass = Class.forName("android.util.Singleton"); Field mInstanceField = singletonClass.getDeclaredField("mInstance"); mInstanceField.setAccessible(true); Object mInstance = mInstanceField.get(gDefault); // 替換mIntance Object proxy = Proxy.newProxyInstance( mInstance.getClass().getClassLoader(), new Class[]{Class.forName("android.app.IActivityManager")}, new IActivityManagerHandler(mInstance)); mInstanceField.set(gDefault, proxy);
處理AppCompatActivity的情況
到目前為止,我們已經可以正常啟動沒有註冊的Activity了,但是其實還有一個BUG:如果啟動的是沒有註冊的AppCompatActivity就會崩潰。
10-25 19:32:30.86787548754 E AndroidRuntime: Caused by: java.lang.IllegalArgumentException: android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{me.linjw.plugindemo/me.linjw.plugindemo.HideActivity} 10-25 19:32:30.86787548754 E AndroidRuntime:at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:285) 10-25 19:32:30.86787548754 E AndroidRuntime:at android.support.v7.app.AppCompatDelegateImplV9.onCreate(AppCompatDelegateImplV9.java:158) 10-25 19:32:30.86787548754 E AndroidRuntime:at android.support.v7.app.AppCompatDelegateImplV14.onCreate(AppCompatDelegateImplV14.java:58) 10-25 19:32:30.86787548754 E AndroidRuntime:at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:72) 10-25 19:32:30.86787548754 E AndroidRuntime:at com.cvte.tv.speech.TestActivity.onCreate(TestActivity.java:14) 10-25 19:32:30.86787548754 E AndroidRuntime:at android.app.Activity.performCreate(Activity.java:6664) 10-25 19:32:30.86787548754 E AndroidRuntime:at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118) 10-25 19:32:30.86787548754 E AndroidRuntime:at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2599)
網上很多講啟動未註冊的Activity的文章要不就沒有講這個,要不就沒有詳細講如何處理,直接一筆帶過了.這裡我手把手帶大家解BUG.
遇到問題先不要慌,先看看列印找到崩潰的程式碼在哪:
@Nullable public static String getParentActivityName(Activity sourceActivity) { try { return getParentActivityName(sourceActivity, sourceActivity.getComponentName()); } catch (NameNotFoundException e) { // Component name of supplied activity does not exist...? throw new IllegalArgumentException(e); } } @Nullable public static String getParentActivityName(Context context, ComponentName componentName) throws NameNotFoundException { PackageManager pm = context.getPackageManager(); ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA); String parentActivity = IMPL.getParentActivityName(context, info); return parentActivity; }
很明顯是PackageManager.getActivityInfo在AndroidManifest裡面找不到Activity丟擲了NameNotFoundException.
所以我們看看有沒有辦法替換一下這個Context.getPackageManager()拿到的PackageManager:
class ContextImpl extends Context { ... @Override public PackageManager getPackageManager() { if (mPackageManager != null) { return mPackageManager; } IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { // Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null; } ... }
ContextImpl會從ActivityThread.getPackageManager獲取IPackageManager,讓我們繼續挖:
public final class ActivityThread { ... static volatile IPackageManager sPackageManager; ... public static IPackageManager getPackageManager() { if (sPackageManager != null) { //Slog.v("PackageManager", "returning cur default = " + sPackageManager); return sPackageManager; } IBinder b = ServiceManager.getService("package"); //Slog.v("PackageManager", "default service binder = " + b); sPackageManager = IPackageManager.Stub.asInterface(b); //Slog.v("PackageManager", "default service = " + sPackageManager); return sPackageManager; } ... }
所以sPackageManager就是我們的突破點,讓我們來把它換掉:
try { //要先獲取一下,保證它初始化 context.getPackageManager(); Class activityThread = Class.forName("android.app.ActivityThread"); Field pmField = activityThread.getDeclaredField("sPackageManager"); pmField.setAccessible(true); final Object origin = pmField.get(null); Object handler = Proxy.newProxyInstance(activityThread.getClassLoader(), new Class[]{Class.forName("android.content.pm.IPackageManager")}, new PackageManagerHandler(context, origin)); pmField.set(null, handler); } catch (Exception e) { Log.e("hook", "hook IPackageManager err", e); } static class PackageManagerHandler implements InvocationHandler { private Context mContext; private Object mOrigin; PackageManagerHandler(Context context, Object origin) { mContext = context; mOrigin = origin; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (!method.getName().equals("getActivityInfo")) { return method.invoke(mOrigin, args); } //如果沒有註冊,並不會丟擲異常,而是會直接返回null Object ret = method.invoke(mOrigin, args); if (ret == null) { for (int i = 0; i < args.length; i++) { if (args[i] instanceof ComponentName) { ComponentName componentName = (ComponentName) args[i]; componentName.getClassName(); args[i] = new ComponentName( mContext.getPackageName(), StubActivity.class.getName() ); return method.invoke(mOrigin, args); } } } return ret; } }
在IPackageManager.getActivityInfo方法丟擲異常的時候invoke會返回null,就代表這個Activity沒有註冊,我們直接將他換成StubActivity就好。
大功告成!
完整Demo
完整Demo見我的 Github