轉(zhuǎn)載請注明出處:
地址:http://www.reibang.com/p/e0190611c25c
目錄
android中我們需要很小心對待線程的創(chuàng)建取胖齐、監(jiān)聽、取消。如果不小心處理,可能就會引入內(nèi)存泄漏,監(jiān)聽的生命周期與宿主不一致導(dǎo)致crash,頻繁創(chuàng)建線程對資源的消耗,線程無意義的運行等問題满力。那么這里對于線程中斷,源碼分析一下glide對其的優(yōu)化轻纪。對于線程創(chuàng)建/監(jiān)聽的選擇油额,總結(jié)了一些知識點。
1. 線程中斷
1. 線程中斷(取消)的方法
我們知道刻帚,如果一個線程已經(jīng)被廢棄了(沒有監(jiān)聽者了)潦嘶,那么線程就沒有繼續(xù)運行的必要了。如果只是取消監(jiān)聽者崇众,這么做肯定是不夠的掂僵,因為線程還在運行。所以我們需要中斷線程的運行來節(jié)省CPU顷歌,而線程的中斷并不是一件容易的事看峻。大體的方法是:
如果使用Thread.interrupt(),那么當(dāng)線程處于阻塞狀態(tài)時(比如wait住衙吩,sleep),那么線程會拋出InterruptException異常溪窒,退出循環(huán)坤塞。我們需要捕獲這個異常冯勉,進(jìn)行相應(yīng)處理,不然就crash了摹芙。
如果是非阻塞情況下灼狰,Thread.interrupt()是不能把線程中斷的。這時候只能設(shè)置volitie關(guān)鍵字來中斷線程浮禾。
這兩種情況需要配合使用來中斷已經(jīng)廢棄且還在運行的線程交胚。
Glide是一個很優(yōu)秀的圖片加載庫。在處理大量圖片上盈电,其做了很多的優(yōu)化蝴簇,那么我們看下其對線程的取消(取消圖片的網(wǎng)絡(luò)加載)做了哪些事情:
2.源碼分析glide對線程中斷的優(yōu)化
glide中EngineJob中:
public void removeCallback(ResourceCallback cb) {
Util.assertMainThread();
if (hasResource || hasException) {
addIgnoredCallback(cb);
} else {
cbs.remove(cb);
if (cbs.isEmpty()) {
cancel();
}
}
}
void cancel() {
if (hasException || hasResource || isCancelled) {
return;
}
engineRunnable.cancel();
Future currentFuture = future;
if (currentFuture != null) {
currentFuture.cancel(true);
}
isCancelled = true;
listener.onEngineJobCancelled(this, key);
}
當(dāng)一個圖片加載任務(wù)EngineJob已經(jīng)沒有監(jiān)聽者時,會調(diào)用future的cancel()方法匆帚。future是提交給線程池任務(wù)返回的熬词。當(dāng)調(diào)用future的cancel(true)時,如果任務(wù)還沒執(zhí)行吸重,那么就取消任務(wù)互拾。如果任務(wù)已經(jīng)執(zhí)行,但被阻塞了嚎幸,那么會調(diào)用Thread的interrupt()方法中斷線程颜矿。
EngineJob中往線程池拋的task是:EngineRunnable,當(dāng)調(diào)用其cancel()時:
private volatile boolean isCancelled;
public void cancel() {
isCancelled = true;
decodeJob.cancel();
}
@Override
public void run() {
if (isCancelled) {
return;
}
Exception exception = null;
Resource<?> resource = null;
try {
resource = decode();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}
if (isCancelled) {
if (resource != null) {
resource.recycle();
}
return;
}
if (resource == null) {
onLoadFailed(exception);
} else {
onLoadComplete(resource);
}
}
使用了volatile關(guān)鍵字來讓runnable的方法不再執(zhí)行嫉晶。當(dāng)task還在隊列中還沒有執(zhí)行的話(這應(yīng)該經(jīng)常發(fā)生骑疆,如果發(fā)送的圖片請求太多),那么直接return车遂。如果已經(jīng)請求執(zhí)行封断,請求獲取了數(shù)據(jù)后,也會做一次判斷舶担。
那么我們看下進(jìn)行請求過程中
resource = decode();坡疼,
是不是就不能中斷了呢?網(wǎng)絡(luò)請求在DecodeJob的decodeSource()方法:
private Resource<T> decodeSource() throws Exception {
Resource<T> decoded = null;
try {
long startTime = LogTime.getLogTime();
final A data = fetcher.loadData(priority);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Fetched data", startTime);
}
if (isCancelled) {
return null;
}
decoded = decodeFromSourceData(data);
} finally {
fetcher.cleanup();
}
return decoded;
}
其中isCancelled也是volatie關(guān)鍵字衣陶,在 decodeJob.cancel();中會被置為true柄瑰。前面代碼中EngineRunnable#cancle()會調(diào)用這個方法。所以在請求完網(wǎng)絡(luò)數(shù)據(jù)時剪况,還會判斷一下教沾,是不是需要中斷。如果需要译断,那么就不用解碼了(解碼是很耗時的操作)授翻。那網(wǎng)絡(luò)請求有沒有在這方面做判斷呢?真正的網(wǎng)絡(luò)請求在DataFetcher#load()中。DataFetcher類的存在能夠解耦圖片加載的具體實現(xiàn)堪唐。比如你是使用android原生的http加載的(生成httpUrlConnection...)巡语,還是使用其他的協(xié)議加載的,還是使用第三方庫加載的(比如okhttp)淮菠。這里我們以HttpUrlFetcher為例:
private volatile boolean isCancelled;
@Override
public InputStream loadData(Priority priority) throws Exception {
return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
}
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
throws IOException {
if (redirects >= MAXIMUM_REDIRECTS) {
throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
} else {
// Comparing the URLs using .equals performs additional network I/O and is generally broken.
// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
try {
if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
throw new IOException("In re-direct loop");
}
} catch (URISyntaxException e) {
// Do nothing, this is best effort.
}
}
urlConnection = connectionFactory.build(url);
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
}
urlConnection.setConnectTimeout(2500);
urlConnection.setReadTimeout(2500);
urlConnection.setUseCaches(false);
urlConnection.setDoInput(true);
// Connect explicitly to avoid errors in decoders if connection fails.
urlConnection.connect();
if (isCancelled) {
return null;
}
final int statusCode = urlConnection.getResponseCode();
if (statusCode / 100 == 2) {
return getStreamForSuccessfulRequest(urlConnection);
} else if (statusCode / 100 == 3) {
String redirectUrlString = urlConnection.getHeaderField("Location");
if (TextUtils.isEmpty(redirectUrlString)) {
throw new IOException("Received empty or null redirect url");
}
URL redirectUrl = new URL(url, redirectUrlString);
return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
} else {
if (statusCode == -1) {
throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
}
throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
}
}
這里也有一個volitile關(guān)鍵字isCancelled男公。當(dāng)http鏈接已經(jīng)建立了,這時候在進(jìn)行請求(code/data)之前判斷一下合陵,如果已經(jīng)取消了枢赔,那么就不進(jìn)行請求數(shù)據(jù)或code了。isCancelled關(guān)鍵字會在其cancel()方法會置為true拥知。而cancel()方法在DecodeJob的cancel()方法中會被調(diào)用踏拜。
所以當(dāng)取消一個任務(wù)時,網(wǎng)絡(luò)請求如果還沒加載數(shù)據(jù)举庶,會進(jìn)行相應(yīng)的中斷执隧。
通過上面的分析:當(dāng)一個圖片網(wǎng)絡(luò)任務(wù)沒有任何監(jiān)聽時,線程處于阻塞狀態(tài)下户侥、任務(wù)還沒執(zhí)行镀琉、網(wǎng)絡(luò)連接后還沒請求數(shù)據(jù)、數(shù)據(jù)請求結(jié)束還沒解碼蕊唐、還沒發(fā)送給監(jiān)聽者這些狀態(tài)時屋摔,都會進(jìn)行中斷取消判斷。所以glide在網(wǎng)絡(luò)請求的取消這塊做的真的很棒替梨。
2. 線程的使用
上面說完了線程的取消钓试。在android中使用線程,我們還需要注意是不是會導(dǎo)致內(nèi)存泄漏副瀑,串行/并發(fā)弓熏,與UI線程交互,線程的創(chuàng)建銷毀等問題糠睡,那么我這里總結(jié)一些知識點挽鞠。并沒有源碼分析。
1. 直接new 一個線程
直接
new thread(new runnable{
@override
public void run{
...
}
}).start();
缺點:
匿名內(nèi)部類持有外部類的引用狈孔,會造成內(nèi)存泄漏信认。
線程優(yōu)先級和ui線程一樣高。
需要自己使用handler處理與ui線程的通信均抽。同時由于handler寫法如果不規(guī)范嫁赏,handler也會持有外部類的引用,造成內(nèi)存泄漏油挥。但是潦蝇,如果handler寫成靜態(tài)內(nèi)部類款熬,那么如果handler的handleMessage(){//邏輯..}邏輯中使用到了activty中的某些view或成員變量,那么如果activty已經(jīng)消失了(雖然持有了activty的成員變量护蝶,但靜態(tài)handler并沒有持有activty华烟,所以activty還是可能被銷毀。導(dǎo)致里面的view為空)持灰,那么這時候再操作這些view,就會報空指針负饲。所以只能在邏輯的最前面加一些撇腳的 fragment堤魁!=null&&fragment.isAdd()的判斷。這是因為這里使用handler處理與ui線程的通信并沒有使用觀察者模式返十。所以并沒有取消訂閱這些操作妥泉。導(dǎo)致很可能crash增加。AsyncTask同理洞坑。
不好管理線程的取消盲链。
2. 使用AsyncTask
不用自己去處理與ui線程的同步。
缺點:
- 如果使用匿名內(nèi)部類迟杂,會持有外部類的引用刽沾,會造成內(nèi)存泄漏。
- 直接使用不含有參數(shù)的execute()啟動task排拷,那么task是串行執(zhí)行的侧漓。這點雖然不用考慮同步問題,但是如果task多的話监氢,會有性能影響布蔗。如果想并發(fā)執(zhí)行task,那么需要使用帶參數(shù)的execute(ExecutorService)浪腐,即指定線程池纵揍。
- AsyncTask需要使用cancel()取消訂閱(有時候可能會不起作用),不然可能造成crash。與上面同理何什。
3.使用 HandlerThread
handlerThread 是含有一套looper庇茫,handler,messageQueue的線程隔盛。如果我們有一個業(yè)務(wù)場景:需要一個持久的后臺線程,且該線程與ui線程需要相互通信拾稳。使用handlerThread會比較方便吮炕。
缺點
- 如果activty界面消失了,那么不容易找到這個handlerThread访得,并且這個thread的優(yōu)先級并沒有那么高龙亲,很可能在內(nèi)存吃緊的時候被銷毀陕凹。所以HandlerThread一般是在其他組件內(nèi)部使用,比如IntentService鳄炉、ThreadPoolExecutor的coreThread都是基于HandlerThread形成的杜耙。
- 使用HandlerThread的handler向HandlerThread拋任務(wù)時,是串行執(zhí)行的拂盯。
4. 使用IntentService
前面說了佑女,IntentService內(nèi)部的工作原理就是service+HandlerThread。我們一般使用service時谈竿,一般啟動有兩種方式:
一種:startService(Intent)通過intent來指派執(zhí)行任務(wù)(service的onStartCommand(intent)會被回調(diào))团驱。生命周期比較長,如果不手調(diào)用selfStop()/stopService空凸,那么service會一直存在(即使activty銷毀了)嚎花。
第二種:如果使用bindService(intent)(service的onBind(ServiceConnection)會被回調(diào)),如果所有頁面的地方都unBindService()呀洲,那么service就會被停止紊选。并且,不像開啟一個線程后道逗,我們基本不能再對線程做什么了兵罢。我們可以通過binder獲取到service的實例(startService()或bindService()也只能回調(diào)service的某些生命周期方法,并不能得到service實例本身)憔辫,進(jìn)而調(diào)用service實例的某些方法來調(diào)控后臺趣些。
雖然上面兩種方式我們都可以在service中創(chuàng)建新線程來執(zhí)行新的任務(wù)。如果我們的任務(wù)不緊急贰您,我們也不想操心線程創(chuàng)建的事情坏平。我們就可以直接使用IntentService來開啟一個后臺。
我們可以實現(xiàn)IntentService的handleIntent(intent)方法锦亦。handleIntent默認(rèn)是在異步線程工作的舶替。IntentService使用context.startService(intent)來啟動(類似handler的作用)。會將intent到IntentService中的一個intent隊列中杠园,等待IntentService的handleIntent()方法的回調(diào)顾瞪。intent任務(wù)串行執(zhí)行。
相比直接使用線程開啟后臺抛蚁,service的優(yōu)先級更高陈醒,更不容易被銷毀。
5. 使用線程池
線程池的worker線程(線程池中瞧甩,線程被封裝成了work類)其實和HandlerThread差不多钉跷。都是一個無線循環(huán)的線程。task執(zhí)行完了肚逸,再到線程池workQueue阻塞隊列中拿task來執(zhí)行爷辙。coreThread核心線程即使沒有任務(wù)彬坏,也不會被回收。當(dāng)task超過了核心線程的數(shù)量膝晾,那么就放到阻塞隊列中栓始。如果阻塞隊列也塞滿了任務(wù),那么就繼續(xù)開啟worker血当,直到所有worker的數(shù)目到MaxThreadNum幻赚,這時候使用某些策略來回應(yīng),比如停止接收歹颓,報錯等坯屿。