1. 程式人生 > >android圖片選擇控制元件(仿微信圖片多選[附原始碼])

android圖片選擇控制元件(仿微信圖片多選[附原始碼])

一、背景:

    最近公司有一個專案需要支援手機本地圖片的多選,就像微信那樣的。
    OK,不能呼叫系統的圖片選擇控制元件,那就自己寫個吧,基本思路就是使用ContentProvider掃描手機中的圖片,然後以Gridview的方式展示圖片,同時為了保證能圖片能快速載入,需要對圖片進行快取(記憶體快取是必須的,由於本來就是本地的圖片,暫時可以不用再在SD卡中快取)。
ContentProvider掃描手機中的圖片好辦,關鍵是如何更快的載入圖片。

二、Universal Image Loader ?

    最初想到的就是使用開源圖片載入框架 UIL , 作為目前使用最廣泛的圖片載入框架,不用真是可惜了,於是優先考慮它了。

    Demo很快就寫好了,大部分手機上也測試OK,但是在一臺小米3手機上出現了記憶體溢位的問題,如下圖(OOM導致了部分圖片出現載入失敗): 
圖片

    究其原因,米三手機的解析度(1920*1080)太大,拍照拍出來的圖片也大(一般都在2M左右),當一次載入的圖片太多的時候就容易出現記憶體溢位的情況。為了不佔用太多的記憶體,我按照官方的說法(見UIL專案介紹)做了如下調整:

1. 將記憶體快取的圖片尺寸改小(之前沒有指定,UIL框架預設會按照手機的解析度來確定快取圖片的尺寸):

ImageLoaderConfiguration.Builder builder = new ImageLoaderConfiguration.Builder(context)
        ...
        .memoryCacheExtraOptions(480, 800) // default = device screen dimensions
        .discCacheExtraOptions(480, 800, CompressFormat.JPEG, 75, null)
        ...
 2.  將顯示時的圖片的質量降低、同時圖片縮放:
DisplayImageOptions options = new DisplayImageOptions.Builder()
            ...
            .bitmapConfig(Bitmap.Config.RGB_565)
            .imageScaleType(ImageScaleType.EXACTLY)
            ...
            .build();
    OK,記憶體溢位的現象是不見了,但是新的問題出現了:
    由於UIL的快取配置是全域性的,如果設定快取的圖片尺寸較小,那麼當用戶需要檢視大圖片的時候,可能加載出來的圖片是小尺寸的快取圖片。

況且像這樣指定快取圖片的大小可能影響到其他使用的UIL顯示大圖介面,所以為了不影響UIL在其他介面的使用,還是不要限制圖片在記憶體中快取的尺寸。
    另外UIL在快取圖片的時候雖然也有控制圖片縮放的引數.imageScaleType但是卻不能根據需要來縮放,所以種種原因迫使我放棄使用UIL來載入本地圖片。

三、普通多執行緒 ?

    在決定放棄使用UIL載入本地圖片之後,我開始自己寫快取圖片的方案,我的想法是隻在記憶體中快取就好,沒必要再在本地磁碟中快取一份,同時為了儲存不OOM,需要限定快取圖片的規格。
    於是我專門寫了一個載入本地圖片的類,使用android支援包中的LruCache作為記憶體快取,根據要顯示圖片的ImageView控制元件的寬和高來縮放圖片以降低記憶體使用量。使用時也比較簡單:

  1. 當要載入圖片的時候首先查詢記憶體LruCache中是否有快取,如果有直接返回該快取圖片。
  2. 如果記憶體中沒有快取,建立一個新執行緒用於載入圖片。載入圖片之前首先根據傳入的圖片的寬高參數計算目標圖片的縮放比,之後根據縮放比從磁碟中取出相應的圖片並返回,同時將圖片快取到記憶體LruCache以便下次使用。

    由於使用LruCache,出現記憶體不足的時候,系統會自動gc,所以一般情況不會出現OOM的情況。同時由於沒有快取的時候都會新建一個執行緒用於載入圖片所以載入圖片的速度也還可以接受。
    但是問題是,當圖片多的時候,頻繁新建執行緒的記憶體開銷會比較大,這樣會導致UI執行緒卡慢的情況(ListView或GridView滾動不流暢)。出現這種情況後我立馬就想可以用執行緒池來代替每次都新建執行緒的情況。

