我报名参与金石计划1期应战——分割10万奖池,这是我的第1篇文章,点击检查活动详情

前言

本文是根据之前的系列文章做的一个合集,精简之后整理为一篇长文供咱们参阅。合集的进口在此。合集内部有每种计划的详细运用手册,咱们能够对照本文参阅运用。

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

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

登录结束之后,咱们需求手动调用

    //办法池的办法
    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 {
}

除了注解的类

@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<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-token:$token")
        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<ProfileDemoActivity>()
    }

能够看到内部也是经过音讯总线来履行继续操作的逻辑的,咱们需求在登录结束之后发送这个告诉才行。

八、阻拦器的计划

最后一种计划是根据职责链形式的改版,自界说阻拦器结束的,和默许的职责链是有些差异的。其间没有用到参数的传递。

原理是咱们界说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()

这样就结束了登录阻拦的功用了。

下面放一张效果图,其实几种计划的效果都是差不多的:

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

总结

本文是一个总纲或许说是总结,这儿的几种办法我都仅仅简略的介绍了一下,详细的运用能够看看单独的文章,每一篇详细运用的办法之前都现已出了对应的文章,并附带了Demo,有兴趣的朋友能够前往检查。

总的来说结束这种办法引荐咱们运用简略易于了解和集成运用的办法。例如办法池,音讯告诉回调,线程协程的计划,自界说阻拦的计划其实都是不错的,咱们自己按需挑选即可。

除开一些集成困难,有兼容性的一些计划之外,其他的这些计划都是能够用的了,剩余的咱们需求考虑的便是,此计划是否有更大的内存开销,是否有内存走漏危险,需求处理页面意外关闭的情况吗?有没有降级或兜底的计划?有没有崩溃的危险?有没有重复调用的危险?等等等等。

本文也仅仅根据Demo的结束,假如正式在生产上面运用的话,咱们能够自行扩展一下它的健壮性。

本文悉数代码均以开源,源码在此。咱们能够点个Star关注一波,有问题我会及时更新。

好了,本期内容如有错漏的地方,希望同学们能够指出交流。假如有更好的办法,也欢迎咱们评论区讨论。

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

Ok,这一期就此结束。

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