一、動態代理 + Hook 的實現
在之前的文章我們講過插件化的實現有點類似,插件化一般是替換系統的 mInstrumentation 為自己的 Instrumentation 。
而我們這里沒有這么麻煩,我們這里需要Hook的是ASM ,是Android啟動頁面過程中的一個 mInstance 對象,它就是ActivityManagerService。
startActivity()最終會進入Instrumentation:
@Overridepublic void startActivityForResult(
String who, Intent intent, int requestCode, @Nullable Bundle options) {
...
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, who,
intent, requestCode, options);
...
}
Instrumentation的execStartActivity代碼:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
... try {
... int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) { throw new RuntimeException("Failure from system", e);
} return null;
}
gDefault是一個Singleton類型的靜態常量,它的get()方法返回的是Singleton類中的private T mInstance ,這個mInstance的創建又是在gDefault實例化時通過create()方法實現。gDefault.get()獲取到的mInstance實例就是ActivityManagerService(AMS)實例。由于gDefault是一個靜態常量,因此可以通過反射獲取到它的實例,同時它是Singleton類型的,因此可以獲取到其中的mInstance。
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() { protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity"); if (false) {
Log.v("ActivityManager", "default service binder = " + b);
} IActivityManager am = asInterface(b); if (false) {
Log.v("ActivityManager", "default service = " + am);
} return am;
}
};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;
}
}
}
由于8.0系統以下 ,8.0系統 - 9.0系統,10系統 - 12系統 的實現均有差異,需要做一下兼容性處理。我們通過下面的工具類方法實現如何使用反射 + Hook + 動態代理實現效果:
public class DynamicProxyUtils { //修改啟動模式
public static void hookAms() { try {
Field singletonField;
Class<?> iActivityManager; // 1,獲取Instrumentation中調用startActivity(,intent,)方法的對象
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 10.0以上是ActivityTaskManager中的IActivityTaskManagerSingleton
Class<?> activityTaskManagerClass = Class.forName("android.app.ActivityTaskManager");
singletonField = activityTaskManagerClass.getDeclaredField("IActivityTaskManagerSingleton");
iActivityManager = Class.forName("android.app.IActivityTaskManager");
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 8.0,9.0在ActivityManager類中IActivityManagerSingleton
Class activityManagerClass = ActivityManager.class;
singletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");
iActivityManager = Class.forName("android.app.IActivityManager");
} else { // 8.0以下在ActivityManagerNative類中 gDefault
Class<?> activityManagerNative = Class.forName("android.app.ActivityManagerNative");
singletonField = activityManagerNative.getDeclaredField("gDefault");
iActivityManager = Class.forName("android.app.IActivityManager");
}
singletonField.setAccessible(true); Object singleton = singletonField.get(null); // 2,獲取Singleton中的mInstance,也就是要代理的對象
Class<?> singletonClass = Class.forName("android.util.Singleton"); Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true); Method getMethod = singletonClass.getDeclaredMethod("get"); Object mInstance = getMethod.invoke(singleton); if (mInstance == null) { return;
} //開始動態代理
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(), new Class[]{iActivityManager}, new AmsHookBinderInvocationHandler(mInstance)); //現在替換掉這個對象
mInstanceField.set(singleton, proxy);
} catch (Exception e) {
e.printStackTrace();
}
} //動態代理執行類
public static class AmsHookBinderInvocationHandler implements InvocationHandler { private Object obj; public AmsHookBinderInvocationHandler(Object rawIActivityManager) {
obj = rawIActivityManager;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("startActivity".equals(method.getName())) {
Intent raw; int index = 0; for (int i = 0; i < args.length; i++) { if (args[i] instanceof Intent) {
index = i; break;
}
} //原始意圖
raw = (Intent) args[index];
YYLogUtils.w("原始意圖:" + raw); //設置新的Intent-直接制定LoginActivity
Intent newIntent = new Intent(); String targetPackage = "com.guadou.kt_demo"; ComponentName componentName = new ComponentName(targetPackage, LoginDemoActivity.class.getName());
newIntent.setComponent(componentName);
YYLogUtils.w("改變了Activity啟動");
args[index] = newIntent;
YYLogUtils.w("攔截activity的啟動成功" + " --->"); return method.invoke(obj, args);
} //如果不是攔截的startActivity方法,就直接放行
return method.invoke(obj, args);
}
}
}
使用的時候我們可以在Application中使用,也可以就在方法中啟動:
mBtnProfile.click { //啟動動態代理
DynamicProxyUtils.hookAms()
gotoActivity<ProfileDemoActivity>()
}
這樣我們就可以把應用類全部的Activity跳轉都替換為我們的LoginActivity了...太壞了。下一步怎么做?
二、Itent的攔截與處理
其實和之前Intent的攔截處理有點類似了,我們判斷是否登錄,如果已經登錄了,直接放行,如果沒有登錄,我們拿到原始的Intent,當做參數傳給新的LoginIntent。登錄執行完成了讓LoginActivity幫我們做后續的意圖。
我們修改動態代理的回調方法:
//動態代理執行類
public static class AmsHookBinderInvocationHandler implements InvocationHandler { private Object obj; public AmsHookBinderInvocationHandler(Object rawIActivityManager) {
obj = rawIActivityManager;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("startActivity".equals(method.getName())) { //如果已經登錄-直接放行
if (LoginManager.isLogin()){ return method.invoke(obj, args);
} //如果未登錄-獲取到原始意圖,再替換Intent攜帶數據到LoginActivity中
Intent raw; int index = 0; for (int i = 0; i < args.length; i++) { if (args[i] instanceof Intent) {
index = i; break;
}
} //原始意圖
raw = (Intent) args[index];
YYLogUtils.w("原始意圖:" + raw); //設置新的Intent-直接制定LoginActivity
Intent newIntent = new Intent(); String targetPackage = "com.guadou.kt_demo"; ComponentName componentName = new ComponentName(targetPackage, LoginDemoActivity.class.getName());
newIntent.setComponent(componentName);
newIntent.putExtra("targetIntent", raw);
YYLogUtils.w("改變了Activity啟動");
args[index] = newIntent;
YYLogUtils.w("攔截activity的啟動成功" + " --->"); return method.invoke(obj, args);
} //如果不是攔截的startActivity方法,就直接放行
return method.invoke(obj, args);
}
}
使用邏輯:
mBtnProfile.click { //啟動動態代理
DynamicProxyUtils.hookAms()
gotoProfilePage()
}
Login頁面的處理:
private var mTargetIntent: Intent? = null
private var mTargetType = 0
override fun init() {
mTargetIntent = intent.getParcelableExtra("targetIntent")
mTargetType = intent.getIntExtra("type", 0)
} fun doLogin() {
showStateLoading()
CommUtils.getHandler().postDelayed({
showStateSuccess()
SP().putString(Constants.KEY_TOKEN, "abc")
setResult(-1, Intent().apply { putExtra("type", mTargetType) }) //設置Result
if (mTargetIntent != null) {
startActivity(mTargetIntent)
}
finish()
}, 500)
}
總結
其實我們可以加入一個黑名單,白名單的集合來管理,例如我們使用注解標記哪一些頁面需要校驗登錄,然后把這些注解的頁面放入一個集合中,在動態代理的回調中,我們判斷如果在這些集合中的頁面才會判斷是否登錄,否則直接放行。
如果需要管理的頁面太多,我們可以使用APT代碼生成,或者ASM字節碼注入等多種方式來實現。網上有一些方案是基于APT代碼生成的示例。
當然如果大家有需求可以自行擴展與實現,比如頁面不多的話,可以自己管理一個黑名單集合,如果多的話可以使用APT生成代碼。
主要注意的是,注解的方案只用于跳轉頁面的場景,如果是彈窗,或者切換Tab的場景就無法實現,還是不夠靈活。
優點與缺點
相比Intent的方案,使用Hook+動態代理的方法對攔截登錄頁面進行了封裝和處理,集中處理的方式在使用起來更加的便捷,后面的繼續執行的邏輯還是和Intent方案一樣的邏輯。
可以說是Intent的進化版,缺點還是和Intent一樣,在繼續執行這一塊還是使用起來麻煩,如果有跳轉頁面之外的邏輯還是免不了各種type區分和定義。除此之外基于Hook的實現跟系統版本有關系,目前只是兼容到Android12版本,如果后期Androd13 14又有修改,那么可能就無法運行了。
總的來說,個人不是很推薦這樣的方案,當然如果大家使用的是定制設備,系統版本是固定的,那么這樣的方案也不是不能用,所以大家需要按需選擇。