Android登录拦截场景-探究多种实现方式

开发者 2024-9-8 08:18:17 45 0 来自 中国
前言

本文是基于之前的系列文章做的一个合集,精简之后整理为一篇长文供各人参考。合集的入口在此。合集内部有每种方案的详细使用手册,各人可以对照本文参考使用。
登录拦截与放行是大部门App开辟都会碰到的一个场景,假如你的App有游客模式,但是部门高级功能须要登录之后才华使用。
那么我们就须要在用户点击这个操纵的时间校验是否登录,当登录完成之后再跳转到指定的页面或弹窗。假如这些入口许多的话,那么我们就须要到处写这些逻辑。比力初级的用法是使用消息总线,当登录完成之后发送对应key消息,然后去完成对应key的事件。
有没有一种更简朴的方式,会合同一方便的管理登录拦截再放行这一个场景。
下面我们一起来看一看详细的方案。
一、方法池方案

本质就是把你要拦截实验的方法作为一个对象,存入到一个方法池列表中,使用完之后再主动开释掉。(须要注意生命周期,当页面Destory的时间要主动开释)
先界说方法对象
public abstract class IFunction {    public String functionName;    public IFunction(String functionName) {        this.functionName = functionName;    }    protected abstract void function();}方法池:
public class FunctionManager {    private static FunctionManager functionManager;    private static HashMap<String, IFunction> mFunctionMap;    public FunctionManager() {        mFunctionMap = new HashMap<>();    }    public static FunctionManager get() {        if (functionManager == null) {            functionManager = new FunctionManager();        }        return functionManager;    }    /**     * 添加方法     */    public FunctionManager addFunction(IFunction function) {        if (mFunctionMap != null) {            mFunctionMap.put(function.functionName, function);        }        return this;    }    /**     * 实验方法     */    public void invokeFunction(String key) {        if (TextUtils.isEmpty(key)) {            return;        }        if (mFunctionMap != null) {            IFunction function = mFunctionMap.get(key);            if (function != null) {                function.function();                //用完移撤消                removeFunction(key);            } else {                try {                    throw new RuntimeException("function not found");                } catch (Exception e) {                    e.printStackTrace();                }            }        }    }    /**     * 使用之后移除相干的缓存     */    public void removeFunction(String key) {        if (mFunctionMap != null) {            mFunctionMap.remove(key);        }    }}使用的时间也黑白常简朴
    private fun checkLogin() {        if (SP().getString(Constants.KEY_TOKEN, "").checkEmpty()) {            FunctionManager.get().addFunction(object : IFunction("gotoProfilePage") {                override fun function() {                    gotoProfilePage()                }            })            gotoLoginPage()        } else {            gotoProfilePage()        }    }登录完成之后,我们须要手动调用
    //方法池的方式    FunctionManager.get().invokeFunction("gotoProfilePage")如许就可以触发回调完成登录拦截的功能了。
假如想对游客的校验也做一个封装,也可以在 FunctionManager 中界说好,可以自由扩展。
二、消息回调方案

其本质是通过消息总线实现,通过管理类发送消息,吸取消息,通过回调的方式去实验拦截的方法。相比前者,他的长处是不须要我们处理惩罚生命周期。
我们指定好同一的消息key之后,都通过这个key来处理惩罚登录完成的逻辑
public class FunctionManager {    private static FunctionManager functionManager;    private static HashMap<String, Function> mFunctionMap;    public FunctionManager() {        mFunctionMap = new HashMap<>();    }    public static FunctionManager get() {        if (functionManager == null) {            functionManager = new FunctionManager();        }        return functionManager;    }    public void addLoginCallback(LifecycleOwner owner, ILoginCallback callback) {        LiveEventBus.get("login", Boolean.class).observe(owner, aBoolean -> {            if (aBoolean != null && aBoolean) {                callback.callback();            }        });    }    public interface ILoginCallback {        void callback();    }    public void finishLogin() {        LiveEventBus.get("login").post(true);    }} FunctionManager.get().addLoginCallback(this) {            gotoProfilePage()        }登录完成之后,我们须要手动调用
    //方法池的方式    FunctionManager.get().finishLogin()如许就可以触发回调完成登录拦截的功能了。
和方法池的方式又异曲同工之妙。
三、Intent的方案

实在不使用一些容器,我们原始的使用Intent也是可以实现逻辑的。
原理是通过登录乐成之后startActivity启动本身的页面,然后通过 onNewIntent 拿到对应的操纵意图去实验对应的操纵。
只是须要我们把原始的意图封装到启动本身的Intent中。
    fun switchPage3() {            f (!LoginManager.isLogin()) {            val intent = Intent(mActivity, Demo3Activity::class.java)            intent.addCategory(switch_tab3)            gotoLoginPage(intent)        } else {                switchFragment(3)        }    }    //把原始意图当参数通报    fun gotoLoginPage(targetIntent: Intent) {        val intent = Intent(mActivity, LoginDemoActivity::class.java)        intent.putExtra("targetIntent", targetIntent)        startActivity(intent)    }    //通过如许的方式可以拿到携带的数据    override fun onNewIntent(intent: Intent) {        super.onNewIntent(intent)        YYLogUtils.w("收到newintent:" + intent.toString())        val categories = intent.categories        when (categories.take(1)[0]) {            switch_tab1 -> {                switchFragment(1)            }            switch_tab2 -> {                switchFragment(2)            }            switch_tab3 -> {                switchFragment(3)            }        }    }那么在Login页面登录完成之后再启动当前页面即可把携带的数据通报返来,通过newIntent就可以做对应的操纵。
四、动态署理+Hook的方案

假如说Intent的方案还须要我们手动的处理惩罚跳转,那么此方案就是升级版,主动的拦截跳转,之后的放行方案我们照旧通过 Intent 与 onNewIntent 的回调来处理惩罚。
难点就是怎样使用Hook代替Activity的启动。
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 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);        }    }}使用的时间我们须要启动署理,在跳转页面的时间就会主动拦截了。
    mBtnProfile.click {        //启动动态署理         DynamicProxyUtils.hookAms()        gotoActivity<rofileDemoActivity>()    }之后的逻辑和上面的Intent方案是一样的回调处理惩罚,走 onNewIntent 里面处理惩罚。
现在的Hook只兼容到Android12。还没有看13的源码不知道有没有变动。而且此方案只能实用于页面的跳转,有些场景好比切换Tab、ViewPager的情况下,是无法实现拦截的。
假如不想全部的页面都拦截,各人也可以自行实现白名单的管理,只拦截部门的页面。
但相对其他方案来说实在不是很好用,如许的主动感觉还不如全手动的Intent机动。
五、Java线程方案

相对其他的方案,此方案的思绪就比力清奇,使用线程的等待与规复来实现,当我们跳转到登录页面的时间我们让线程等待,然后等待登录完成之后我们再规复等待。
/** * 登录拦截的线程管理 */public class LoginInterceptThreadManager  {    private static LoginInterceptThreadManager threadManager;    private static final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();    private static final Handler mHandler = new Handler();    private LoginInterceptThreadManager() {    }    public static LoginInterceptThreadManager get() {        if (threadManager == null) {            threadManager = new LoginInterceptThreadManager();        }        return threadManager;    }    /**     * 查抄是否须要登录     */    public void checkLogin(Runnable nextRunnable, Runnable loginRunnable) {        if (LoginManager.isLogin()) {            //已经登录            mHandler.post(nextRunnable);            return;        }        //假如没有登录-先去登录页面        mHandler.post(loginRunnable);        singleThreadExecutor.execute(() -> {            try {                YYLogUtils.w("开始运行-克制");                synchronized (singleThreadExecutor) {                    singleThreadExecutor.wait();                    YYLogUtils.w("等待notifyAll完成了,继续实验");                    if (LoginManager.isLogin()) {                        mHandler.post(nextRunnable);                    }                }            } catch (InterruptedException e) {                e.printStackTrace();            }        });    }    public void loginFinished() {        if (mHandler == null) return;        if (singleThreadExecutor == null) return;        synchronized (singleThreadExecutor) {            singleThreadExecutor.notifyAll();        }    }}使用的时间也简朴
    private fun checkLogin() {        LoginInterceptThreadManager.get().checkLogin( {            gotoProfilePage()        }, {            gotoLoginPage()        })    }    private fun gotoLoginPage() {        gotoActivity<LoginDemoActivity>()    }    private fun gotoProfilePage() {        gotoActivity<rofileDemoActivity>()    }登录完成之后,我们须要手动调用
    //方法池的方式    oginInterceptThreadManager.get().loginFinished()如许就可以触发回调完成登录拦截的功能了。
六、Kotlin协程方案

既然线程都可以,没原理协程不能使用如许的方案,协程也可以使用等待规复的方案,还能使用协程通讯的方案,开启两个协程,然后当登录完成之后去关照此中的吸取协程去继续实验。
class LoginInterceptCoroutinesManager private constructor() : DefaultLifecycleObserver, CoroutineScope by MainScope() {    companion object {        private var instance: LoginInterceptCoroutinesManager? = null            get() {                if (field == null) {                    field = LoginInterceptCoroutinesManager()                }                return field            }        fun get(): LoginInterceptCoroutinesManager {            return instance!!        }    }    private lateinit var mCancellableContinuation: CancellableContinuation<Boolean>    fun checkLogin(loginAction: () -> Unit, nextAction: () -> Unit) {        launch {            if (LoginManager.isLogin()) {                nextAction()                return@launch            }            loginAction()            val isLogin = suspendCancellableCoroutine<Boolean> {                mCancellableContinuation = it                YYLogUtils.w("暂停协程,等待叫醒")            }            YYLogUtils.w("已经规复协程,继续实验")            if (isLogin) {                nextAction()            }        }    }    fun loginFinished() {        if (!this@LoginInterceptCoroutinesManager::mCancellableContinuation.isInitialized) return        if (mCancellableContinuation.isCancelled) return        mCancellableContinuation.resume(LoginManager.isLogin(), null)    }    override fun onDestroy(owner: LifecycleOwner) {        YYLogUtils.w("LoginInterceptCoroutinesManager - onDestroy")        mCancellableContinuation.cancel()        cancel()    }}使用也比力简朴
       //协程的方式        mBtnProfile2.click {            LoginInterceptCoroutinesManager.get().checkLogin(loginAction = {                gotoLoginPage()            }, nextAction = {                gotoProfilePage()            })        }登录完成之后,我们须要手动调用
    //方法池的方式    oginInterceptThreadManager.get().loginFinished()如许就可以触发回调完成登录拦截的功能了。
协程另一种方案就是关照的方式:
class LoginInterceptCoroutinesManager private constructor() : DefaultLifecycleObserver, CoroutineScope by MainScope() {    companion object {        private var instance: LoginInterceptCoroutinesManager? = null            get() {                if (field == null) {                    field = LoginInterceptCoroutinesManager()                }                return field            }        fun get(): LoginInterceptCoroutinesManager {            return instance!!        }    }    private val channel = Channel<Boolean>()    fun checkLogin(loginAction: () -> Unit, nextAction: () -> Unit) {        launch {            if (LoginManager.isLogin()) {                nextAction()                return@launch            }            loginAction()            val isLogin = channel.receive()            YYLogUtils.w("收到消息:" + isLogin)            if (isLogin) {                nextAction()            }        }    }    fun loginFinished() {        launch {            async {                YYLogUtils.w("发送消息:" + LoginManager.isLogin())                channel.send(LoginManager.isLogin())            }        }    }    override fun onDestroy(owner: LifecycleOwner) {        cancel()    }}使用起来和暂停规复的方案是一样样的。
七、Aop切面方案

除了这些方案之外,网上比力盛行的就是面向切面AOP的方案。
须要我们集成 AspectJ 框架来实现。
使用的时间就须要界说一个自界说的注解,然后围绕这个注解做一些操纵。
//不须要回调的处理惩罚@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Login {}除了注解的类
@Aspectpublic class LoginAspect {    @Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")    public void Login() {    }    @Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.LoginCallback)")    public void LoginCallback() {    }    //带回调的注解处理惩罚    @Around("LoginCallback()")    public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {        YYLogUtils.w("走进AOP方法-LoginCallback()");        Signature signature = joinPoint.getSignature();        if (!(signature instanceof MethodSignature)){            throw new RuntimeException("该注解只能用于方法上");        }        LoginCallback loginCallback = ((MethodSignature) signature).getMethod().getAnnotation(LoginCallback.class);        if (loginCallback == null) return;        //判断当前是否已经登录        if (LoginManager.isLogin()) {            joinPoint.proceed();        } else {            LifecycleOwner lifecycleOwner = (LifecycleOwner) joinPoint.getTarget();            LiveEventBus.get("login").observe(lifecycleOwner, new Observer<Object>() {                @Override                public void onChanged(Object integer) {                    try {                        joinPoint.proceed();                        LiveEventBus.get("login").removeObserver(this);                    } catch (Throwable throwable) {                        throwable.printStackTrace();                        LiveEventBus.get("login").removeObserver(this);                    }                }            });            LoginManager.gotoLoginPage();        }    }    //不带回调的注解处理惩罚    @Around("Login()")    public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {        YYLogUtils.w("走进AOP方法-Login()");        Signature signature = joinPoint.getSignature();        if (!(signature instanceof MethodSignature)){            throw new RuntimeException("该注解只能用于方法上");        }        Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);        if (login == null) return;        //判断当前是否已经登录        if (LoginManager.isLogin()) {            joinPoint.proceed();        } else {            //假如未登录,去登录页面            LoginManager.gotoLoginPage();        }    }}界说一个工具类来界说一些固定的方法:
object LoginManager {    @JvmStatic    fun isLogin(): Boolean {        val token = SP().getString(Constants.KEY_TOKEN, "")        YYLogUtils.w("LoginManager-tokentoken")        val checkEmpty = token.checkEmpty()        return !checkEmpty    }    @JvmStatic    fun gotoLoginPage() {        commContext().gotoActivity<LoginDemoActivity>()    }}到这里我们就能使用AOP来拦截了。我们把须要拦截的方法使用我们的自界说注解来标志。然后我们的处理惩罚器就会对这个注解做一些围绕的操纵。
    override fun init() {        mBtnCleanToken.click {            SP().remove(Constants.KEY_TOKEN)            toast("扫除乐成")        }        mBtnProfile.click {           //不带回调的登录方式           gotoProfilePage2()        }    }    @Login    private fun gotoProfilePage2() {        gotoActivity<rofileDemoActivity>()    }可以看到内部也是通过消息总线来实验继续操纵的逻辑的,我们须要在登录完成之后发送这个关照才行。
八、拦截器的方案

末了一种方案是基于责任链模式的改版,自界说拦截器实现的,和默认的责任链是有些差别的。此中没有用到参数的通报。
原理是我们界说2层拦截,一个是校验登录,一个是实验逻辑。当我们校验登录不通过的时间就会跳转到登录页面,当登录完成之后,我们继续拦截器就会走到实验逻辑。间接的完成一个登录拦截的功能。
拦截器的界说
object LoginInterceptChain {    private var index: Int = 0    private val interceptors by lazy(LazyThreadSafetyMode.NONE) {        ArrayList<Interceptor>(2)    }    //默认初始化Login的拦截器    private val loginIntercept = LoginInterceptor()    // 实验拦截器。    fun process() {        if (interceptors.isEmpty()) return        when (index) {            in interceptors.indices -> {                val interceptor = interceptors[index]                index++                interceptor.intercept(this)            }            interceptors.size -> {                clearAllInterceptors()            }        }    }    // 添加一个拦截器。    fun addInterceptor(interceptor: Interceptor): LoginInterceptChain {        //默认添加Login判断的拦截器        if (!interceptors.contains(loginIntercept)) {            interceptors.add(loginIntercept)        }        if (!interceptors.contains(interceptor)) {            interceptors.add(interceptor)        }        return this    }    //放行登录判断拦截器    fun loginFinished() {        if (interceptors.contains(loginIntercept) && interceptors.size > 1) {            loginIntercept.loginfinished()        }    }    //扫除全部的拦截器    private fun clearAllInterceptors() {        index = 0        interceptors.clear()    }}校验登录的拦截器:
/** * 判断是否登录的拦截器 */class LoginInterceptor : BaseLoginInterceptImpl() {    override fun intercept(chain: LoginInterceptChain) {        super.intercept(chain)        if (LoginManager.isLogin()) {            //假如已经登录 -> 放行, 转交给下一个拦截器            chain.process()        } else {            //假如未登录 -> 去登录页面            LoginDemoActivity.startInstance()        }    }    fun loginfinished() {        //假如登录完成,调用方法放行到下一个拦截器        mChain?.process()    }}继续实验的拦截器:
/** * 登录完成下一步的拦截器 */class LoginNextInterceptor(private val action: () -> Unit) : BaseLoginInterceptImpl() {    override fun intercept(chain: LoginInterceptChain) {        super.intercept(chain)        if (LoginManager.isLogin()) {            //假如已经登录实验当前的任务            action()        }        mChain?.process()    }}使用的时间我们使用拦截器管理即可
    private fun checkLogin() {        LoginInterceptChain.addInterceptor(LoginNextInterceptor {            gotoProfilePage()        }).process()    }登录完成之跋文得手动放行哦
    //拦截器放行    LoginInterceptChain.loginFinished()如许就完成了登录拦截的功能了。
总结

本文是一个总纲或者说是总结,这里的几种方法我都只是简朴的先容了一下,详细的使用可以看看单独的文章,每一篇详细使用的方式之前都已经出了对应的文章,并附带了Demo,有爱好的朋侪可从前往查察。
总的来说实现这种方式保举各人使用简朴易于明白和集成使用的方式。例如方法池,消息关照回调,线程协程的方案,自界说拦截的方案实在都是不错的,各人本身按需选择即可。
除开一些集成困难,有兼容性的一些方案之外,其他的这些方案都是可以用的了,剩下的我们须要思量的就是,此方案是否有更大的内存开销,是否有内存泄漏风险,须要处理惩罚页面不测关闭的情况吗?有没有降级或兜底的方案?有没有瓦解的风险?有没有重复调用的风险?等等等等。
本文也只是基于Demo的实现,假如正式在生产上面使用的话,各人可以自行扩展一下它的坚固性。
本文全部代码均以开源,源码在此。各人可以点个Star关注一波,有题目我会及时更新。
好了,本期内容如有错漏的地方,盼望同砚们可以指出交换。假如有更好的方法,也接待各人批评区讨论。
假如感觉本文对你有一点点的开导,还望你能点赞支持一下,你的支持是我最大的动力。
作者:newki
链接:https://juejin.cn/post/7143040409558581262
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 18:23, Processed in 0.181698 second(s), 32 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表