【Java&Android開源庫程式碼剖析】のandroid-smart-image-view

yangxi_001發表於2013-11-27

Android應用開發已經進入到相對成熟的階段,特別在國外,湧現出了各式各樣的成熟穩定的開源庫,供普通開發者使用。這種情況雖然極大加速了app開發的程式,但同時帶來的問題是大多數普通開發者在使用這些開源庫的時候只是止步於知道怎麼使用它,但對開源庫的底層實現原理並不清楚,或者不怎麼深究,導致的問題很多:1)當開源庫出現bug時,不能夠很好很快的定位出問題;2)自己日常的程式碼編寫只侷限於實現app的業務邏輯,太上層,對技術水平的提升沒有多大的好處;3)對追求完美的人來說,只有對自己專案中所有程式碼實現的原理都清楚的時候,才會安心,才會有成就感;4)當自己專案需要寫基礎庫程式碼時,如果已經熟知各種開源庫的實現,那麼更能設計出好的架構,寫出好的程式碼。

    以上種種的解決方案就是多學習多研究開源庫的原始碼,瞭解其執行機理,從而提升自身的技術積累,這就是本系列的初衷。本系列將選取各種常見或者不常見的開源庫,只要它有剖析的價值,剛開始大部分將是基於Java語言的,後續會逐漸覆蓋Objective C以及C、C++、PHP等語言。同時歡迎同學們推薦自己想了解的開源庫,我會在甄別後排進本系列日程安排中。

    James Smith,網名loopj,在Android平臺上,因為android-async-http(https://github.com/loopj/android-async-http)這個開源庫而知名的,本系列我們會仔細剖析這個庫,但不是現在,剛開始我們稍微來個簡單一點的,同樣出自於loopj之手,名為android-smart-image-view(https://github.com/loopj/android-smart-image-view

從github上將程式碼檢出,我們可以看到整個專案的程式碼只包含7個Java原始檔,這個庫是對Android SDK中的ImageView控制元件的擴充套件,方便非同步載入網路上指定URL的圖片,以及系統聯絡人的頭像等,同時,提供了簡單可擴充套件的框架,方便使用者根據實際圖片的來源進行擴充套件。SmartImageView的使用方法和ImageView類似,具體可參見http://loopj.com/android-smart-image-view/上面的說明。

    android-smart-image-view擴充套件自ImageView,使其方便地顯示不同來源的圖片資源,因此,首先需要定義一個介面,來表示圖片獲取這樣一個公共的行為。而在Android中,圖片最終在繪製到畫布canvas上的時候,都是以點陣圖bitmap表示的,因此,介面定義如下:

  1. public interface SmartImage {  
  2.     public Bitmap getBitmap(Context context);  
  3. }  

根據圖片來源的不同,分別實現SmartImage介面,並在getBitmap函式中處理圖片獲取的邏輯,類圖結構如下:

我們先看上半部分的實現結構,發現3個類實現了SmartImage介面,分別是BitmapImage、ContactImage和WebImage,下面分別介紹。

1)BitmapImage類,最簡單的實現(可認為是dummy類),因為它僅僅是在建構函式中傳入Bitmap例項,然後在呼叫getBitmap時返回它。

2)ContactImage類,實現系統聯絡人頭像的獲取,在建構函式中傳入指定的聯絡人id,之後在getBitmap函式中查詢指定id的聯絡人對應的頭像,當然沒有設定頭像時返回null。

3)WebImage類,實現從指定URL獲取圖片資源,當然不是每次都從網路上載入,而是實現了一個簡單的二級快取,即記憶體快取和磁碟快取,每次載入時,都會先判斷該圖片是否存在於記憶體或者磁碟快取中,快取沒有命中時,才到指定URL上下載。

 

