Android照片牆完整版,完美結合LruCache和DiskLruCache
在上一篇文章當中,我們學習了DiskLruCache的概念和基本用法,但僅僅是掌握理論知識顯然是不夠的,那麼本篇文章我們就來繼續進階一下,看一看在實戰當中應該怎樣合理使用DiskLruCache。還不熟悉DiskLruCache用法的朋友可以先去參考我的上一篇文章 Android DiskLruCache完全解析,硬碟快取的最佳方案 。
其實,在真正的專案實戰當中如果僅僅是使用硬碟快取的話,程式是有明顯短板的。而如果只使用記憶體快取的話,程式當然也會有很大的缺陷。因此,一個優秀的程式必然會將記憶體快取和硬碟快取結合到一起使用,那麼本篇文章我們就來看一看,如何才能將LruCache和DiskLruCache完美結合到一起。
在 Android照片牆應用實現,再多的圖片也不怕崩潰 這篇文章當中,我編寫了一個照片牆的應用程式,但當時只是單純使用到了記憶體快取而已,而今天我們就對這個例子進行擴充套件,製作一個完整版的照片牆。
那我們開始動手吧,新建一個Android專案,起名叫PhotoWallDemo,這裡我使用的是Android 4.0的API。然後新建一個libcore.io包,並將DiskLruCache.java檔案拷貝到這個包下,這樣就把準備工作完成了。
接下來首先需要考慮的仍然是圖片源的問題,簡單起見,我仍然是吧所有圖片都上傳到了我的CSDN相簿當中,然後新建一個Images類,將所有相簿中圖片的網址都配置進去,程式碼如下所示:
public class Images { public final static String[] imageThumbUrls = new String[] { "http://img.my.csdn.net/uploads/201407/26/1406383299_1976.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383291_6518.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383291_8239.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383290_9329.jpg" , "http://img.my.csdn.net/uploads/201407/26/1406383290_1042.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383275_3977.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383265_8550.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383264_3954.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383264_4787.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383264_8243.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383248_3693.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383243_5120.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383242_3127.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383242_9576.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383242_1721.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383219_5806.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383214_7794.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383213_4418.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383213_3557.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383210_8779.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383172_4577.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383166_3407.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383166_2224.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383166_7301.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383165_7197.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383150_8410.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383131_3736.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383130_5094.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383130_7393.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383129_8813.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383100_3554.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383093_7894.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383092_2432.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383092_3071.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383091_3119.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383059_6589.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383059_8814.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383059_2237.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383058_4330.jpg", "http://img.my.csdn.net/uploads/201407/26/1406383038_3602.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382942_3079.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382942_8125.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382942_4881.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382941_4559.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382941_3845.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382924_8955.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382923_2141.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382923_8437.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382922_6166.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382922_4843.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382905_5804.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382904_3362.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382904_2312.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382904_4960.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382900_2418.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382881_4490.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382881_5935.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382880_3865.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382880_4662.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382879_2553.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382862_5375.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382862_1748.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382861_7618.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382861_8606.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382861_8949.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382841_9821.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382840_6603.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382840_2405.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382840_6354.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382839_5779.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382810_7578.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382810_2436.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382809_3883.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382809_6269.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382808_4179.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382790_8326.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382789_7174.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382789_5170.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382789_4118.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382788_9532.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382767_3184.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382767_4772.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382766_4924.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382766_5762.jpg", "http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg" };}
設定好了圖片源之後,我們需要一個GridView來展示照片牆上的每一張圖片。開啟或修改activity_main.xml中的程式碼,如下所示:<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <GridView android:id="@+id/photo_wall" android:layout_width="match_parent" android:layout_height="match_parent" android:columnWidth="@dimen/image_thumbnail_size" android:gravity="center" android:horizontalSpacing="@dimen/image_thumbnail_spacing" android:numColumns="auto_fit" android:stretchMode="columnWidth" android:verticalSpacing="@dimen/image_thumbnail_spacing" > </GridView></LinearLayout>
很簡單,只是在LinearLayout中寫了一個GridView而已。接著我們要定義GridView中每一個子View的佈局,新建一個photo_layout.xml佈局,加入如下程式碼:<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" > <ImageView android:id="@+id/photo" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:scaleType="fitXY" /></RelativeLayout>
仍然很簡單,photo_layout.xml佈局中只有一個ImageView控制元件,就是用它來顯示圖片的。這樣我們就把所有的佈局檔案都寫好了。
接下來新建PhotoWallAdapter做為GridView的介面卡,程式碼如下所示:
public class PhotoWallAdapter extends ArrayAdapter<String> { /** * 記錄所有正在下載或等待下載的任務。 */ private Set<BitmapWorkerTask> taskCollection; /** * 圖片快取技術的核心類,用於快取所有下載好的圖片,在程式記憶體達到設定值時會將最少最近使用的圖片移除掉。 */ private LruCache<String, Bitmap> mMemoryCache; /** * 圖片硬碟快取核心類。 */ private DiskLruCache mDiskLruCache; /** * GridView的例項 */ private GridView mPhotoWall; /** * 記錄每個子項的高度。 */ private int mItemHeight = 0; public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects, GridView photoWall) { super(context, textViewResourceId, objects); mPhotoWall = photoWall; taskCollection = new HashSet<BitmapWorkerTask>(); // 獲取應用程式最大可用記憶體 int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxMemory / 8; // 設定圖片快取大小為程式最大可用記憶體的1/8 mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount(); } }; try { // 獲取圖片快取路徑 File cacheDir = getDiskCacheDir(context, "thumb"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } // 建立DiskLruCache例項,初始化快取資料 mDiskLruCache = DiskLruCache .open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); } } @Override public View getView(int position, View convertView, ViewGroup parent) { final String url = getItem(position); View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null); } else { view = convertView; } final ImageView imageView = (ImageView) view.findViewById(R.id.photo); if (imageView.getLayoutParams().height != mItemHeight) { imageView.getLayoutParams().height = mItemHeight; } // 給ImageView設定一個Tag,保證非同步載入圖片時不會亂序 imageView.setTag(url); imageView.setImageResource(R.drawable.empty_photo); loadBitmaps(imageView, url); return view; } /** * 將一張圖片儲存到LruCache中。 * * @param key * LruCache的鍵,這裡傳入圖片的URL地址。 * @param bitmap * LruCache的鍵,這裡傳入從網路上下載的Bitmap物件。 */ public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemoryCache(key) == null) { mMemoryCache.put(key, bitmap); } } /** * 從LruCache中獲取一張圖片,如果不存在就返回null。 * * @param key * LruCache的鍵,這裡傳入圖片的URL地址。 * @return 對應傳入鍵的Bitmap物件,或者null。 */ public Bitmap getBitmapFromMemoryCache(String key) { return mMemoryCache.get(key); } /** * 載入Bitmap物件。此方法會在LruCache中檢查所有螢幕中可見的ImageView的Bitmap物件, * 如果發現任何一個ImageView的Bitmap物件不在快取中,就會開啟非同步執行緒去下載圖片。 */ public void loadBitmaps(ImageView imageView, String imageUrl) { try { Bitmap bitmap = getBitmapFromMemoryCache(imageUrl); if (bitmap == null) { BitmapWorkerTask task = new BitmapWorkerTask(); taskCollection.add(task); task.execute(imageUrl); } else { if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } } } catch (Exception e) { e.printStackTrace(); } } /** * 取消所有正在下載或等待下載的任務。 */ public void cancelAllTasks() { if (taskCollection != null) { for (BitmapWorkerTask task : taskCollection) { task.cancel(false); } } } /** * 根據傳入的uniqueName獲取硬碟快取的路徑地址。 */ public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } /** * 獲取當前應用程式的版本號。 */ public int getAppVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 1; } /** * 設定item子項的高度。 */ public void setItemHeight(int height) { if (height == mItemHeight) { return; } mItemHeight = height; notifyDataSetChanged(); } /** * 使用MD5演算法對傳入的key進行加密並返回。 */ public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } /** * 將快取記錄同步到journal檔案中。 */ public void fluchCache() { if (mDiskLruCache != null) { try { mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } } private String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); } /** * 非同步下載圖片的任務。 * * @author guolin */ class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> { /** * 圖片的URL地址 */ private String imageUrl; @Override protected Bitmap doInBackground(String... params) { imageUrl = params[0]; FileDescriptor fileDescriptor = null; FileInputStream fileInputStream = null; Snapshot snapShot = null; try { // 生成圖片URL對應的key final String key = hashKeyForDisk(imageUrl); // 查詢key對應的快取 snapShot = mDiskLruCache.get(key); if (snapShot == null) { // 如果沒有找到對應的快取,則準備從網路上請求資料,並寫入快取 DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } // 快取被寫入後,再次查詢key對應的快取 snapShot = mDiskLruCache.get(key); } if (snapShot != null) { fileInputStream = (FileInputStream) snapShot.getInputStream(0); fileDescriptor = fileInputStream.getFD(); } // 將快取資料解析成Bitmap物件 Bitmap bitmap = null; if (fileDescriptor != null) { bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor); } if (bitmap != null) { // 將Bitmap物件新增到記憶體快取當中 addBitmapToMemoryCache(params[0], bitmap); } return bitmap; } catch (IOException e) { e.printStackTrace(); } finally { if (fileDescriptor == null && fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { } } } return null; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); // 根據Tag找到相應的ImageView控制元件,將下載好的圖片顯示出來。 ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl); if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } taskCollection.remove(this); } /** * 建立HTTP請求,並獲取Bitmap物件。 * * @param imageUrl * 圖片的URL地址 * @return 解析後的Bitmap物件 */ private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; } }}
程式碼有點長,我們一點點進行分析。首先在PhotoWallAdapter的建構函式中,我們初始化了LruCache類,並設定了記憶體快取容量為程式最大可用記憶體的1/8,緊接著呼叫了DiskLruCache的open()方法來建立例項,並設定了硬碟快取容量為10M,這樣我們就把LruCache和DiskLruCache的初始化工作完成了。
接著在getView()方法中,我們為每個ImageView設定了一個唯一的Tag,這個Tag的作用是為了後面能夠準確地找回這個ImageView,不然非同步載入圖片會出現亂序的情況。然後在getView()方法的最後呼叫了loadBitmaps()方法,載入圖片的具體邏輯也就是在這裡執行的了。
進入到loadBitmaps()方法中可以看到,實現是呼叫了getBitmapFromMemoryCache()方法來從記憶體中獲取快取,如果獲取到了則直接呼叫ImageView的setImageBitmap()方法將圖片顯示到介面上。如果記憶體中沒有獲取到,則開啟一個BitmapWorkerTask任務來去非同步載入圖片。
那麼在BitmapWorkerTask的doInBackground()方法中,我們就靈活運用了上篇文章中學習的DiskLruCache的各種用法。首先根據圖片的URL生成對應的MD5 key,然後呼叫DiskLruCache的get()方法來獲取硬碟快取,如果沒有獲取到的話則從網路上請求圖片並寫入硬碟快取,接著將Bitmap物件解析出來並新增到記憶體快取當中,最後將這個Bitmap物件顯示到介面上,這樣一個完整的流程就執行完了。
那麼我們再來分析一下上述流程,每次載入圖片的時候都優先去記憶體快取當中讀取,當讀取不到的時候則回去硬碟快取中讀取,而如果硬碟快取仍然讀取不到的話,就從網路上請求原始資料。不管是從硬碟快取還是從網路獲取,讀取到了資料之後都應該新增到記憶體快取當中,這樣的話我們下次再去讀取圖片的時候就能迅速從記憶體當中讀取到,而如果該圖片從記憶體中被移除了的話,那就重複再執行一遍上述流程就可以了。
這樣我們就把LruCache和DiskLruCache完美結合到一起了。接下來還需要編寫MainActivity的程式碼,非常簡單,如下所示:
public class MainActivity extends Activity { /** * 用於展示照片牆的GridView */ private GridView mPhotoWall; /** * GridView的介面卡 */ private PhotoWallAdapter mAdapter; private int mImageThumbSize; private int mImageThumbSpacing; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mImageThumbSize = getResources().getDimensionPixelSize( R.dimen.image_thumbnail_size); mImageThumbSpacing = getResources().getDimensionPixelSize( R.dimen.image_thumbnail_spacing); mPhotoWall = (GridView) findViewById(R.id.photo_wall); mAdapter = new PhotoWallAdapter(this, 0, Images.imageThumbUrls, mPhotoWall); mPhotoWall.setAdapter(mAdapter); mPhotoWall.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { final int numColumns = (int) Math.floor(mPhotoWall .getWidth() / (mImageThumbSize + mImageThumbSpacing)); if (numColumns > 0) { int columnWidth = (mPhotoWall.getWidth() / numColumns) - mImageThumbSpacing; mAdapter.setItemHeight(columnWidth); mPhotoWall.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } } }); } @Override protected void onPause() { super.onPause(); mAdapter.fluchCache(); } @Override protected void onDestroy() { super.onDestroy(); // 退出程式時結束所有的下載任務 mAdapter.cancelAllTasks(); }}
上述程式碼中,我們通過getViewTreeObserver()的方式監聽View的佈局事件,當佈局完成以後,我們重新修改一下GridView中子View的高度,以保證子View的寬度和高度可以保持一致。
到這裡還沒有結束,最後還需要配置一下AndroidManifest.xml檔案,並加入相應的許可權,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.photoswalldemo" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.example.photoswalldemo.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
好了,全部程式碼都在這兒了,讓我們來執行一下吧,效果如下圖所示:
第一次從網路上請求圖片的時候有點慢,但之後載入圖片就會非常快了,滑動起來也很流暢。
那麼我們最後再檢查一下這些圖片是不是已經正確快取在指定地址了,進入 /sdcard/Android/data/<application package>/cache/thumb 這個路徑,如下圖所示:
可以看到,每張圖片的快取以及journal檔案都在這裡了,說明我們的硬碟快取已經成功了。
好了,今天的講解就到這裡,有疑問的朋友可以在下面留言。
關注我的技術公眾號,每天都有優質技術文章推送。關注我的娛樂公眾號,工作、學習累了的時候放鬆一下自己。
微信掃一掃下方二維碼即可關注: