源碼分析glide對線程中斷的優(yōu)化

轉(zhuǎn)載請注明出處:

源碼分析glide對線程中斷的優(yōu)化

地址: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),比如停止接收歹颓,報錯等坯屿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市巍扛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乏德,老刑警劉巖撤奸,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喊括,居然都是意外死亡胧瓜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門郑什,熙熙樓的掌柜王于貴愁眉苦臉地迎上來府喳,“玉大人,你說我怎么就攤上這事蘑拯《勐” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵申窘,是天一觀的道長弯蚜。 經(jīng)常有香客問我,道長剃法,這世上最難降的妖魔是什么碎捺? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮贷洲,結(jié)果婚禮上收厨,老公的妹妹穿的比我還像新娘。我一直安慰自己优构,他們只是感情好诵叁,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俩块,像睡著了一般黎休。 火紅的嫁衣襯著肌膚如雪浓领。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天势腮,我揣著相機(jī)與錄音联贩,去河邊找鬼。 笑死捎拯,一個胖子當(dāng)著我的面吹牛泪幌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播署照,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼祸泪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了建芙?” 一聲冷哼從身側(cè)響起没隘,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎禁荸,沒想到半個月后右蒲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡赶熟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年瑰妄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片映砖。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡间坐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邑退,到底是詐尸還是另有隱情竹宋,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布瓜饥,位于F島的核電站逝撬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乓土。R本人自食惡果不足惜宪潮,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趣苏。 院中可真熱鬧狡相,春花似錦、人聲如沸食磕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彬伦。三九已至滔悉,卻和暖如春伊诵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背回官。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工曹宴, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人歉提。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓笛坦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親苔巨。 傳聞我的和親對象是個殘疾皇子版扩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容