【獲取系統聯絡人頭像】

    獲取聯絡人頭像,也就是要訪問系統通訊錄這個app的資料,因此需要在AndroidManifest.xml檔案中加入許可權宣告:

  1. <uses-permission android:name="android.permission.READ_CONTACTS"/>   

    在Android系統中訪問其他app的資料時,一般都是通過ContentProvider實現的,一個ContentProvider類實現了一組標準的方法介面,從而能夠讓其他app儲存或者讀取它提供的各種資料型別。其他app通過ContentResolver介面就可以訪問ContentProvider提供的資料。在ContactImage類的getBitmap函式實現中,就是首先獲取ContentResolver的例項,並根據聯絡人id生成查詢的Uri,然後呼叫系統Contact類的openContactPhotoInputStream函式得到頭像的資料流,最後使用BitmapFactory.decodeStream函式將資料流生成Bitmap例項。(需要說明的一點是,這裡獲取的是手機的聯絡人頭像,而不是Sim卡中的聯絡人頭像的,因為Sim卡由於容量限制等原因,是沒有聯絡人頭像資料的)。

  1. public class ContactImage implements SmartImage {  
  2.       
  3.     private long contactId;  
  4.     public ContactImage(long contactId) {  
  5.         this.contactId = contactId;  
  6.     }  
  7.   
  8.     public Bitmap getBitmap(Context context) {  
  9.         Bitmap bitmap = null;  
  10.         ContentResolver contentResolver = context.getContentResolver();  
  11.   
  12.         try {  
  13.             Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);  
  14.             InputStream input = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri);  
  15.             if(input != null) {  
  16.                 bitmap = BitmapFactory.decodeStream(input);  
  17.             }  
  18.         } catch(Exception e) {  
  19.             e.printStackTrace();  
  20.         }  
  21.   
  22.         return bitmap;  
  23.     }  
  24. }  

    講到這裡,有人會有疑問,ContactImage中的聯絡人id 變數contactId是怎麼來的呢?contactId同樣是通過ContentResolver查詢得到的,示例程式碼如下所示:

  1. private static final int DISPLAY_NAME_INDEX = 0;  
  2. private static final int PHONE_NUMBER_INDEX = 1;  
  3. private static final int PHOTO_ID_INDEX = 2;  
  4. private static final int CONTACT_ID_INDEX = 3;  
  5. private static final String[] PHONES_PROJECTION = new String[] {    
  6.     Phone.DISPLAY_NAME, Phone.NUMBER, Phone.PHOTO_ID,Phone.CONTACT_ID };   
  7.   
  8. private void getPhoneContact(Context context) {  
  9.     ContentResolver contentResolver = context.getContentResolver();  
  10.     Cursor cursor = contentResolver.query(Phone.CONTENT_URI, PHONES_PROJECTION, nullnullnull);  
  11.     if (cursor != null) {  
  12.         while(cursor.moveToNext()) {  
  13.             String displayName = cursor.getString(DISPLAY_NAME_INDEX); // 聯絡人名字  
  14.             String phoneNum = cursor.getString(PHONE_NUMBER_INDEX);    // 聯絡人號碼  
  15.             Long contactId = cursor.getLong(CONTACT_ID_INDEX);         // 聯絡人id  
  16.             Long photoId = cursor.getLong(PHOTO_ID_INDEX);             // 聯絡人頭像id(photoId大於0時表示聯絡人有頭像)  
  17.         }  
  18.           
  19.         cursor.close();  
  20.     }  
  21. }  


【從指定URL載入圖片】

這裡的指定URL通常指的是圖片的外鏈,格式類似

http://farm6.staticflickr.com/5489/9272288811_286d003d9e_o.png

    因此,簡單的使用URLConnection的getContent方法就可以獲取圖片的資料,之後利用BitmapFactory將其轉換為Bitmap就可以了。程式碼實現如下:

  1. private Bitmap getBitmapFromUrl(String url) {  
  2.     Bitmap bitmap = null;  
  3.   
  4.     try {  
  5.         URLConnection conn = new URL(url).openConnection();  
  6.         conn.setConnectTimeout(CONNECT_TIMEOUT);  
  7.         conn.setReadTimeout(READ_TIMEOUT);  
  8.         bitmap = BitmapFactory.decodeStream((InputStream) conn.getContent());  
  9.     } catch(Exception e) {  
  10.         e.printStackTrace();  
  11.     }  
  12.   
  13.     return bitmap;  
  14. }  


