登錄攔截與放行是大部分App開發(fā)都會遇到的一個場景钱床,如果你的App有游客模式誉券,但是部分高級功能需要登錄之后才能使用击吱。
那么我們就需要在用戶點擊這個操作的時候校驗是否登錄虑椎,當登錄完成之后再跳轉到指定的頁面或彈窗榆鼠。如果這些入口很多的話纲爸,那么我們就需要到處寫這些邏輯。比較初級的用法是使用消息總線妆够,當登錄完成之后發(fā)送對應key消息识啦,然后去完成對應key的事件。
有沒有一種更簡單的方式神妹,集中統(tǒng)一方便的管理登錄攔截再放行這一個場景颓哮。
下面我們一起來看一看具體的方案。
一鸵荠、方法池方案
本質就是把你要攔截執(zhí)行的方法作為一個對象冕茅,存入到一個方法池列表中,使用完之后再自動釋放掉蛹找。(需要注意生命周期姨伤,當頁面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;
}
/**
* 執(zhí)行方法
*/
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")
這樣就可以觸發(fā)回調完成登錄攔截的功能了庸疾。
如果想對游客的校驗也做一個封裝乍楚,也可以在 FunctionManager 中定義好,可以自由擴展届慈。
二徒溪、消息回調方案
其本質是通過消息總線實現凌箕,通過管理類發(fā)送消息,接收消息词渤,通過回調的方式去執(zhí)行攔截的方法牵舱。相比前者,他的好處是不需要我們處理生命周期缺虐。
我們指定好統(tǒng)一的消息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()
這樣就可以觸發(fā)回調完成登錄攔截的功能了高氮。
和方法池的方式又異曲同工之妙慧妄。
三、Intent的方案
其實不使用一些容器剪芍,我們原始的使用Intent也是可以實現邏輯的塞淹。
原理是通過登錄成功之后startActivity啟動自己的頁面,然后通過 onNewIntent 拿到對應的操作意圖去執(zhí)行對應的操作罪裹。
只是需要我們把原始的意圖封裝到啟動自己的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就可以做對應的操作状共。
四套耕、動態(tài)代理+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;
}
//開始動態(tài)代理
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{iActivityManager},
new AmsHookBinderInvocationHandler(mInstance));
//現在替換掉這個對象
mInstanceField.set(singleton, proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
//動態(tài)代理執(zhí)行類
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 {
//啟動動態(tài)代理
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完成了,繼續(xù)執(zhí)行");
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()
這樣就可以觸發(fā)回調完成登錄攔截的功能了。
六垒迂、Kotlin協(xié)程方案
既然線程都可以械姻,沒道理協(xié)程不能使用這樣的方案,協(xié)程也可以使用等待恢復的方案机断,還能使用協(xié)程通信的方案楷拳,開啟兩個協(xié)程,然后當登錄完成之后去通知其中的接收協(xié)程去繼續(xù)執(zhí)行毫缆。
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("暫停協(xié)程唯竹,等待喚醒")
}
YYLogUtils.w("已經恢復協(xié)程乐导,繼續(xù)執(zhí)行")
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()
}
}
使用也比較簡單
//協(xié)程的方式
mBtnProfile2.click {
LoginInterceptCoroutinesManager.get().checkLogin(loginAction = {
gotoLoginPage()
}, nextAction = {
gotoProfilePage()
})
}
登錄完成之后苦丁,我們需要手動調用
//方法池的方式
oginInterceptThreadManager.get().loginFinished()
這樣就可以觸發(fā)回調完成登錄攔截的功能了。
協(xié)程另一種方案就是通知的方式:
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("發(fā)送消息:" + 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>()
}
可以看到內部也是通過消息總線來執(zhí)行繼續(xù)操作的邏輯的偎巢,我們需要在登錄完成之后發(fā)送這個通知才行蔼夜。
八、攔截器的方案
最后一種方案是基于責任鏈模式的改版压昼,自定義攔截器實現的求冷,和默認的責任鏈是有些差異的瘤运。其中沒有用到參數的傳遞。
原理是我們定義2層攔截匠题,一個是校驗登錄拯坟,一個是執(zhí)行邏輯。當我們校驗登錄不通過的時候就會跳轉到登錄頁面韭山,當登錄完成之后似谁,我們繼續(xù)攔截器就會走到執(zhí)行邏輯。間接的完成一個登錄攔截的功能掠哥。
攔截器的定義
object LoginInterceptChain {
private var index: Int = 0
private val interceptors by lazy(LazyThreadSafetyMode.NONE) {
ArrayList<Interceptor>(2)
}
//默認初始化Login的攔截器
private val loginIntercept = LoginInterceptor()
// 執(zhí)行攔截器巩踏。
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()
}
}
繼續(xù)執(zhí)行的攔截器:
/**
* 登錄完成下一步的攔截器
*/
class LoginNextInterceptor(private val action: () -> Unit) : BaseLoginInterceptImpl() {
override fun intercept(chain: LoginInterceptChain) {
super.intercept(chain)
if (LoginManager.isLogin()) {
//如果已經登錄執(zhí)行當前的任務
action()
}
mChain?.process()
}
}
使用的時候我們使用攔截器管理即可
private fun checkLogin() {
LoginInterceptChain.addInterceptor(LoginNextInterceptor {
gotoProfilePage()
}).process()
}
登錄完成之后記得手動放行哦
//攔截器放行
LoginInterceptChain.loginFinished()
這樣就完成了登錄攔截的功能了塞琼。