四、執行緒池 ?

    用執行緒池代替Thread比較簡單,Java中的Executors類以工廠模式的方式提供了一些快速建立常用的執行緒池的方法。例如:

  1. 建立固定大小的執行緒池: Executors.newFixedThreadPool(int nThreads);
  2. 建立大小為1的固定執行緒池: Executors.newSingleThreadExecutor();
  3. 建立執行緒keepAliveTime為1分鐘的可伸縮執行緒池: Executors.newCachedThreadPool();
  4. 建立一個支援定時及週期性的任務執行的執行緒池: Executors.newScheduledThreadPool(int nThreads);

    使用執行緒池後UI主執行緒不卡了,但又有問題,當圖片多的時候,Gridview滑動到底部,這時候圖片可能要等很久才能加載出來。這種情況很好理解,當圖片很多的時候,執行緒池中的那幾個執行緒根本不夠用,所以這時候圖片的載入還是會表現出一定的順序性。如果直接滑動到GridView或ListView的底部,圖片自然要等一段時間才能加載出來。
    於是乎我想,可以通過監聽ListView/GridView的滾動來控制圖片是否載入,當控制元件滾動的時候不載入圖片,只有當其靜止的時候才載入。

五、監聽ListView/GridView的滾動事件 ?

       最初想到這種方式後並沒有立即就去做,原因就是這種方式的可複用性比較低,要所有顯示本地圖片的介面都去監聽滾動事件有點不切實際,況且從沒見過哪個圖片載入框架要監聽控制元件的滾動,但他們都執行的好好的,所以這種方法肯定不是最好的。
    更現實的問題是當某次要載入的圖片特別多的時候(比如說超過1000張),如果使用者恰好要選擇靠後的圖片,這時候控制元件大部分時間處於滾動的狀態,如果此時不載入圖片,就會給使用者一個圖片沒有載入的訊號,使用者感官上會覺得圖片載入很慢。同時在滑動的過程中,從使用者手指離開螢幕到控制元件完全停止滾動是一個減速的過程,這個過程有一段時間,如果在這段時間就開始載入圖片而不是等到控制元件完全停止才載入,那麼等控制元件停止的時候就可以省下不少載入圖片的時間了。
    綜合考慮以上因素,我放棄了通過監聽ListView/GridView 的滾動事假來判斷是否應該載入圖片的這種想法。

六、最終解決方案:

    重新審視這個問題,我覺得至少有兩個條件必須滿足:      1.儘可能少佔用記憶體,不OOM是底線(執行緒池+記憶體LruCache基本可以達到這個要求)。      2.圖片載入的速度要快。

    第二個問題是目前最棘手的問題。如果優先載入ListView/GridView可見區域的圖片而暫時忽略不可見部分的圖片,由於可見部分的圖片數量比較少,即使單個圖片比較大也能在短時間內載入完成。這樣不管使用者想要看前面的圖片還是後面的圖片都能在短時間內在介面上顯示,這樣使用者的體驗會比較好。
    前面講到的監聽控制元件的滾動事件其實際也是優先載入可見區域的圖片。那麼可不可以用其他方法優先載入視覺化區域的圖片呢?
    經過一段時間的探索,我的最終方案確定了: 由於在ListView/GridView滾動的時候會呼叫其adapter的getView方法(該方法中傳送載入圖的請求),而且是隻有當ListView或GridView中的Item可見的時候才會呼叫getView方法, 這樣我們就可以人為定義圖片載入的優先順序了,總結起來一句話:“後來居上”。 具體實施方案如下:

  1. 自己維護一個請求列表,根據請求的不同優先順序載入圖片,具體是這樣的。
  2. 首先判斷圖片是否在記憶體快取中,如果有,直接從記憶體中取出圖片。
  3. 如果沒有,構建一個圖片請求物件,加入到請求列表(如果請求列表中原來有這個請求,刪除原來的,將新的加入到列表末尾,這樣可以保證該請求的優先順序高)
  4. 執行緒池按照如下的規則處理圖片請求列表:
    • 如果請求列表中的請求數量小於執行緒池中空閒的執行緒數,順序的將請求分配給執行緒池中空閒執行緒(這時候請求列表中的所有請求同時得到執行)
    • 如果請求列表中的請求數量大於執行緒池中空閒的執行緒數,將空閒的執行緒的分配給優先順序高的請求(優先順序根據請求的先後順序來定,後請求的優先順序高)
  5. 當某個請求被執行完後,從請求列表中刪除請求任務,同時,如果請求列表中還有未處理完的任務,繼續按照上述規則處理請求。

    這樣最終效果還不錯,也達到了我與其的效果。
圖片

PS:以上是本人在做圖片選擇控制元件時的一些經歷與體會,純粹是個人的一些想法,可能並不是最好的解決方案,如果您有更好的解決方案,希望您能分享出來,讓大家共同學習。

七、檢視原始碼