前言

前段时间在组内做了一下现有的代码分析,发现很多以前的legacy code多线程的使用都不算是最佳实践,而且坏事的地方在于,刚毕业的学生,因为没有别的参照物,往往会复制粘贴以前的旧代码,这就造成了坏习惯不停的扩散。所以本人就总结分析了一下Android的多线程技术选型,还有应用场景。借着和组内分享的机会也在简书上总结一下。因为自己的技术水平有限,有不对的地方还希望大家能多多指正。

这篇文章我会先分析一些大家可能踩过的雷区,然后再列出一些可以改进的地方。

1. 在代码中直接创建新的Thread.

new Thread(new Runnable() {      @Override      public void run() {      }  }).start();

以上的做法是非常不可取的,缺点非常的多,想必大部分朋友面试的时候都会遇到这种问题,分析一下为啥不可以。浪费线程资源是第一,最重要的是我们无法控制该线程的执行,因此可能会造成不必要的内存泄漏。在Activity或者Fragment这种有生命周期的控件里面直接执行这段代码,相信大部分人都知道会可能有内存泄漏。但是就算在其他的设计模式,比如MVP,同样也可能会遇到这个问题。

//runnable->presenter->viewpublic class Presenter {    //持有view引用    private IView view;    public Presenter(IView v){        this.view = v;    }    public void doSomething(String[] args){        new Thread(new Runnable() {            @Override            public void run() {                /**                ** 持有presenter引用                **/                //do something            }        }).start();    }    public static interface IView{}}

比如图中的一段代码(我标记了引用方向),通常MVP里面的View都是一个接口,但是接口的实现可能是Activity。那么在代码中就可能存在内存泄漏了。Thread的runnable是匿名内部类,持有presenter的引用,presenter持有view的引用。这里的引用链就会造成内存泄漏了。关键是,就算你持有线程的句柄,也无法把这个引用关系给解除。

所以优秀的设计模式也阻止不了内存泄漏。。。。。

2. 频繁使用HandlerThread

虽然HandlerThread是安卓framework的亲儿子,但是在实际的开发过程中却很少能有他的适用之处。HandlerThread继承于Thread类,所以每次开启一个HandlerThread就和开启一个普通Thread一样,很浪费资源。我们可以通过使用HandlerThread的例子来分析他最大的作用是什么。

