前段時間在組內(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)行约素。
然而。笆凌。
在實(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ù)雜义锥。讓我來慢慢解釋。
我們以一個實(shí)際的app例子開始喇勋,讓我們看看youtube的app退訂頻道功能:
用戶點(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)說币绩,很多情況下,寫操作不能取消府阀,讀操作可以缆镣。
很多情況下,比如支付试浙,訂閱等等這種用戶場景董瞻,需要涉及到異步操作的都是會有以上的問題。在這些場景下,我們需要遵循以下流程钠糊。
最最重點(diǎn)的部分挟秤,就是當(dāng)用戶退出的時候雖然我們停止更新UI,但當(dāng)用戶重新進(jìn)入的時候抄伍,app需要主動的重新向后臺發(fā)送請求艘刚,查看當(dāng)前訂閱狀態(tài)。這樣截珍,才是一個健康的app攀甚。
所以很遺憾,RxJava并沒有很好的支持這一場景岗喉,至于怎么解決秋度,有什么框架比較合適,下一章再介紹钱床。