通过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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容