static HandlerThread thread = new HandlerThread("test");static {    thread.start();}public void testHandlerThread(){    Handler handler = new Handler(thread.getLooper());    handler.post(new Runnable() {        @Override        public void run() {                //do something        }    });    //如果不需要了就remove handler's message    handler.removeCallbacksAndMessages(null);}    public void test(){    //如果我还想利用HandlerThread,但是已经丢失了handler的句柄,那么我们利用handler thread再构建一个handler    Handler handler = new Handler(thread.getLooper());    handler.post(new Runnable() {        @Override        public void run() {           //do something        }    });}

综上所述,HandlerThread最屌的地方就在于,只要你还有它的句柄,你可以随时拿到在该线程下创建的Looper对象,用于生成一个Handler。之后post的所有runnable都可以在该HandlerThread下运行。然而。。

在实际的开发中,我们好像很难找到这么一个需求,要在指定的一个线程下执行某些任务。注意了是指定的一个,不是一些(线程池)。唯一比Thread厉害的地方恐怕就是可以取消未执行的任务,减少内存泄漏的情况了吧。不过个人观点是线程池好像也可以做到。所以并没有察觉 HandlerThread有任何的优势。而且其实实现也很简单,我们可以随时手写一个简陋版的HandlerThread.

public static class DemoThread extends Thread{    private LinkedBlockingQueue queue  = new LinkedBlockingQueue<>();  @Override  public void run() {    super.run();      while(true){        if(!queue.isEmpty()){           Runnable runnable;           synchronized (this){               runnable = queue.poll();           }           if(runnable!= null) {                runnable.run();           }         }      }  }  public synchronized void post(Runnable runnable){    queue.add(runnable);  }  public synchronized void clearAllMessage(){    queue.clear();  }         public synchronized void clearOneMessage(Runnable runnable){    for(Runnable runnable1 : queue){      if(runnable == runnable1){        queue.remove(runnable);       }      }    }   }  public void testDemoThread(){    DemoThread thread = new DemoThread();    thread.start();     //发一个消息     Runnable r = new Runnable() {        @Override        public void run() {           }        };     thread.post(r);    //不想执行了。。。。删掉    thread.clearOneMessage(r);  }

看分分钟完成HandlerThread能做到的一切。。。。是不是很简单。

3.直接使用AsyncTask.execute()

AsyncTask.execute(new Runnable() {  @Override  public void run() {  }});

个人认为AsyncTask的设计暴露了这个 接口方法 谷歌做的非常不恰当。它这样允许开发者直接使用AsyncTask本身的线程池,我们可以看看源代码做验证

@MainThreadpublic static void execute(Runnable runnable) {     sDefaultExecutor.execute(runnable); }

果不其然,execute直接访问了executor。

这样的问题在于,这样使用完全丧失了AsyncTask本身的意图。个人的观点是,AsyncTask提供了一个后台任务切换到主线程的通道,就像RxJava的subscribeOn/observeOn一样,同时提供cancel方法,可以取消掉切换回主线程执行的代码,从而防止内存泄漏。

AsyncTask asyncTask = new AsyncTask() {    @Override    protected Object doInBackground(Object[] objects) {         return null;    }    @Override    protected void onPostExecute(Object o) {         //1.提供了后台线程切换回主线程的方法           super.onPostExecute(o);        }     };            //2.可以随时取消   asyncTask.cancel(true);

But!如果直接使用execute方法的话,我们完全没有利用到AsyncTask本身设计的初衷下的优势,和直接自己创建一个线程池没有任何区别,还存在内存泄漏的风险。这样的用法,肯定不能称之为best practice.

4. 以为RxJava的unsubscribe能包治百病

这个误区标题起的有点模糊,这个没办法,因为例子有点点复杂。让我来慢慢解释。

我们以一个实际的app例子开始,让我们看看youtube的app退订频道功能:

用户点击退订按钮之后,app发出api call,告诉后台我们停止订阅该频道,同时把UI更新为progress bar,当api call结束,在api的回调里面我们更新UI控件显示已退订UI。我们写一个示例代码看看:完美!

但是万一用户在点击退订按钮,但是api call还没发出去之前就退出了app呢?

public class YoutubePlayerActivity extends Activity {    private Subscription subscription;    public void setUnSubscribeListner(){        unsubscribeButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                subscription = Observable.create(new Observable.OnSubscribe() {                    @Override                    public void call(Subscriber<? super Void> subscriber) {                        try {                            //在这里我们做取消订阅的API, http                            API api = new API();                            api.unSubscribe();                        }                        catch (Exception e){                            subscriber.onError(e);                        }                        subscriber.onNext(null);                        subscriber.onCompleted();                    }                })                 .subscribeOn(Schedulers.io())                 .observeOn(AndroidSchedulers.mainThread())                 .subscribe(new Action1() {                      @Override                      public void call(Void aVoid) {                          //API call成功!,在这里更新订阅button的ui                          unsubscribeButton.toggleSubscriptionStatus();                      }                 });            }        });    }    @Override    protected void onDestroy() {        super.onDestroy();       //onDestroy 里面对RxJava stream进行unsubscribe,防止内存泄漏        subscription.unsubscribe();    }}

看似好像没啥问题,没有内存泄漏,可以后台线程和主线程直接灵活切换,更新UI不会crash。而且我们使用了Schedulers.io()调度器,看似也没有浪费线程资源。

BUT!!!!!!

我们先仔细想想一个问题。我们在点击button之后,我们的Observable

API api = new API();api.unSubscribe();

会立刻执行么?

答案是NO。因为我们的Observable是subscribeOn io线程池。如果该线程池现在非常拥挤,这段代码,这个Observable是不会立刻执行的。该段代码会华丽丽的躺在线程池的队列中,安安静静的等待轮到自己执行。

那么如果用户点击按钮,同时退出app,我们unubscribe了这个RxJava 的observable 我们就存在一个不会执行api call的风险。也就是用户点击退订按钮,退出app,返回app的时候,会发现,咦,怎么明明点了退订,竟然还是订阅状态?

这就回到了一个本质问题,来自灵魂的拷问。是不是所有异步调用,都需要和Activity或者fragment的生命周期绑定?

答案同样是NO,在很多应用场景下,当用户做出一个行为的时候,我们必须坚定不移的执行该行为背后的一切操作,至于异步操作完成之后的UI更新,则视当前Activity或者fragment的生命周期决定。也就是异步操作和生命周期无关,UI更新和生命周期有关。简单点说,很多情况下,写操作不能取消,读操作可以。

很多情况下,比如支付,订阅等等这种用户场景,需要涉及到异步操作的都是会有以上的问题。在这些场景下,我们需要遵循以下流程。

最最重点的部分,就是当用户退出的时候虽然我们停止更新UI,但当用户重新进入的时候,app需要主动的重新向后台发送请求,查看当前订阅状态。这样,才是一个健康的app。

文章不易,如果大家喜欢这篇文章,或者对你有帮助希望大家多多, 点赞转发关注 。文章会持续更新的。绝对干货!!!

更多相关文章

  1. 【Cocos2dx通信(Http&Socket)相关编译到Android细节总结】编译加
  2. android线程 Handler Message Queue AsyncTask
  3. AsyncTask原理及不足
  4. 一个android应用效果开发引发的惨案(android Home按键)
  5. android Server及IntentServer
  6. android线程 Handler Message Queue AsyncTask线程模型 线程交互
  7. android 设置Alpha值实现图片渐变效果
  8. 避免Drawable保持引用的内存泄露
  9. 学习笔记(七)多线程开发

随机推荐

  1. mysql视图功能与用法实例分析
  2. mysql多表联合查询操作实例分析
  3. Ubuntu移除mysql后重新安装的方法
  4. MySQL读取Binlog日志常见的3种错误
  5. 详解sql中的参照完整性(一对一,一对多,多对
  6. MySQL DML语句整理汇总
  7. 软件测试-MySQL(六:数据库函数)
  8. 腾讯面试:一条SQL语句执行得很慢的原因有
  9. MySQL常用类型转换函数总结(推荐)
  10. 详解mysql解压缩版安装步骤