【二級快取實現】

    為了加快圖片的載入速度,smart-image庫引入了簡單的二級快取,我們知道,資料獲取速度取決於物理介質,一般是記憶體>磁碟>網路,因此,在載入某個URL的圖片時,會優先判斷是否命中記憶體快取,沒有則查詢磁碟快取,最終才會考慮從網路上載入,同時更新記憶體快取和磁碟快取記錄。

    考慮到快取查詢的速度問題,在實現記憶體快取時一般都會使用類似雜湊表這樣查詢時間複雜度低的資料結構。由於存在多個執行緒同時在雜湊表中查詢的情況,因此需要考慮多執行緒併發訪問的問題,記憶體快取的實現使用ConcurrentHashMap也就在情理之中了。Android平臺上app的記憶體是有限制的,當記憶體超過這個限制時,會出現OOM(OutOfMemory),為了避免這個問題,記憶體快取中我們不會直接持有Bitmap例項的引用,而是通過SoftReference來持有Bitmap物件的軟引用,如果一個物件具有軟引用,記憶體空間足夠時,垃圾回收器不會回收它,只有在記憶體空間不足時,才會回收這些物件佔用的記憶體。因此,軟引用通常用來實現記憶體敏感的快取記憶體。

    Android系統上磁碟快取可以放在內部儲存空間,也可以放在外部儲存空間(即SD卡)。對於小圖片的快取可以放在內部儲存空間中,但當圖片比較大,數量比較多時,那麼就應該將圖片快取放到SD卡上,因為畢竟內部儲存空間一般比SD卡空間要小很多。smart-image庫的磁碟快取是放在內部儲存空間中的,也就是放在app的快取目錄中,該目錄使用Context.getCacheDir()函式來獲取,格式類似於:/data/data/app的包名/cache。cache目錄主要用於存放快取檔案,當系統的內部儲存空間不足時,該目錄下面的檔案會被刪除;當然,我們不能依賴系統來清理這些快取檔案,而是應該對這些快取檔案設定最大儲存空間,當實際佔用空間超過這個最大值時,就需要對使用一定的演算法對快取檔案進行清理。這一點在smart-image庫的實現中並沒有做考慮。

    兩級快取空間的建立在WebImageCache類的建構函式中進行,程式碼如下:

  1. public WebImageCache(Context context) {  
  2.     // Set up in-memory cache store  
  3.     memoryCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>();  
  4.   
  5.     // Set up disk cache store  
  6.     Context appContext = context.getApplicationContext();  
  7.     diskCachePath = appContext.getCacheDir().getAbsolutePath() + DISK_CACHE_PATH;  
  8.   
  9.     File outFile = new File(diskCachePath);  
  10.     outFile.mkdirs();  
  11.   
  12.     diskCacheEnabled = outFile.exists();  
  13.   
  14.     // Set up threadpool for image fetching tasks  
  15.     writeThread = Executors.newSingleThreadExecutor();  
  16. }  

    判斷Bitmap是否命中記憶體快取的程式碼如下所示,就是先取出Bitmap的軟引用,並判斷是否已經被系統回收,如果沒有就從軟引用中取出Bitmap例項:

  1. private Bitmap getBitmapFromMemory(String url) {  
  2.     Bitmap bitmap = null;  
  3.     SoftReference<Bitmap> softRef = memoryCache.get(getCacheKey(url));  
  4.     if(softRef != null){  
  5.         bitmap = softRef.get();  
  6.     }  
  7.   
  8.     return bitmap;  
  9. }  

    判斷Bitmap是否命中磁碟快取的程式碼如下所示,基本原理就是根據URL在磁碟上查詢對應的檔案,如果存在,就將其轉換成Bitmap例項返回。由於URL中可能包含一些不能出現在檔名中的特殊字元,因此,在講URL轉換成檔名時需要做預處理,過濾掉這些字元。

  1. private Bitmap getBitmapFromDisk(String url) {  
  2.     Bitmap bitmap = null;  
  3.     if(diskCacheEnabled){  
  4.         String filePath = getFilePath(url);  
  5.         File file = new File(filePath);  
  6.         if(file.exists()) {  
  7.             bitmap = BitmapFactory.decodeFile(filePath);  
  8.         }  
  9.     }  
  10.     return bitmap;  
  11. }  
  12.   
  13. private String getFilePath(String url) {  
  14.     return diskCachePath + getCacheKey(url);  
  15. }  
  16.   
  17. private String getCacheKey(String url) {  
  18.     if(url == null){  
  19.         throw new RuntimeException("Null url passed in");  
  20.     } else {  
  21.         return url.replaceAll("[.:/,%?&=]""+").replaceAll("[+]+""+");  
  22.     }  
  23. }  

    將Bitmap存到記憶體快取的步驟很簡單,就是往HashMap中新增一個資料而已,不過要注意存的是Bitmap的軟引用。程式碼如下所示。

  1. private void cacheBitmapToMemory(final String url, final Bitmap bitmap) {  
  2.     memoryCache.put(getCacheKey(url), new SoftReference<Bitmap>(bitmap));  
  3. }  

    往磁碟快取中新增Bitmap是通過執行緒池ExecutorService實現的,一方面是限制同時存在的執行緒個數,另一方面是解決同步問題。smart-image庫使用的是隻有一個執行緒的執行緒池,在WebImageCache的建構函式中可以看到,因此,磁碟快取的新增是順序進行的。生成快取的過程是先根據URL在cache目錄中生成對應的檔案,然後呼叫Bitmap.compress函式按指定壓縮格式和壓縮質量將Bitmap寫到磁碟檔案輸出流中。

  1. private void cacheBitmapToDisk(final String url, final Bitmap bitmap) {  
  2.     writeThread.execute(new Runnable() {  
  3.         @Override  
  4.         public void run() {  
  5.             if(diskCacheEnabled) {  
  6.                 BufferedOutputStream ostream = null;  
  7.                 try {  
  8.                     ostream = new BufferedOutputStream(new FileOutputStream(  
  9.                             new File(diskCachePath, getCacheKey(url))), 2*1024);  
  10.                     bitmap.compress(CompressFormat.PNG, 100, ostream);  
  11.                 } catch (FileNotFoundException e) {  
  12.                     e.printStackTrace();  
  13.                 } finally {  
  14.                     try {  
  15.                         if(ostream != null) {  
  16.                             ostream.flush();  
  17.                             ostream.close();  
  18.                         }  
  19.                     } catch (IOException e) {}  
  20.                 }  
  21.             }  
  22.         }  
  23.     });  
  24. }  


    至此,總算將上面類圖中相關類介紹完畢。接著就來看另外一個類圖:

    這個類圖中有SmartImageTask和SmartImageView兩個類以及onCompleteListener和onCompleteHandler兩個介面,而SmartImage類在上文中已經介紹過了。可以很容易的看出SmartImageTask和SmartImageView是聚合的關係,task為view提供處理後臺圖片載入等操作,view則專注於ui的呈現。

    一般這種後臺task類會實現Runnable介面,特別是在和執行緒池配合使用的時候,SmartImageTask也不例外,因為在SmartImageView中就有一個執行緒池。

    SmartImageTask既然實現了Runnable介面,那麼它的主要邏輯實現就是在run方法中的。從類圖結構中可以看到SmartImageTask聚合了SmartImage,使用SmartImage的getBitmap函式來獲取指定URL的Bitmap例項。程式碼如下:

  1. @Override  
  2. public void run() {  
  3.     if(image != null) {  
  4.         complete(image.getBitmap(context));  
  5.         context = null;  
  6.     }  
  7. }  

    除此之外,task類中還實現了回撥機制,供view類使用。包括一個靜態型別的handler類(將handler定義成static,是為了避免記憶體洩露),一個圖片載入完成的回撥介面OnCompleteListener,定義分別如下:

  1. public static class OnCompleteHandler extends Handler {  
  2.     @Override  
  3.     public void handleMessage(Message msg) {  
  4.         Bitmap bitmap = (Bitmap)msg.obj;  
  5.         onComplete(bitmap);  
  6.     }  
  7.   
  8.     public void onComplete(Bitmap bitmap){};  
  9. }  
  10.   
  11. public abstract static class OnCompleteListener {  
  12.     public abstract void onComplete();  
  13. }  

    當圖片載入還未完成時,如果需要取消載入,那麼可以設定標誌位cancelled為false即可,這時就算圖片載入成功了,也不會傳送Message告知上層view類。

    SmartImageView是ImageView的子類,定義了包含4個執行緒的執行緒池,用來執行SmartImageTask任務。在給ImageView設定圖片資源時,可以選擇是否設定預設圖片,是否設定載入失敗的圖片,以及是否設定載入完成後的回撥介面。在啟用新的task任務前,得先判斷是否已經存在給當前ImageView設定圖片的task在執行中,如果是,就取消它,然後新建task任務並加入執行緒池中,永遠保證一個ImageView有且只有一個最新的task在執行。

  1. public void setImage(final SmartImage image, final Integer fallbackResource, final Integer loadingResource, final SmartImageTask.OnCompleteListener completeListener) {  
  2.     // Set a loading resource  
  3.     if(loadingResource != null){  
  4.         setImageResource(loadingResource);  
  5.     }  
  6.   
  7.     // Cancel any existing tasks for this image view  
  8.     if(currentTask != null) {  
  9.         currentTask.cancel();  
  10.         currentTask = null;  
  11.     }  
  12.   
  13.     // Set up the new task  
  14.     currentTask = new SmartImageTask(getContext(), image);  
  15.     currentTask.setOnCompleteHandler(new SmartImageTask.OnCompleteHandler() {  
  16.         @Override  
  17.         public void onComplete(Bitmap bitmap) {  
  18.             if(bitmap != null) {  
  19.                 setImageBitmap(bitmap);  
  20.             } else {  
  21.                 // Set fallback resource  
  22.                 if(fallbackResource != null) {  
  23.                     setImageResource(fallbackResource);  
  24.                 }  
  25.             }  
  26.   
  27.             if(completeListener != null){  
  28.                 completeListener.onComplete();  
  29.             }  
  30.         }  
  31.     });  
  32.   
  33.     // Run the task in a threadpool  
  34.     threadPool.execute(currentTask);  
  35. }  


    最後,當要取消執行緒池中所有在等待和執行的task時,可呼叫ExecutorService的shutdownNow函式,執行緒池的建立和銷燬如下程式碼所示:

  1. private static final int LOADING_THREADS = 4;  
  2. private static ExecutorService threadPool = Executors.newFixedThreadPool(LOADING_THREADS);  
  3.   
  4. public static void cancelAllTasks() {  
  5.     threadPool.shutdownNow();  
  6.     threadPool = Executors.newFixedThreadPool(LOADING_THREADS);  
  7. }  


