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

登录拦截与放行是大部分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 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 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[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);
        }

    }
}

使用的时候我们需要启动代理,在跳转页面的时候就会自动拦截了。

    mBtnProfile.click {

        //启动动态代理
         DynamicProxyUtils.hookAms()

        gotoActivity()
    }

之后的逻辑和上面的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()
    }

    private fun gotoProfilePage() {
        gotoActivity()
    }

登录完成之后,我们需要手动调用

    //方法池的方式
    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

    fun checkLogin(loginAction: () -> Unit, nextAction: () -> Unit) {

        launch {

            if (LoginManager.isLogin()) {
                nextAction()
                return@launch
            }

            loginAction()

            val isLogin = suspendCancellableCoroutine {

                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()


    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 {
}

除了注解的类

@Aspect
public 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() {
                @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-token:$token")
        val checkEmpty = token.checkEmpty()
        return !checkEmpty
    }

    @JvmStatic
    fun gotoLoginPage() {
        commContext().gotoActivity()
    }
}

到这里我们就能使用AOP来拦截了。我们把需要拦截的方法使用我们的自定义注解来标记。然后我们的处理器就会对这个注解做一些围绕的操作。

    override fun init() {

        mBtnCleanToken.click {
            SP().remove(Constants.KEY_TOKEN)
            toast("清除成功")
        }

        mBtnProfile.click {

           //不带回调的登录方式
           gotoProfilePage2()
        }

    }

    @Login
    private fun gotoProfilePage2() {
        gotoActivity()
    }

可以看到内部也是通过消息总线来执行继续操作的逻辑的,我们需要在登录完成之后发送这个通知才行。

八、拦截器的方案

最后一种方案是基于责任链模式的改版,自定义拦截器实现的,和默认的责任链是有些差异的。其中没有用到参数的传递。
原理是我们定义2层拦截,一个是校验登录,一个是执行逻辑。当我们校验登录不通过的时候就会跳转到登录页面,当登录完成之后,我们继续拦截器就会走到执行逻辑。间接的完成一个登录拦截的功能。
拦截器的定义

object LoginInterceptChain {

    private var index: Int = 0

    private val interceptors by lazy(LazyThreadSafetyMode.NONE) {
        ArrayList(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()

这样就完成了登录拦截的功能了。

来自:链接:https://juejin.cn/post/7143040409558581262

你可能感兴趣的:(Android登录拦截场景-多种实现方式)