携手创作,共同生长!这是我参与「日新计划 8 月更文应战」的第26天,点击查看活动概况
登录阻拦系列:
- 登录阻拦-AOP的完成
- 登录阻拦-办法池和音讯回调的完成
- 登录阻拦-线程的完成
- 登录阻拦-协程的完成
- 登录阻拦-Intent的完成
- 登录阻拦-动态署理Hook的完成
- 登录阻拦-阻拦器形式的完成
前言
前面的一篇文章咱们讲到了Intent原始的办法,虽然运用起来很麻烦可是仍是能完成作用的,那有没有简洁一点的封装?
有的,其实就和本篇的标题一样,早前网上还有这样的一种计划,运用动态署理+Hook的办法,替换发动Activity的目标,把悉数的startActivity都阻拦掉,替换掉咱们自定义的Activiy。
假如都写死了一切的Activity跳转都写到一个阻拦中,咱们又怎么完成阻拦登录的功用呢?
咱们收拾一下思路:
- 咱们需求先运用动态署理+Hook的办法替换悉数的Activity发动。
- 咱们在动态署理的回调中咱们需求拿到原始的Intent,内部判别是否现已登录
- 假如现已登录,咱们需求放行,直接履行原始的Intent办法。
- 假如没有登录,咱们需求阻拦,替换掉原始的Intentn,跳转到咱们指定的Intent – LoginActivity的跳转。
- 那怎么完成登录完成持续目的呢?仍是和前面的文章一样,仍是把之前的inent当参数传递给LoginActivity,然后让LoginActivity帮咱们履行之前的Intent。
接下来咱们一步一步来完成这个计划。
一、动态署理 + Hook 的完成
在之前的文章咱们讲过插件化的完成有点相似,插件化一般是替换体系的 mInstrumentation 为自己的 Instrumentation 。
而咱们这儿没有这么麻烦,咱们这儿需求Hook的是ASM ,是Android发动页面过程中的一个 mInstance 目标,它便是ActivityManagerService。(下面源码为摘录!)
startActivity()最终会进入Instrumentation:
@Override
public 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);
}
}
运用的逻辑和Intent那篇文章一样的:
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)
}
完成的作用:(按钮文本没改,其实不是AOP完成的,AOP在另一个分支上)
总结
运用动态署理加Hook的计划,我能够理解为Intent计划的升级版。
持续优化
其实咱们能够参加一个黑名单,白名单的调集来办理,例如咱们运用注解符号哪一些页面需求校验登录,然后把这些注解的页面放入一个调集中,在动态署理的回调中,咱们判别假如在这些调集中的页面才会判别是否登录,不然直接放行。
假如需求办理的页面太多,咱们能够运用APT代码生成,或许ASM字节码注入等多种办法来完成。网上有一些计划是根据APT代码生成的示例。
当然假如我们有需求能够自行扩展与完成,比方页面不多的话,能够自己办理一个黑名单调集,假如多的话能够运用APT生成代码。
主要注意的是,注解的计划只用于跳转页面的场景,假如是弹窗,或许切换Tab的场景就无法完成,仍是不够灵活。
优点与缺陷
比较Intent的计划,运用Hook+动态署理
的办法对阻拦登录页面进行了封装和处理,集中处理的办法在运用起来更加的快捷,后边的持续履行的逻辑仍是和Intent计划一样的逻辑。
能够说是Intent的进化版,缺陷仍是和Intent一样,在持续履行这一块仍是运用起来麻烦,假如有跳转页面之外的逻辑仍是免不了各种type区别和定义。除此之外根据Hook的完成跟体系版别有联系,目前只是兼容到Android12版别,假如后期Androd13 14又有修正,那么或许就无法运行了。
总的来说,个人不是很推荐这样的计划,当然假如我们运用的是定制设备,体系版别是固定的,那么这样的计划也不是不能用,所以我们需求按需选择。
关于动态署理+Hook
的计划假如我们有更好的计划也能够评论区提出我们一起沟通。
后期我会再出一些阻拦登录的其他思路,我们能够和之前的办法做一下比照,看看哪一种更和你的食欲,你们运用的又是哪一种计划?能够评论区沟通哦。
好了,我本人如有讲解不到位或讹夺的地方,期望同学们能够指出沟通。
假如感觉本文对你有一点点点的启示,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。