Android之遠程圖片擷取和本機快取
來源:互聯網
上載者:User
概述對於用戶端——伺服器端應 用,從遠程擷取圖片算是經常要用的一個功能,而圖片資源往往會消耗比較大的流量,對應用來說,如果處理不好這個問題,那會讓使用者很崩潰,不知不覺手機流量 就用完了,等使用者發現是你的應用消耗掉了他手機流量的話,那麼可想而知你的應用將面臨什麼樣的命運。另外一個問題就是載入速度,如果應用中圖片載入速度很慢的話,那麼使用者同樣會等到崩潰。那麼如何處理好圖片資源的擷取和管理呢?非同步下載本機快取非同步下載大家都知道,在android應用中UI線程5秒沒響應的話就會拋出無響應異常,對於遠程擷取大的資源來說,這種異常還是很容易就會拋出來的,那麼怎麼避免這種問題的產生。在android中提供兩種方法來做這件事情:啟動一個新的線程來擷取資源,完成後通過Handler機制發送訊息,並在UI線程中處理訊息,從而達到在非同步線程中擷取圖片,然後通過Handler Message來更新UI線程的過程。使用android中提供的AsyncTask來完成。具體的做法這裡就不介紹了,查下API就可以了,或者是google、baidu下。這裡主要來說本機快取。本機快取對於圖片資源來說,你不可能讓應用每次擷取的時候都重新到遠程去下載(ListView),這樣會浪費資源,但是你又不能讓所有圖片資源都放到記憶體 中去(雖然這樣載入會比較快),因為圖片資源往往會佔用很大的記憶體空間,容易導致OOM。那麼如果下載下來的圖片儲存到SDCard中,下次直接從 SDCard上去擷取呢?這也是一種做法,我看了下,還是有不少應用採用這種方式的。採用LRU等一些演算法可以保證sdcard被佔用的空間只有一小部 分,這樣既保證了圖片的載入、節省了流量、又使SDCard的空間只佔用了一小部分。另外一種做法是資源直接儲存在記憶體中,然後設定到期時間和LRU規 則。sdcard儲存:在sdcard上開闢一定的空間,需要先判斷sdcard上剩餘空間是否足夠,如果足夠的話就可以開闢一些空間,比如10M當需要擷取圖片時,就先從sdcard上的目錄中去找,如果找到的話,使用該圖片,並更新圖片最後被使用的時間。如果找不到,通過URL去download去伺服器端下載圖片,如果下載成功了,放入到sdcard上,並使用,如果失敗了,應該有重試機制。比如3次。下載成功後儲存到sdcard上,需要先判斷10M空間是否已經用完,如果沒有用完就儲存,如果空間不足就根據LRU規則刪除一些最近沒有被使用者的資源。關鍵代碼:儲存圖片到SD卡上private void saveBmpToSd(Bitmap bm, Stringurl) { if (bm == null) { Log.w(TAG, " trying to savenull bitmap"); return; } //判斷sdcard上的空間 if (FREE_SD_SPACE_NEEDED_TO_CACHE >freeSpaceOnSd()) { Log.w(TAG, "Low free space onsd, do not cache"); return; } String filename =convertUrlToFileName(url); String dir = getDirectory(filename); File file = new File(dir +"/" + filename); try { file.createNewFile(); OutputStream outStream = newFileOutputStream(file); bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream); outStream.flush(); outStream.close(); Log.i(TAG, "Image saved tosd"); } catch (FileNotFoundException e) { Log.w(TAG,"FileNotFoundException"); } catch (IOException e) { Log.w(TAG,"IOException"); } }儲存圖片到SD卡上 /** * 計算sdcard上的剩餘空間 * @return */ private int freeSpaceOnSd() { StatFs stat = newStatFs(Environment.getExternalStorageDirectory() .getPath()); double sdFreeMB = ((double)stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB; return (int) sdFreeMB; }修改檔案的最後修改時間 /** * 修改檔案的最後修改時間 * @param dir * @param fileName */ private void updateFileTime(String dir,String fileName) { File file = new File(dir,fileName); long newModifiedTime =System.currentTimeMillis(); file.setLastModified(newModifiedTime); }本機快取最佳化 /** *計算儲存目錄下的檔案大小,當檔案總大小大於規定的CACHE_SIZE或者sdcard剩餘空間小於FREE_SD_SPACE_NEEDED_TO_CACHE的規定 * 那麼刪除40%最近沒有被使用的檔案 * @param dirPath * @param filename */ private void removeCache(String dirPath) { File dir = new File(dirPath); File[] files = dir.listFiles(); if (files == null) { return; } int dirSize = 0; for (int i = 0; i < files.length;i++) { if(files[i].getName().contains(WHOLESALE_CONV)) { dirSize += files[i].length(); } } if (dirSize > CACHE_SIZE * MB ||FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { int removeFactor = (int) ((0.4 *files.length) + 1); Arrays.sort(files, newFileLastModifSort()); Log.i(TAG, "Clear some expiredcache files "); for (int i = 0; i <removeFactor; i++) { if(files[i].getName().contains(WHOLESALE_CONV)) { files[i].delete(); } } } } /** * 刪除到期檔案 * @param dirPath * @param filename */ private void removeExpiredCache(StringdirPath, String filename) { File file = new File(dirPath,filename); if (System.currentTimeMillis() -file.lastModified() > mTimeDiff) { Log.i(TAG, "Clear some expiredcache files "); file.delete(); } }檔案使用時間排序/** * TODO 根據檔案的最後修改時間進行排序 * */classFileLastModifSort implements Comparator<File>{ public int compare(File arg0, File arg1) { if (arg0.lastModified() >arg1.lastModified()) { return 1; } else if (arg0.lastModified() ==arg1.lastModified()) { return 0; } else { return -1; } }}記憶體儲存:在記憶體中儲存的話,只能儲存一定的量,而不能一直往裡面放,需要設定資料的到期時間、LRU等演算法。這裡有一個方法是把常用的資料放到一個緩衝中 (A),不常用的放到另外一個緩衝中(B)。當要擷取資料時先從A中去擷取,如果A中不存在那麼再去B中擷取。B中的資料主要是A中LRU出來的資料,這 裡的記憶體回收主要針對B記憶體,從而保持A中的資料可以有效被命中。1.先定義A緩衝:private final HashMap<String, Bitmap>mHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY/ 2, 0.75f, true) { @Override protected booleanremoveEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) { if (size() >HARD_CACHE_CAPACITY) { //當map的size大於30時,把最近不常用的key放到mSoftBitmapCache中,從而保證mHardBitmapCache的效率 mSoftBitmapCache.put(eldest.getKey(), newSoftReference<Bitmap>(eldest.getValue())); return true; } else return false; } };2.再定於B緩衝: /** *當mHardBitmapCache的key大於30的時候,會根據LRU演算法把最近沒有被使用的key放入到這個緩衝中。 *Bitmap使用了SoftReference,當記憶體空間不足時,此cache中的bitmap會被記憶體回收掉 */ private final staticConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache =new ConcurrentHashMap<String,SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);3.從緩衝中擷取資料: /** * 從緩衝中擷取圖片 */ private Bitmap getBitmapFromCache(Stringurl) { // 先從mHardBitmapCache緩衝中擷取 synchronized (mHardBitmapCache) { final Bitmap bitmap =mHardBitmapCache.get(url); if (bitmap != null) { //如果找到的話,把元素移到linkedhashmap的最前面,從而保證在LRU演算法中是最後被刪除 mHardBitmapCache.remove(url); mHardBitmapCache.put(url,bitmap); return bitmap; } } //如果mHardBitmapCache中找不到,到mSoftBitmapCache中找 SoftReference<Bitmap>bitmapReference = mSoftBitmapCache.get(url); if (bitmapReference != null) { final Bitmap bitmap =bitmapReference.get(); if (bitmap != null) { return bitmap; } else { mSoftBitmapCache.remove(url); } } return null; }4.如果緩衝中不存在,那麼就只能去伺服器端去下載: /** * 非同步下載圖片 */ class ImageDownloaderTask extendsAsyncTask<String, Void, Bitmap> { private static final int IO_BUFFER_SIZE= 4 * 1024; private String url; private finalWeakReference<ImageView> imageViewReference; public ImageDownloaderTask(ImageViewimageView) { imageViewReference = newWeakReference<ImageView>(imageView); } @Override protected BitmapdoInBackground(String... params) { final AndroidHttpClient client =AndroidHttpClient.newInstance("Android"); url = params[0]; final HttpGet getRequest = newHttpGet(url); try { HttpResponse response =client.execute(getRequest); final int statusCode =response.getStatusLine().getStatusCode(); if (statusCode !=HttpStatus.SC_OK) { Log.w(TAG, "從" +url + "中下載圖片時出錯!,錯誤碼:" + statusCode); return null; } final HttpEntity entity =response.getEntity(); if (entity != null) { InputStream inputStream =null; OutputStream outputStream =null; try { inputStream =entity.getContent(); finalByteArrayOutputStream dataStream = new ByteArrayOutputStream(); outputStream = newBufferedOutputStream(dataStream, IO_BUFFER_SIZE); copy(inputStream,outputStream); outputStream.flush(); final byte[] data =dataStream.toByteArray(); final Bitmap bitmap =BitmapFactory.decodeByteArray(data, 0, data.length); return bitmap; } finally { if (inputStream !=null) { inputStream.close(); } if (outputStream !=null) { outputStream.close(); } entity.consumeContent(); } } } catch (IOException e) { getRequest.abort(); Log.w(TAG, "I/O errorwhile retrieving bitmap from " + url, e); } catch (IllegalStateException e) { getRequest.abort(); Log.w(TAG, "Incorrect URL:" + url); } catch (Exception e) { getRequest.abort(); Log.w(TAG, "Error whileretrieving bitmap from " + url, e); } finally { if (client != null) { client.close(); } } return null; }這是兩種做法,還有一些應用在下載的時候使用了線程池和Message Queue,對於圖片下載的效率要更好一些。有興趣的同學可以看下。總結對於遠程圖片等相對比較大的資源一定要在非同步線程中去擷取本地做緩衝