version 0.5
[TOC]
前言
前阶段公司的业务比较忙,很长时间没有整理出来什么博客.最近刚空出些时间,简单再整理一些记录.
产生原因:
客户端是被动向服务器查询登录状态,一些网络请求需要一个刷新token来验证客户端是否处于登录态,是则可以进行用户操作,否则做登出操作.
刚开始直接单纯的每个请求刷新token,刷新token,然后请求是没什么问题的.
但是随着版本迭代,任务增多,有些时候,比如app首次启动, 会进行一些列用户相关操作, 比如拉取用户信息, 拉取特定的活动项目,这样一个刷新token的操作可能会并发, 而我们的服务端刷新token每次可能都会不一样,这就产生了一些问题. 以下会展开示意.
之前的方案(并发刷新token)
为了简化理解我画了几张图,来说明情况,为了表示并发,我用RequestA,RequestB,RequestC分别表示三个请求,Server表示服务端

可以看见理想状态下,其实是没什么问题,请求都能正常收到与发送,但前提是他们是只有当A请求完全完成后B的后续请求,刷新token才不会受到干扰.
实际情况
然而,提到了随着业务增多,实际中大多请求都是并发的,于是乎就有可能有下面的情况.

可以看到,实际中,很有可能产生,A,B同时刷新token,而在A拿到新Token A去再一次请求时,B已经从服务器拿到了Token B导致了A请求又一次失败,随着并发的增多这种失败的可能性越来越多.
改进思路
我所想的是全局有一个单例的线程来掌管整个Token的刷新,并且这个token的刷新不是并发,而是队列,但是又不能让之后的请求变成队列. 否则简单的将所有请求变成队列即可,但实际情况我们根本不会让请求都是串行,无论从用户体验还是代码的书写上都是不好的.
所以在与iOS端讨论后,我们决定使用单一管理,并可阻塞的队列方式来管理token的请求过程,确保app内不会并发发送请求token的过程.

改进后就是主要几点:
- 当A请求过期后,需要向
TokenManager去请求token. 
TokenManager会阻塞住队列,让后来的B请求等待. 
- 当刷新完成后,通知所有队列中的对象,因为这个
TokenManager只负责刷新返回token一个职责 
- 所有请求拿到新的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
   |  tokenThread = new HandlerThread("token-handlerThread"); tokenThread.start();
 
   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
   | 
 
 
 
  private void doGetOrRefreshToken(RefreshTokenListener listener) {          if (checkIsTokenExpired()) {         if (mContext != null) {                          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 {                           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(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 |