安卓全局单例刷新Token

安卓全局单例刷新Token

三月 20, 2019

version 0.5

[TOC]

前言

前阶段公司的业务比较忙,很长时间没有整理出来什么博客.最近刚空出些时间,简单再整理一些记录.

产生原因:

客户端是被动向服务器查询登录状态,一些网络请求需要一个刷新token来验证客户端是否处于登录态,是则可以进行用户操作,否则做登出操作.

刚开始直接单纯的每个请求刷新token,刷新token,然后请求是没什么问题的.

但是随着版本迭代,任务增多,有些时候,比如app首次启动, 会进行一些列用户相关操作, 比如拉取用户信息, 拉取特定的活动项目,这样一个刷新token的操作可能会并发, 而我们的服务端刷新token每次可能都会不一样,这就产生了一些问题. 以下会展开示意.

之前的方案(并发刷新token)

为了简化理解我画了几张图,来说明情况,为了表示并发,我用RequestA,RequestB,RequestC分别表示三个请求,Server表示服务端

~~理想状态~~刷新token

可以看见理想状态下,其实是没什么问题,请求都能正常收到与发送,但前提是他们是只有当A请求完全完成后B的后续请求,刷新token才不会受到干扰.

实际情况

然而,提到了随着业务增多,实际中大多请求都是并发的,于是乎就有可能有下面的情况.

实际刷新token

可以看到,实际中,很有可能产生,A,B同时刷新token,而在A拿到新Token A去再一次请求时,B已经从服务器拿到了Token B导致了A请求又一次失败,随着并发的增多这种失败的可能性越来越多.

改进思路

我所想的是全局有一个单例的线程来掌管整个Token的刷新,并且这个token的刷新不是并发,而是队列,但是又不能让之后的请求变成队列. 否则简单的将所有请求变成队列即可,但实际情况我们根本不会让请求都是串行,无论从用户体验还是代码的书写上都是不好的.

所以在与iOS端讨论后,我们决定使用单一管理,并可阻塞的队列方式来管理token的请求过程,确保app内不会并发发送请求token的过程.

修改后刷新机制.png

改进后就是主要几点:

  1. 当A请求过期后,需要向TokenManager去请求token.
  2. TokenManager会阻塞住队列,让后来的B请求等待.
  3. 当刷新完成后,通知所有队列中的对象,因为这个TokenManager只负责刷新返回token一个职责
  4. 所有请求拿到新的token,再来并发执行而互不影响.

代码概要

主要是实现一个任务队列,并要求阻塞, 因为刷新token也是一个异步请求,所以可以用wait()来阻塞住,当一次请求完成后,使用notify()来让队列继续执行,然后再加入一个超时规则,一段时间内,不会重新刷新token,加快之后的token请求

那么在安卓中,HandlerThread内部已经有了一个loop的实现,就很方便处理这中情景,而不必要自己去写一些任务队列与Loop,简化代码量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//单独开辟一个线程来处理looper
tokenThread = new HandlerThread("token-handlerThread");
tokenThread.start();

//通过Handler来处理消息
mHandler = new Handler(tokenThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg != null && msg.obj != null) {
switch (msg.what) {
case WITH_RETRY:
doGetOrRefreshTokenWithRetry(msg.arg1, (RefreshTokenListener) msg.obj);
break;
case FORCE:
doForceRefreshToken((RefreshTokenListener) msg.obj);
break;
case NORMAL:
default:
doGetOrRefreshToken((RefreshTokenListener) msg.obj);
break;
}
} else {
AILog.d(TAG, "getTokenHandler : msg is null");
}

}
};

初始化Handler去操作不同的请求方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 直接获取或刷新token
*
* @param listener
*/
private void doGetOrRefreshToken(RefreshTokenListener listener) {
//AILog.d(TAG, "doGetOrRefreshToken: ");
if (checkIsTokenExpired()) {
if (mContext != null) {
//使用CountDownLatch来阻塞
CountDownLatch latch = new CountDownLatch(1);
MobileAccount.getInst().refreshToken(mContext, new MobileAccount.RefreshCallback() {
@Override
public void onSuccess(int code, String refreshedToken) {
userUpload(refreshedToken);
lastToken = refreshedToken;
if (listener != null) {
listener.onSuccess(refreshedToken);
}
lastRefreshTime = System.currentTimeMillis();
//去掉阻塞
latch.countDown();
}

@Override
public void doLogout(int code) {
AILog.e(TAG, "doGetOrRefreshToken doLogout() called with: code = [" + code + "]");
if (listener != null) {
listener.doLogout(code);
}
clear();
//去掉阻塞
latch.countDown();
}

@Override
public void onError(int errorCode) {
AILog.e(TAG, "doGetOrRefreshToken onError() called with: errorCode = [" + errorCode + "]");
if (listener != null) {
listener.onError(errorCode);
}
clear();
//去掉阻塞
latch.countDown();
}
});
//阻塞线程,并设置超时等待
waitUntilNotify(latch);
}
} else {
//AILog.d(TAG, "doGetOrRefreshToken with not fresh: " + lastToken);
//直接获得
listener.onSuccess(lastToken);
}

}

当任务执行到访问网络刷新token时,通过信号量wait()阻塞住任务,当收到回调时notify()去执行,为了防止超时,内部起了一个定时器.(已弃用Lock方式,发现有极少情况可能在加锁之前请求已经过来,导致锁一直不释放)

采用CountDownLatch来阻塞,防止之前如果速度过快导致lock时已经notify的过程,同时提升可读性

1
2
3
4
5
6
7
8
9
10
11
12
13
private void waitUntilNotify(CountDownLatch latch) {
if (latch != null) {
//判断是否还需要加锁
if (latch.getCount() > 0) {
try {
// latch.await();
latch.await(REFRESH_EXPIRED_TIME, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

阻塞当前线程,并等待一定时长,防止过多请求服务器导致token快速过期。以上是核心部分的简要说明。

外部通过RefreshTokenListener来处理token的回调,做相应的处理.
相对于外部请求,依然是无感知TokenManager的存在.

外部调用时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TokenManager.getInstance().getToken(new TokenManager.RefreshTokenListener() {
@Override
public void onSuccess(String token) {

}

@Override
public void doLogOut(int code) {

}

@Override
public void onError(int code) {

}
});

更新说明:

版本 时间 说明
version 0.1 2019年03月20日11:29:20 初版
version 0.2 2019年03月22日09:56:52 修改TokenManager图表
version 0.3 2019年03月28日10:21:02 优化代码
version 0.4 2020年04月28日18:12:59 改用CountDownLatch简化
version 0.5 2022年02月08日17:49:01 更换图床为Github