通过HandlerThread保证网络请求成功率(线程间通信)

碰到一个bug,因为服务器在国内,国外用户在使用时可能因为网络原因即使请求发送了,但是服务器依旧没有收到上传的数据,需要确保客户端把请求成功发送。
因此我的思路是把失败的请求收集起来,添加到请求失败队列中,在一段周期性时间后对队列中所有失败请求重新进行一次请求,如果成功就从队列中删除,失败则等待进行下一次的请求。

开始想到的是Timer,但是查到timer存在bug。

如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。Timer线程并不捕获异常,所以TimerTask抛出的未检查的异常会终止timer线程。这种情况下,Timer也不会再重新恢复线程的执行了;它错误的认为整个Timer都被取消了。此时,已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。

然后想到Java提供的四种线程池中的newScheduledThreadPool

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

//线程池能按时间计划来执行任务,允许用户设定计划执行任务的时间,int类型的参数是设定  
//线程池中线程的最小数目。当任务较多时,线程池可能会自动创建更多的工作线程来执行任务  
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
scheduledThreadPool.schedule(new Runnable() {  
  
    @Override  
    public void run() {  
        System.out.println("delay 3 seconds");  
    }  
}, 3, TimeUnit.SECONDS);  
//表示延迟3秒执行

定期执行示例代码如下:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {  
  
    @Override  
    public void run() {  
        System.out.println("delay 1 seconds, and excute every 3 seconds");  
    }  
}, 1, 3, TimeUnit.SECONDS);  
//表示延迟1秒后每3秒执行一次

ScheduledExecutorService比Timer更安全,功能更强大,满心欢喜的在Java项目下测试了半天,觉得可以使用,但是一到Android项目中就出问题。
当在一次请求的失败回调中执行周期task,task竟然失效并且捕捉到了 Can’t create handler inside thread that has not called Looper.prepare() 异常

@Override
public void fail(Object result) {
    executorService.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
            UpdateBusStatusTime updateBusStatusTime = new UpdateBusStatusTime();
            //网络请求
             GetHttpResultTool tool = new GetHttpResultTool(updateBusStatusTime);
             tool.setCallback(new GetHttpResultTool.CallBack() {
                 @Override
                  public void success(Object result) {
                       throw new NullPointerException();
                   }

                  @Override
                  public void fail(Object result) {}
              });
              tool.setPost(true);
              tool.execute();
         }
   },5,5,TimeUnit.SECONDS);
}

去掉okhttp的访问请求则一切正常,应该是okhttp的锅,没办法接着换,最终找到了HandlerThread

HandlerThread

HandlerThread继承于Thread,所以它本质就是个Thread。与普通Thread的差别就在于,它不仅建立了一个线程,并且创立了消息队列,有自己的looper,可以让我们在自己的线程中分发和处理消息,并对外提供自己这个Looper对象的get方法。
HandlerThread自带Looper使他可以通过消息队列,来重复使用当前线程。当有耗时任务投放到该循环线程中时,线程执行耗时任务,执行完之后循环线程处于等待状态,直到下一个新的耗时任务被投放进来。这样一来就避免了多次创建Thread线程导致的性能问题了。
这是它的优点也是缺点,每一个任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。

HandlerThread的常规用法

1.在onCreate方法中进行初始化

protected void onCreate(Bundle savedInstanceState) {
   //创建一个HandlerThread的实例对象
   mCheckMsgThread= new HandlerThread("handler_thread");
   //启动一个HandlerThread线程
   mCheckMsgThread.start();
   //构建子线程中的Handler
   mCheckMsgHandler = new Handler(mCheckMsgThread.getLooper(), mSubCallback);
   //延迟1分钟进行循环检测是否有失败请求
   mCheckMsgHandler.sendEmptyMessageDelayed(1, 60 * 1000);
}

2.构建循环消息处理机制

  private Handler.Callback mSubCallback = new Handler.Callback() {
        //该接口的实现就是处理异步耗时任务的,因此该方法执行在子线程中
        @Override
        public boolean handleMessage(Message msg) {
            checkForUpdate();
            //每5分钟进行一次重试
            mCheckMsgHandler.sendEmptyMessageDelayed(1, 5 * 60 * 1000);
            return false;
        }
    };
   /**
    * 重复执行请求
    */
    private void checkForUpdate() {
        if (errorQueueMap != null && errorQueueMap.size() > 0) {
            for (final Map.Entry<UpdateBusStatusTime, Boolean> entry : errorQueueMap.entrySet()) {
                if (!entry.getValue()) {
                    //失败后重新执行网络请求
                    final GetHttpResultTool tool = new GetHttpResultTool(entry.getKey());
                    tool.setCallback(new GetHttpResultTool.CallBack() {
                        @Override
                        public void success(final Object result) {
                            Log.i("", "网络成功,循环结束 " + result);
                            entry.setValue(true);
                            //我做完了,现在将消息通知回主线程
                            mUIHandler .sendEmptyMessage(2) ;
                        }
                        @Override
                        public void fail(Object result) {
                            Log.i("", "网络请求失败,继续执行循环 " + result);
                        }
                    });
                }
            }
        }
    }

3.构建UI线程Handler处理消息

    private Handler mUIHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //当子线程的handler处理完事件后,通知在UI线程中做的事情,比如弹个吐司提醒用户啊
        }
    };

4.退出循环
Looper是通过调用loop方法驱动着消息循环的进行: 从MessageQueue中阻塞式地取出一个消息,然后让Handler处理该消息,周而复始,loop方法是个死循环方法。因此在onDestroy的时候需要退出释放资源。

mCheckMsgThread.quit() ;
原生线程间通信

略提一下,抛开Handler在原生Java中其实也有办法做线程间通信。只是方法要么不够优雅,要么会造成cpu运算负荷超高或死锁等情况。而且经常因为技术不到位,导致翻车的情况。所以android中的Hanlder通信机制非常巧妙的避开这样的问题,提供一下原生方法参考学习。

  • 共享内存变量 使用同步锁(容易死锁)
  • 共享内存变量 判断变量状态 (极其消耗内存)
  • 管道通信
总结
  1. HandelrThread是一个自带Looper的线程,因此只能作为子线程使用,因此也实现了能够循环执行网络请求操作
  2. HandlerThread处理任务是串行执行,按消息发送顺序进行处理,里面的网络请求是异步的与此并不冲突。
  3. 通过mCheckMsgHandler = new Handler(mCheckMsgThread.getLooper(), mSubCallback) 这句代码mCheckMsgHandler 获得了HandlerThread线程的Looper,这样就把他们联系到一起。而这句代码之所以在 mCheckMsgThread.start()后执行,是因为start后run()方法才能跑起来,Looper才能够创建,mCheckMsgThread.getLooper()才能够正确得到。
  4. 通过HandlerThread联系子线程的Handler和UI线程的Handler实现了线程间通信。

参考
http://www.cnblogs.com/wufeng0927/p/5374191.html
http://www.jianshu.com/p/69c826c8a87d
http://blog.csdn.net/m0_37837382/article/details/70143224

    原文作者:hongjay
    原文地址: https://www.jianshu.com/p/43e2796a43c5
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