Android多線程技術選型最全指南(part 1 - 誤區)

前段時間在組內作了一下現有的代碼分析,發現不少之前的legacy code多線程的使用都不算是最佳實踐,並且壞事的地方在於,剛畢業的學生,由於沒有別的參照物,每每會複製粘貼之前的舊代碼,這就形成了壞習慣不停的擴散。因此本人就總結分析了一下Android的多線程技術選型,還有應用場景。藉着和組內分享的機會也在簡書上總結一下。由於本身的技術水平有限,有不對的地方還但願你們能多多指正。(代碼的例子方面,確定不能用咱們本身組內產品的源代碼,簡書上的都是我修改過的)java

這篇文章我會先分析一些你們可能踩過的雷區,而後再列出一些能夠改進的地方。面試

誤區

1.在代碼中直接建立新的Thread.

new Thread(new Runnable() {
            @Override
            public void run() {

            }
        }).start();
複製代碼

以上的作法是很是不可取的,缺點很是的多,想必大部分朋友面試的時候都會遇到這種問題,分析一下爲啥不能夠。浪費線程資源是第一,最重要的是咱們沒法控制該線程的執行,所以可能會形成沒必要要的內存泄漏。在Activity或者Fragment這種有生命週期的控件裏面直接執行這段代碼,相信大部分人都知道會可能有內存泄漏。可是就算在其餘的設計模式,好比MVP,一樣也可能會遇到這個問題。設計模式

//runnable->presenter->view
public 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的引用。這裏的引用鏈就會形成內存泄漏了。關鍵是,就算你持有線程的句柄,也沒法把這個引用關係給解除。api

因此優秀的設計模式也阻止不了內存泄漏。。。。。bash

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下運行。 然而。。 app

download.jpeg
在實際的開發中,咱們好像很難找到這麼一個需求,要在指定的 一個線程下執行某些任務。注意了是指定的一個,不是一些(線程池)。惟一比Thread厲害的地方恐怕就是能夠取消未執行的任務,減小內存泄漏的狀況了吧。不過我的觀點是線程池好像也能夠作到。因此並無察覺 HandlerThread有任何的優點。並且其實實現也很簡單,咱們能夠隨時手寫一個簡陋版的HandlerThread.

public static class DemoThread extends Thread{
        private LinkedBlockingQueue<Runnable> 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自己的線程池,咱們能夠看看源代碼作驗證異步

@MainThread
    public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }
複製代碼

果不其然,execute直接訪問了executor。async

這樣的問題在於,這樣使用徹底喪失了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能包治百病

這個誤區標題起的有點模糊,這個沒辦法,由於例子有點點複雜。讓我來慢慢解釋。

download (1).jpeg

咱們以一個實際的app例子開始,讓咱們看看youtube的app退訂頻道功能:

IMG_9443.jpg

用戶點擊退訂按鈕以後,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<Void>() {
                    @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<Void>() {
                            @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更新和生命週期有關。簡單點說,不少狀況下,寫操做不能取消,讀操做能夠。

不少狀況下,好比支付,訂閱等等這種用戶場景,須要涉及到異步操做的都是會有以上的問題。在這些場景下,咱們須要遵循如下流程。

Screen Shot 2019-07-04 at 7.17.00 PM.png
最最重點的部分,就是當用戶退出的時候雖然咱們中止更新UI,但當用戶從新進入的時候,app須要主動的從新向後臺發送請求,查看當前訂閱狀態。這樣,纔是一個健康的app。

因此很遺憾,RxJava並無很好的支持這一場景,至於怎麼解決,有什麼框架比較合適,下一章再介紹。

相關文章
相關標籤/搜索