美文网首页小技巧Android开发经验谈Android知识
通过HandlerThread保证网络请求成功率(线程间通信)

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

作者: hongjay | 来源:发表于2017-07-27 14:59 被阅读517次

    碰到一个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

    相关文章

      网友评论

      本文标题:通过HandlerThread保证网络请求成功率(线程间通信)

      本文链接:https://www.haomeiwen.com/subject/hamzkxtx.html