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

前段時間在組內(nèi)做了一下現(xiàn)有的代碼分析私股,發(fā)現(xiàn)很多以前的legacy code多線程的使用都不算是最佳實(shí)踐削樊,而且壞事的地方在于,剛畢業(yè)的學(xué)生譬巫,因?yàn)闆]有別的參照物,往往會復(fù)制粘貼以前的舊代碼督笆,這就造成了壞習(xí)慣不停的擴(kuò)散芦昔。所以本人就總結(jié)分析了一下Android的多線程技術(shù)選型,還有應(yīng)用場景娃肿。借著和組內(nèi)分享的機(jī)會也在簡書上總結(jié)一下咕缎。因?yàn)樽约旱募夹g(shù)水平有限珠十,有不對的地方還希望大家能多多指正。(代碼的例子方面凭豪,肯定不能用我們自己組內(nèi)產(chǎn)品的源代碼焙蹭,簡書上的都是我修改過的)

這篇文章我會先分析一些大家可能踩過的雷區(qū),然后再列出一些可以改進(jìn)的地方墅诡。

誤區(qū)

1.在代碼中直接創(chuàng)建新的Thread.

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

            }
        }).start();

以上的做法是非常不可取的壳嚎,缺點(diǎn)非常的多,想必大部分朋友面試的時候都會遇到這種問題末早,分析一下為啥不可以烟馅。浪費(fèi)線程資源是第一,最重要的是我們無法控制該線程的執(zhí)行然磷,因此可能會造成不必要的內(nèi)存泄漏郑趁。在Activity或者Fragment這種有生命周期的控件里面直接執(zhí)行這段代碼,相信大部分人都知道會可能有內(nèi)存泄漏姿搜。但是就算在其他的設(shè)計模式寡润,比如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{}
}

比如圖中的一段代碼(我標(biāo)記了引用方向)梭纹,通常MVP里面的View都是一個接口,但是接口的實(shí)現(xiàn)可能是Activity致份。那么在代碼中就可能存在內(nèi)存泄漏了变抽。Thread的runnable是匿名內(nèi)部類,持有presenter的引用氮块,presenter持有view的引用绍载。這里的引用鏈就會造成內(nèi)存泄漏了。關(guān)鍵是,就算你持有線程的句柄滔蝉,也無法把這個引用關(guān)系給解除击儡。

所以優(yōu)秀的設(shè)計模式也阻止不了內(nèi)存泄漏。蝠引。阳谍。。立肘。

2.頻繁使用HandlerThread

雖然HandlerThread是安卓framework的親兒子边坤,但是在實(shí)際的開發(fā)過程中卻很少能有他的適用之處。HandlerThread繼承于Thread類谅年,所以每次開啟一個HandlerThread就和開啟一個普通Thread一樣茧痒,很浪費(fèi)資源。我們可以通過使用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旺订,但是已經(jīng)丟失了handler的句柄弄企,那么我們利用handler thread再構(gòu)建一個handler
        Handler handler = new Handler(thread.getLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                //do something
            }
        });
    }

綜上所述,HandlerThread最屌的地方就在于区拳,只要你還有它的句柄拘领,你可以隨時拿到在該線程下創(chuàng)建的Looper對象,用于生成一個Handler樱调。之后post的所有runnable都可以在該HandlerThread下運(yùn)行约素。
然而。笆凌。

download.jpeg

在實(shí)際的開發(fā)中圣猎,我們好像很難找到這么一個需求,要在指定的一個線程下執(zhí)行某些任務(wù)乞而。注意了是指定的一個送悔,不是一些(線程池)。唯一比Thread厲害的地方恐怕就是可以取消未執(zhí)行的任務(wù)爪模,減少內(nèi)存泄漏的情況了吧欠啤。不過個人觀點(diǎn)是線程池好像也可以做到。所以并沒有察覺 HandlerThread有任何的優(yōu)勢屋灌。而且其實(shí)實(shí)現(xiàn)也很簡單洁段,我們可以隨時手寫一個簡陋版的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();
        //發(fā)一個消息
        Runnable r = new Runnable() {
            @Override
            public void run() {

            }
        };
        thread.post(r);
        //不想執(zhí)行了。共郭。眉撵。。刪掉
        thread.clearOneMessage(r);
    }

看分分鐘完成HandlerThread能做到的一切落塑。。罐韩。憾赁。是不是很簡單。

3.直接使用AsyncTask.execute()

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

個人認(rèn)為AsyncTask的設(shè)計暴露了這個接口方法谷歌做的非常不恰當(dāng)散吵。它這樣允許開發(fā)者直接使用AsyncTask本身的線程池龙考,我們可以看看源代碼做驗(yàn)證

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

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

這樣的問題在于晦款,這樣使用完全喪失了AsyncTask本身的意圖。個人的觀點(diǎn)是枚冗,AsyncTask提供了一個后臺任務(wù)切換到主線程的通道缓溅,就像RxJava的subscribeOn/observeOn一樣,同時提供cancel方法赁温,可以取消掉切換回主線程執(zhí)行的代碼坛怪,從而防止內(nèi)存泄漏淤齐。

  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本身設(shè)計的初衷下的優(yōu)勢更啄,和直接自己創(chuàng)建一個線程池沒有任何區(qū)別,還存在內(nèi)存泄漏的風(fēng)險居灯。這樣的用法祭务,肯定不能稱之為best practice.

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