【擴充套件和優化】

    前面說到如果圖片有除了URL和聯絡人頭像之外的其他來源的話,那麼需要開發者實現SmartImage介面來進行擴充套件。國外另一位開發者commonsguy(以後會介紹他的開源專案)

Post了一個SmartImage的實現類VideoImage ,用於獲取系統中視訊的縮圖。

  1. class VideoImage implements SmartImage {  
  2.       
  3.     private int videoId; // 視訊id   
  4.       
  5.     private int thumbnailKind; // MICRO_KIND-微型縮略模式;MINI_KIND-迷你縮略模式,前者解析度更低  
  6.       
  7.     public VideoImage(int videoId, int thumbnailKind) {  
  8.         this.videoId = videoId;  
  9.         this.thumbnailKind = thumbnailKind;  
  10.     }  
  11.   
  12.     @Override  
  13.     public Bitmap getBitmap(Context context) {  
  14.         return (MediaStore.Video.Thumbnails.getThumbnail(  
  15.                 context.getContentResolver(), videoId, thumbnailKind, null));  
  16.     }  
  17.   
  18. }  

    在Android開發中,如果系統記憶體不足的情況下,繼續建立Bitmap例項的話,會導致OutOfMemoryError,從而導致app crash。因此,是否需要在建立Bitmap之前判斷系統可用的記憶體大小呢?是否應該捕獲OOME呢,這一點在smart-image庫中目前沒有考慮,因為畢竟這個庫只適用於小圖片的載入。如果非要優化的話,那麼可以在WebImage類的建立Bitmap物件的地方加入低記憶體的判斷,如果記憶體過低,那麼可以將圖片的取樣值inSample降低,從而降低圖片質量,降低其佔用的記憶體空間,改進後的getBitmapFromUrl函式如下所示:

  1. private Bitmap getBitmapFromUrl(String url) {  
  2.     Bitmap bitmap = null;  
  3.       
  4.     try {  
  5.         URLConnection conn = new URL(url).openConnection();  
  6.         conn.setConnectTimeout(CONNECT_TIMEOUT);  
  7.         conn.setReadTimeout(READ_TIMEOUT);  
  8.         ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();  
  9.         int inSample = 1;  
  10.         if (memInfo.lowMemory) {  
  11.             inSample = 12;  
  12.         }  
  13.         BitmapFactory.Options options = new BitmapFactory.Options();  
  14.         options.inSampleSize = inSample;  
  15.         bitmap = BitmapFactory.decodeStream((InputStream) conn.getContent(), null, options);  
  16.     } catch(Exception e) {  
  17.         e.printStackTrace();  
  18.     }  
  19.   
  20.     return bitmap;  
  21. }  

    當然,降低質量後的圖片還是超過可分配的記憶體大小時,還是會出現OutOfMemoryError,那麼我們是否可以捕獲這個異常呢?答案是可以,但不推薦。Java文件中明確說明的一點是java.lang.Error類是java.lang.Throwable的子類,java.lang.Exception也是Throwable的子類,Exception表示的是可以而且應該被捕獲的異常,而Error表示的是會導致程式crash的致命錯誤,這個一般是不應該進行捕獲的。但是,某些情況下,我們的程式在發生OutOfMemoryError異常後,可能需要做一些日誌操作,或者能夠做一些補救措施,例如釋放記憶體或者降低申請的記憶體空間等等,那麼還是可以catch住OutOfMemoryError異常的。

 

——歡迎轉載,請註明出處 http://blog.csdn.net/asce1885 ,未經本人同意請勿用於商業用途,謝謝——


相關文章