前段時間在組內做了一下現有的程式碼分析,發現很多以前的legacy code多執行緒的使用都不算是最佳實踐,而且壞事的地方在於,剛畢業的學生,因為沒有別的參照物,往往會複製貼上以前的舊程式碼,這就造成了壞習慣不停的擴散。所以本人就總結分析了一下Android的多執行緒技術選型,還有應用場景。藉著和組內分享的機會也在簡書上總結一下。因為自己的技術水平有限,有不對的地方還希望大家能多多指正。(程式碼的例子方面,肯定不能用我們自己組內產品的原始碼,簡書上的都是我修改過的)
這篇文章我會先分析一些大家可能踩過的雷區,然後再列出一些可以改進的地方。
誤區
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的引用。這裡的引用鏈就會造成記憶體洩漏了。關鍵是,就算你持有執行緒的控制程式碼,也無法把這個引用關係給解除。
所以優秀的設計模式也阻止不了記憶體洩漏。。。。。
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下執行。
然而。。
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。
這樣的問題在於,這樣使用完全喪失了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<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更新和生命週期有關。簡單點說,很多情況下,寫操作不能取消,讀操作可以。
很多情況下,比如支付,訂閱等等這種使用者場景,需要涉及到非同步操作的都是會有以上的問題。在這些場景下,我們需要遵循以下流程。
最最重點的部分,就是當使用者退出的時候雖然我們停止更新UI,但當使用者重新進入的時候,app需要主動的重新向後臺傳送請求,檢視當前訂閱狀態。這樣,才是一個健康的app。所以很遺憾,RxJava並沒有很好的支援這一場景,至於怎麼解決,有什麼框架比較合適,下一章再介紹。