這個誤區(qū)標(biāo)題起的有點(diǎn)模糊,這個沒辦法怪嫌,因?yàn)槔佑悬c(diǎn)點(diǎn)復(fù)雜义锥。讓我來慢慢解釋。


download (1).jpeg

我們以一個實(shí)際的app例子開始喇勋,讓我們看看youtube的app退訂頻道功能:


IMG_9443.jpg

用戶點(diǎn)擊退訂按鈕之后缨该,app發(fā)出api call,告訴后臺我們停止訂閱該頻道川背,同時把UI更新為progress bar贰拿,當(dāng)api call結(jié)束,在api的回調(diào)里面我們更新UI控件顯示已退訂UI熄云。我們寫一個示例代碼看看:

完美膨更!

但是萬一用戶在點(diǎn)擊退訂按鈕,但是api call還沒發(fā)出去之前就退出了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進(jìn)行unsubscribe矗漾,防止內(nèi)存泄漏
        subscription.unsubscribe();
    }
}

看似好像沒啥問題,沒有內(nèi)存泄漏薄料,可以后臺線程和主線程直接靈活切換敞贡,更新UI不會crash。而且我們使用了Schedulers.io()調(diào)度器摄职,看似也沒有浪費(fèi)線程資源誊役。

BUT!9仁小;坠浮!F扔啤鹏漆!

我們先仔細(xì)想想一個問題。我們在點(diǎn)擊button之后,我們的Observable

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

會立刻執(zhí)行么甫男?

答案是NO且改。因?yàn)槲覀兊腛bservable是subscribeOn io線程池。如果該線程池現(xiàn)在非常擁擠板驳,這段代碼又跛,這個Observable是不會立刻執(zhí)行的。該段代碼會華麗麗的躺在線程池的隊(duì)列中若治,安安靜靜的等待輪到自己執(zhí)行慨蓝。

那么如果用戶點(diǎn)擊按鈕,同時退出app端幼,我們unubscribe了這個RxJava 的observable 我們就存在一個不會執(zhí)行api call的風(fēng)險礼烈。也就是用戶點(diǎn)擊退訂按鈕,退出app婆跑,返回app的時候此熬,會發(fā)現(xiàn),咦滑进,怎么明明點(diǎn)了退訂犀忱,竟然還是訂閱狀態(tài)?

這就回到了一個本質(zhì)問題扶关,來自靈魂的拷問阴汇。是不是所有異步調(diào)用,都需要和Activity或者fragment的生命周期綁定节槐?

答案同樣是NO搀庶,在很多應(yīng)用場景下,當(dāng)用戶做出一個行為的時候铜异,我們必須堅(jiān)定不移的執(zhí)行該行為背后的一切操作哥倔,至于異步操作完成之后的UI更新,則視當(dāng)前Activity或者fragment的生命周期決定揍庄。也就是異步操作和生命周期無關(guān)未斑,UI更新和生命周期有關(guān)。簡單點(diǎn)說币绩,很多情況下,寫操作不能取消府阀,讀操作可以缆镣。

很多情況下,比如支付试浙,訂閱等等這種用戶場景董瞻,需要涉及到異步操作的都是會有以上的問題。在這些場景下,我們需要遵循以下流程钠糊。


Screen Shot 2019-07-04 at 7.17.00 PM.png

最最重點(diǎn)的部分挟秤,就是當(dāng)用戶退出的時候雖然我們停止更新UI,但當(dāng)用戶重新進(jìn)入的時候抄伍,app需要主動的重新向后臺發(fā)送請求艘刚,查看當(dāng)前訂閱狀態(tài)。這樣截珍,才是一個健康的app攀甚。

所以很遺憾,RxJava并沒有很好的支持這一場景岗喉,至于怎么解決秋度,有什么框架比較合適,下一章再介紹钱床。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荚斯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子查牌,更是在濱河造成了極大的恐慌事期,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僧免,死亡現(xiàn)場離奇詭異刑赶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)懂衩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門撞叨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人浊洞,你說我怎么就攤上這事牵敷。” “怎么了法希?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵枷餐,是天一觀的道長。 經(jīng)常有香客問我苫亦,道長毛肋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任屋剑,我火速辦了婚禮润匙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唉匾。我一直安慰自己孕讳,他們只是感情好匠楚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厂财,像睡著了一般芋簿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上璃饱,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天与斤,我揣著相機(jī)與錄音,去河邊找鬼帜平。 笑死幽告,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裆甩。 我是一名探鬼主播冗锁,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嗤栓!你這毒婦竟也來了冻河?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤茉帅,失蹤者是張志新(化名)和其女友劉穎叨叙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堪澎,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡擂错,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了樱蛤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钮呀。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖昨凡,靈堂內(nèi)的尸體忽然破棺而出爽醋,到底是詐尸還是另有隱情,我是刑警寧澤便脊,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布蚂四,位于F島的核電站,受9級特大地震影響哪痰,放射性物質(zhì)發(fā)生泄漏遂赠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一晌杰、第九天 我趴在偏房一處隱蔽的房頂上張望跷睦。 院中可真熱鬧,春花似錦乎莉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哼鬓。三九已至,卻和暖如春边灭,著一層夾襖步出監(jiān)牢的瞬間异希,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工绒瘦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留称簿,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓惰帽,卻偏偏與公主長得像憨降,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子该酗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350