前言
本文是基于之前的系列文章做的一個合集,精簡之后整理為一篇長文供大家參考耘成。合集的入口在此。合集內(nèi)部有每種方案的詳細(xì)使用手冊,大家可以對照本文參考使用。
登錄攔截與放行是大部分App開發(fā)都會遇到的一個場景,如果你的App有游客模式堡称,但是部分高級功能需要登錄之后才能使用。
那么我們就需要在用戶點(diǎn)擊這個操作的時候校驗(yàn)是否登錄艺演,當(dāng)?shù)卿浲瓿芍笤偬D(zhuǎn)到指定的頁面或彈窗却紧。如果這些入口很多的話,那么我們就需要到處寫這些邏輯胎撤。比較初級的用法是使用消息總線晓殊,當(dāng)?shù)卿浲瓿芍蟀l(fā)送對應(yīng)key消息,然后去完成對應(yīng)key的事件伤提。
有沒有一種更簡單的方式巫俺,集中統(tǒng)一方便的管理登錄攔截再放行這一個場景。
下面我們一起來看一看具體的方案肿男。
一介汹、方法池方案
本質(zhì)就是把你要攔截執(zhí)行的方法作為一個對象,存入到一個方法池列表中舶沛,使用完之后再自動釋放掉嘹承。(需要注意生命周期,當(dāng)頁面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();
}
}
}
}
/**
* 使用之后移除相關(guān)的緩存
*/
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()
}
}
登錄完成之后如庭,我們需要手動調(diào)用
//方法池的方式
FunctionManager.get().invokeFunction("gotoProfilePage")
這樣就可以觸發(fā)回調(diào)完成登錄攔截的功能了赶撰。
如果想對游客的校驗(yàn)也做一個封裝,也可以在 FunctionManager 中定義好柱彻,可以自由擴(kuò)展。
二餐胀、消息回調(diào)方案
其本質(zhì)是通過消息總線實(shí)現(xiàn)哟楷,通過管理類發(fā)送消息,接收消息否灾,通過回調(diào)的方式去執(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()
}
登錄完成之后,我們需要手動調(diào)用
//方法池的方式
FunctionManager.get().finishLogin()
這樣就可以觸發(fā)回調(diào)完成登錄攔截的功能了扣汪。
和方法池的方式又異曲同工之妙断楷。
三、Intent的方案
其實(shí)不使用一些容器崭别,我們原始的使用Intent也是可以實(shí)現(xiàn)邏輯的冬筒。
原理是通過登錄成功之后startActivity啟動自己的頁面恐锣,然后通過 onNewIntent 拿到對應(yīng)的操作意圖去執(zhí)行對應(yīng)的操作。
只是需要我們把原始的意圖封裝到啟動自己的Intent中舞痰。
fun switchPage3() {
f (!LoginManager.isLogin()) {
val intent = Intent(mActivity, Demo3Activity::class.java)
intent.addCategory(switch_tab3)
gotoLoginPage(intent)
} else {
switchFragment(3)
}
}
//把原始意圖當(dāng)參數(shù)傳遞
fun gotoLoginPage(targetIntent: Intent) {
val intent = Intent(mActivity, LoginDemoActivity::class.java)
intent.putExtra("targetIntent", targetIntent)
startActivity(intent)
}
//通過這樣的方式可以拿到攜帶的數(shù)據(jù)
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頁面登錄完成之后再啟動當(dāng)前頁面即可把攜帶的數(shù)據(jù)傳遞回來土榴,通過newIntent就可以做對應(yīng)的操作。
四响牛、動態(tài)代理+Hook的方案
如果說Intent的方案還需要我們手動的處理跳轉(zhuǎn)玷禽,那么此方案就是升級版,自動的攔截跳轉(zhuǎn)呀打,之后的放行方案我們還是通過 Intent 與 onNewIntent 的回調(diào)來處理矢赁。
難點(diǎn)就是如何使用Hook代替Activity的啟動。
public class DynamicProxyUtils {
//修改啟動模式
public static void hookAms() {
try {
Field singletonField;
Class<?> iActivityManager;
// 1聚磺,獲取Instrumentation中調(diào)用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));
//現(xiàn)在替換掉這個對象
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);
//設(shè)置新的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);
}
}
}
使用的時候我們需要啟動代理蜒蕾,在跳轉(zhuǎn)頁面的時候就會自動攔截了。
mBtnProfile.click {
//啟動動態(tài)代理
DynamicProxyUtils.hookAms()
gotoActivity<ProfileDemoActivity>()
}
之后的邏輯和上面的Intent方案是一樣的回調(diào)處理焕阿,走 onNewIntent 里面處理咪啡。
目前的Hook只兼容到Android12。還沒有看13的源碼不知道有沒有變動暮屡。并且此方案只能適用于頁面的跳轉(zhuǎn)撤摸,有些場景比如切換Tab、ViewPager的情況下褒纲,是無法實(shí)現(xiàn)攔截的准夷。
如果不想全部的頁面都攔截,大家也可以自行實(shí)現(xiàn)白名單的管理莺掠,只攔截部分的頁面衫嵌。
但相對其他方案來說其實(shí)不是很好用,這樣的自動感覺還不如全手動的Intent靈活彻秆。
五楔绞、Java線程方案
相對其他的方案,此方案的思路就比較清奇唇兑,利用線程的等待與恢復(fù)來實(shí)現(xiàn)酒朵,當(dāng)我們跳轉(zhuǎn)到登錄頁面的時候我們讓線程等待,然后等待登錄完成之后我們再恢復(fù)等待扎附。
/**
* 登錄攔截的線程管理
*/
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()) {
//已經(jīng)登錄
mHandler.post(nextRunnable);
return;
}
//如果沒有登錄-先去登錄頁面
mHandler.post(loginRunnable);
singleThreadExecutor.execute(() -> {
try {
YYLogUtils.w("開始運(yùn)行-停止");
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>()
}
登錄完成之后蔫耽,我們需要手動調(diào)用
//方法池的方式
oginInterceptThreadManager.get().loginFinished()
這樣就可以觸發(fā)回調(diào)完成登錄攔截的功能了。
六留夜、Kotlin協(xié)程方案
既然線程都可以针肥,沒道理協(xié)程不能使用這樣的方案饼记,協(xié)程也可以使用等待恢復(fù)的方案,還能使用協(xié)程通信的方案慰枕,開啟兩個協(xié)程具则,然后當(dāng)?shù)卿浲瓿芍笕ネㄖ渲械慕邮諈f(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("已經(jīng)恢復(fù)協(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()
})
}
登錄完成之后,我們需要手動調(diào)用
//方法池的方式
oginInterceptThreadManager.get().loginFinished()
這樣就可以觸發(fā)回調(diào)完成登錄攔截的功能了蜂厅。
協(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()
}
}
使用起來和暫头朔玻恢復(fù)的方案是一樣樣的。
七掘猿、Aop切面方案
除了這些方案之外病游,網(wǎng)上比較流行的就是面向切面AOP的方案。
需要我們集成 AspectJ 框架來實(shí)現(xiàn)稠通。
使用的時候就需要定義一個自定義的注解衬衬,然后圍繞這個注解做一些操作。
//不需要回調(diào)的處理
@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() {
}
//帶回調(diào)的注解處理
@Around("LoginCallback()")
public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走進(jìn)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;
//判斷當(dāng)前是否已經(jīng)登錄
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();
}
}
//不帶回調(diào)的注解處理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走進(jìn)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;
//判斷當(dāng)前是否已經(jīng)登錄
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來攔截了滋尉。我們把需要攔截的方法使用我們的自定義注解來標(biāo)記。然后我們的處理器就會對這個注解做一些圍繞的操作飞主。
override fun init() {
mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}
mBtnProfile.click {
//不帶回調(diào)的登錄方式
gotoProfilePage2()
}
}
@Login
private fun gotoProfilePage2() {
gotoActivity<ProfileDemoActivity>()
}
可以看到內(nèi)部也是通過消息總線來執(zhí)行繼續(xù)操作的邏輯的狮惜,我們需要在登錄完成之后發(fā)送這個通知才行。
八碌识、攔截器的方案
最后一種方案是基于責(zé)任鏈模式的改版碾篡,自定義攔截器實(shí)現(xiàn)的,和默認(rèn)的責(zé)任鏈?zhǔn)怯行┎町惖姆げ汀F渲袥]有用到參數(shù)的傳遞耽梅。
原理是我們定義2層攔截,一個是校驗(yàn)登錄胖烛,一個是執(zhí)行邏輯。當(dāng)我們校驗(yàn)登錄不通過的時候就會跳轉(zhuǎn)到登錄頁面诅迷,當(dāng)?shù)卿浲瓿芍笈宸覀兝^續(xù)攔截器就會走到執(zhí)行邏輯。間接的完成一個登錄攔截的功能罢杉。
攔截器的定義
object LoginInterceptChain {
private var index: Int = 0
private val interceptors by lazy(LazyThreadSafetyMode.NONE) {
ArrayList<Interceptor>(2)
}
//默認(rèn)初始化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 {
//默認(rèn)添加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()
}
}
校驗(yàn)登錄的攔截器:
/**
* 判斷是否登錄的攔截器
*/
class LoginInterceptor : BaseLoginInterceptImpl() {
override fun intercept(chain: LoginInterceptChain) {
super.intercept(chain)
if (LoginManager.isLogin()) {
//如果已經(jīng)登錄 -> 放行, 轉(zhuǎn)交給下一個攔截器
chain.process()
} else {
//如果未登錄 -> 去登錄頁面
LoginDemoActivity.startInstance()
}
}
fun loginfinished() {
//如果登錄完成滩租,調(diào)用方法放行到下一個攔截器
mChain?.process()
}
}
繼續(xù)執(zhí)行的攔截器:
/**
* 登錄完成下一步的攔截器
*/
class LoginNextInterceptor(private val action: () -> Unit) : BaseLoginInterceptImpl() {
override fun intercept(chain: LoginInterceptChain) {
super.intercept(chain)
if (LoginManager.isLogin()) {
//如果已經(jīng)登錄執(zhí)行當(dāng)前的任務(wù)
action()
}
mChain?.process()
}
}
使用的時候我們使用攔截器管理即可
private fun checkLogin() {
LoginInterceptChain.addInterceptor(LoginNextInterceptor {
gotoProfilePage()
}).process()
}
登錄完成之后記得手動放行哦
//攔截器放行
LoginInterceptChain.loginFinished()
這樣就完成了登錄攔截的功能了赋秀。
總結(jié)
本文是一個總綱或者說是總結(jié)利朵,這里的幾種方法我都只是簡單的介紹了一下,具體的使用可以看看單獨(dú)的文章猎莲,每一篇具體使用的方式之前都已經(jīng)出了對應(yīng)的文章绍弟,并附帶了Demo,有興趣的朋友可以前往查看著洼。
總的來說實(shí)現(xiàn)這種方式推薦大家使用簡單易于理解和集成使用的方式樟遣。例如方法池,消息通知回調(diào)身笤,線程協(xié)程的方案豹悬,自定義攔截的方案其實(shí)都是不錯的,大家自己按需選擇即可液荸。
除開一些集成困難瞻佛,有兼容性的一些方案之外,其他的這些方案都是可以用的了娇钱,剩下的我們需要考慮的就是伤柄,此方案是否有更大的內(nèi)存開銷,是否有內(nèi)存泄露風(fēng)險(xiǎn)忍弛,需要處理頁面意外關(guān)閉的情況嗎响迂?有沒有降級或兜底的方案?有沒有崩潰的風(fēng)險(xiǎn)细疚?有沒有重復(fù)調(diào)用的風(fēng)險(xiǎn)蔗彤?等等等等。
本文也只是基于Demo的實(shí)現(xiàn)疯兼,如果正式在生產(chǎn)上面使用的話然遏,大家可以自行擴(kuò)展一下它的健壯性。
本文全部代碼均以開源吧彪,源碼在此待侵。大家可以點(diǎn)個Star關(guān)注一波,有問題我會及時更新姨裸。
好了秧倾,本期內(nèi)容如有錯漏的地方,希望同學(xué)們可以指出交流傀缩。如果有更好的方法那先,也歡迎大家評論區(qū)討論。
如果感覺本文對你有一點(diǎn)點(diǎn)的啟發(fā)赡艰,還望你能點(diǎn)贊
支持一下,你的支持是我最大的動力售淡。
作者:newki