携手创作,共同生长!这是我参与「日新计划 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在另一个分支上)

Android登录拦截的场景-基于动态代理+Hook的实现

总结

运用动态署理加Hook的计划,我能够理解为Intent计划的升级版。

持续优化

其实咱们能够参加一个黑名单,白名单的调集来办理,例如咱们运用注解符号哪一些页面需求校验登录,然后把这些注解的页面放入一个调集中,在动态署理的回调中,咱们判别假如在这些调集中的页面才会判别是否登录,不然直接放行。

假如需求办理的页面太多,咱们能够运用APT代码生成,或许ASM字节码注入等多种办法来完成。网上有一些计划是根据APT代码生成的示例。

当然假如我们有需求能够自行扩展与完成,比方页面不多的话,能够自己办理一个黑名单调集,假如多的话能够运用APT生成代码。

主要注意的是,注解的计划只用于跳转页面的场景,假如是弹窗,或许切换Tab的场景就无法完成,仍是不够灵活。

优点与缺陷

比较Intent的计划,运用Hook+动态署理的办法对阻拦登录页面进行了封装和处理,集中处理的办法在运用起来更加的快捷,后边的持续履行的逻辑仍是和Intent计划一样的逻辑。

能够说是Intent的进化版,缺陷仍是和Intent一样,在持续履行这一块仍是运用起来麻烦,假如有跳转页面之外的逻辑仍是免不了各种type区别和定义。除此之外根据Hook的完成跟体系版别有联系,目前只是兼容到Android12版别,假如后期Androd13 14又有修正,那么或许就无法运行了。

总的来说,个人不是很推荐这样的计划,当然假如我们运用的是定制设备,体系版别是固定的,那么这样的计划也不是不能用,所以我们需求按需选择。

关于动态署理+Hook的计划假如我们有更好的计划也能够评论区提出我们一起沟通。

后期我会再出一些阻拦登录的其他思路,我们能够和之前的办法做一下比照,看看哪一种更和你的食欲,你们运用的又是哪一种计划?能够评论区沟通哦。

好了,我本人如有讲解不到位或讹夺的地方,期望同学们能够指出沟通。

假如感觉本文对你有一点点点的启示,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

Android登录拦截的场景-基于动态代理+Hook的实现