概述
詳細
一.概述
目前為止,第三方的圖片加載框架挺多的,比如UIL , Volley Imageloader等等。但是最好能知道實現原理,所以下面就來看看設計並開發一個加載網絡、本地的圖片框架。
總所周知,圖片框架中肯定需要用到緩存,這里我們和其他框架一樣,采用LruCache來管理圖片的緩存,當然圖片的加載測量使用LIFO比較好點,因為要加載最新的給用戶。
我們采用異步消息處理機制來實現圖片異步加載任務:用於UI線程當Bitmap加載完成后更新ImageView。
加載網絡圖片的原理,就是如果啟用了硬盤緩存,加載時,先從內存中加載,然后從硬盤加載,最后再從網絡下載。下載完成后,寫入硬盤和內存緩存。
如果沒有啟用硬盤緩存,就直接從網絡壓縮下載獲取,最后加入內存緩存即可。
二.演示效果圖
三.圖片加載框架實現解析
1、圖片壓縮
很多情況下,網絡或者本地的圖片都比較大,而我們的ImageView顯示大小比較小,這時候就需要我們進行圖片的壓縮,以顯示到ImageView上面去。
1.1、本地圖片壓縮
(1)獲取ImageView所顯示的大小
/** * 獲取ImageView所要顯示的寬和高 */ public static ImageSize getImageViewSize(ImageView imageView) { ImageSize imageSize = new ImageSize(); DisplayMetrics displayMetrics = imageView.getContext().getResources() .getDisplayMetrics(); ViewGroup.LayoutParams lp = imageView.getLayoutParams(); // 獲取imageview的實際寬度 int width = imageView.getWidth(); if (width <= 0) {// 獲取imageview在layout中聲明的寬度 width = lp.width; } if (width <= 0) {// 檢查最大值 width = getImageViewFieldValue(imageView, "mMaxWidth"); } if (width <= 0) { width = displayMetrics.widthPixels; } // 獲取imageview的實際高度 int height = imageView.getHeight(); if (height <= 0) {// 獲取imageview在layout中聲明的寬度 height = lp.height; } if (height <= 0) {// 檢查最大值 height = getImageViewFieldValue(imageView, "mMaxHeight"); } if (height <= 0) { height = displayMetrics.heightPixels; } imageSize.width = width; imageSize.height = height; return imageSize; }
上面代碼中最大寬度,沒有用getMaxWidth();用的是反射獲取的,這是因為getMaxWidth竟然要API 16,沒辦法,為了兼容問題,只能采用反射機制,所以不太贊同反射。
(2)設置圖片的inSampleSize
根據ImageView所要顯示的大小和圖片的實際大小來計算inSampleSize,實現如下:
/** * 根據ImageView的寬高和圖片實際的寬高計算SampleSize */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,int reqHeight) { int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; if (width > reqWidth || height > reqHeight) { int widthRadio = Math.round(width * 1.0f / reqWidth); int heightRadio = Math.round(height * 1.0f / reqHeight); inSampleSize = Math.max(widthRadio, heightRadio); } return inSampleSize; }
1.2、網絡壓縮
上面是本地的圖片的壓縮,如果是網絡圖片的話, 分兩種情況,如果硬盤緩存開啟的話, 就把圖片下載到本地,然后在采用上面本地壓縮方法;
如果硬盤緩存沒有開啟的話,才用BitmapFactory.decodeStream()來獲取bitmap,然后和本地壓縮一樣的方法來計算采樣率壓縮。如下:
/** * 根據url下載圖片並壓縮 */ public static Bitmap downloadImageByUrl(String urlStr, ImageView imageview) { InputStream is = null; try { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); is = new BufferedInputStream(conn.getInputStream()); is.mark(is.available()); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts); //獲取imageview想要顯示的寬和高 ImageSize imageViewSize = ImageUtils.getImageViewSize(imageview); opts.inSampleSize = ImageUtils.calculateInSampleSize(opts, imageViewSize.width, imageViewSize.height); opts.inJustDecodeBounds = false; is.reset(); bitmap = BitmapFactory.decodeStream(is, null, opts); conn.disconnect(); return bitmap; } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (IOException e) { } } return null; }
圖片壓縮差不多就這樣了,下面來看看圖片加載框架的設計與實現
2、圖片加載框架的設計架構
圖片壓縮完了,就放入我們的LruCache,然后通過setImageBitmap方法設置到我們的ImageView上。
圖片加載框架的整體架構如下:
(1)、單例實現,單例默認不傳參數,當然也支持傳參單例調用框架。
(2)、圖片緩存管理:包含一個LruCache用於管理我們的圖片。
(3)、任務隊列:每來一次新的加載圖片的請求,封裝成Task添加到的任務隊列TaskQueue中去;
(4)、后台輪詢線程:該線程在第一次初始化實例的時候啟動,然后會一直在后台運行;當每來一次加載圖片請求的時候,
除了會創建一個新的任務到任務隊列中去,同時發一個消息到后台線程,后台線程去使用線程池去TaskQueue去取一個任務執行;
基本的框架設計架構就是上面這些,下面來看看具體的實現:
3、圖片加載框架的具體實現
3.1、單例實現以及構造方法:
public static XCImageLoader getInstance() { if (mInstance == null) { synchronized (XCImageLoader.class) { if (mInstance == null) { mInstance = new XCImageLoader(DEAFULT_THREAD_COUNT,Type.LIFO); } } } return mInstance; } public static XCImageLoader getInstance(int threadCount,Type type) { if (mInstance == null) { synchronized (XCImageLoader.class) { if (mInstance == null) { mInstance = new XCImageLoader(threadCount,type); } } } return mInstance; } private XCImageLoader(int threadCount,Type type){ init(threadCount, type); } /** * 初始化信息 * @param threadCount * @param type */ private void init(int threadCount,Type type){ initBackThread(); //獲取當前應用的最大可用內存 int maxMemory = (int) Runtime.getRuntime().maxMemory(); mLruCache = new LruCache<String,Bitmap>(maxMemory/8){ @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; //創建線程池 mThreadPool = Executors.newFixedThreadPool(threadCount); mTaskQueue = new LinkedList<Runnable>(); mType = type; mPoolTThreadSemaphore = new Semaphore(threadCount); }
3.2、后台輪詢線程:
后台線程中,創建一個Handler用來處理圖片加載任務發過來的圖片顯示消息。
/** * 初始化后台輪詢線程 */ private void initBackThread() { //后台輪詢線程 mPoolThread = new Thread(){ @Override public void run() { Looper.prepare(); mPoolThreadHandler = new Handler(){ @Override public void handleMessage(Message msg) { //從線程池中取出一個任務開始執行 mThreadPool.execute(getTaskFromQueue()); try { mPoolTThreadSemaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } } }; //釋放信號量 mPoolThreadHandlerSemaphore.release(); Looper.loop(); } }; mPoolThread.start(); }
3.3、使用框架顯示圖片-加載圖片並顯示到ImageView上
加載顯示圖片的時候,判斷是否有LruCache,如果有的話,就從LruCache中取出來加載顯示;
否則的話,就新建一個圖片加載任務並添加到任務隊列中。
/** * 加載圖片並顯示到ImageView上 */ public void displayImage(final String path,final ImageView imageView ,final boolean isFromNet){ imageView.setTag(path); if(mUIHandler == null){ mUIHandler = new Handler(){ @Override public void handleMessage(Message msg) { // 獲取得到圖片,為imageview回調設置圖片 ImageHolder holder = (ImageHolder) msg.obj; Bitmap bmp = holder.bitmap; ImageView imageview = holder.imageView; String path = holder.path; // 將path與getTag存儲路徑進行比較,防止錯亂 if (imageview.getTag().toString().equals(path)) { if(bmp != null){ imageview.setImageBitmap(bmp); } } } }; } // 根據path在緩存中獲取bitmap Bitmap bm = getBitmapFromLruCache(path); if (bm != null) { refreshBitmap(path, imageView, bm); }else{//如果沒有LruCache,則創建任務並添加到任務隊列中 addTaskToQueue(createTask(path, imageView, isFromNet)); } }
3.4、創建圖片加載任務並添加到任務隊列中
圖片加載任務首先會判斷是否從網絡加載,如果是的話,再一次判斷是否有LruCache和DiskCache,如果都沒有的話, 就從網絡下載加載;
如果不從網絡加載,就直接從本地加載;最后無論是否網絡加載,都要把圖片寫入到LruCache和DiskCache中去,並且刷新顯示Bitmap到
ImageView上。
當然最后添加任務到任務隊列后,會通過mPoolThreadHandler.sendEmptyMessage(24)方法來通知后台線程去任務線程池中取出一個
任務線程來執行。
/** * 添加任務到任務隊列中 */ private synchronized void addTaskToQueue(Runnable runnable) { mTaskQueue.add(runnable); try { if (mPoolThreadHandler == null) mPoolThreadHandlerSemaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } mPoolThreadHandler.sendEmptyMessage(24); } /** * 根據參數,創建一個任務 */ private Runnable createTask(final String path, final ImageView imageView, final boolean isFromNet) { return new Runnable() { @Override public void run() { Bitmap bm = null; if (isFromNet) { File file = getDiskCacheDir(imageView.getContext(), Utils.makeMd5(path)); if (file.exists())// 如果在緩存文件中發現 { Log.v(TAG, "disk cache image :" + path); bm = loadImageFromLocal(file.getAbsolutePath(), imageView); } else { if (mIsDiskCacheEnable)// 檢測是否開啟硬盤緩存 { boolean downloadState = ImageDownloadUtils .downloadImageByUrl(path, file); if (downloadState)// 如果下載成功 { Log.v(TAG, "download image :" + path + " to disk cache: " + file.getAbsolutePath()); bm = loadImageFromLocal(file.getAbsolutePath(), imageView); } } else {// 直接從網絡加載 bm = ImageDownloadUtils.downloadImageByUrl(path, imageView); } } } else { bm = loadImageFromLocal(path, imageView); } // 3、把圖片加入到緩存 setBitmapToLruCache(path, bm); refreshBitmap(path, imageView, bm); mPoolTThreadSemaphore.release(); } }; }
3.4、顯示Bitmap到ImageView上
通過UIHandler發消息來顯示Bitmap到ImageView上去。
/** * 刷新圖片到ImageView */ private void refreshBitmap(final String path, final ImageView imageView, Bitmap bm) { Message message = Message.obtain(); ImageHolder holder = new ImageHolder(); holder.bitmap = bm; holder.path = path; holder.imageView = imageView; message.obj = holder; mUIHandler.sendMessage(message); }
最后,框架中使用到了兩個信號量,下面稍微解析下:
第一個:mPoolThreadHandlerSemaphore= new Semaphore(0); 用於控制我們的mPoolThreadHandler的初始化完成,我們在使用mPoolThreadHandler會進行判空,如果為null,會通過mPoolThreadHandlerSemaphore.acquire()進行阻塞;當mPoolThreadHandler初始化結束,我們會調用.release();解除阻塞。
第二個:mPoolTThreadSemaphore= new Semaphore(threadCount);這個信號量的數量和我們加載圖片的線程個數一致;每取一個任務去執行,我們會讓信號量減一;每完成一個任務,會讓信號量+1,再去取任務;目的是什么呢?為什么當我們的任務到來時,如果此時在沒有空閑線程,任務則一直添加到TaskQueue中,當線程完成任務,可以根據策略去TaskQueue中去取任務,只有這樣,我們的LIFO才有意義。
四.框架的使用實例
這里,我們用一個簡單GridView加載顯示1000張圖片來演示我們的框架使用。
4.1、布局文件實現:
activity_xcimager_loader.xml:
<RelativeLayout 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" tools:context=".XCImagerLoaderActivity"> <GridView android:id="@+id/gridview" android:layout_width="match_parent" android:layout_height="match_parent" android:numColumns="3" android:horizontalSpacing="5dp" android:verticalSpacing="5dp" > </GridView> </RelativeLayout>
layout_gridview_item.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="120dp"> <ImageView android:id="@+id/image_view" android:layout_width="match_parent" android:layout_height="120dp" android:scaleType="centerCrop"/> <TextView android:id="@+id/text_pos" android:layout_width="50dp" android:layout_height="50dp" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:text="1" android:gravity="center" android:textColor="#000000" android:background="#FFFF00" /> </RelativeLayout>
4.2、實例演示類文件實現:
public class XCImagerLoaderActivity extends AppCompatActivity { private GridView mGridView; private String[] mUrlStrs = ImageSources.imageUrls; private XCImageLoader mImageLoader; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_xcimager_loader); init(); mImageLoader = XCImageLoader.getInstance(3, XCImageLoader.Type.LIFO); } private void init() { mGridView = (GridView) findViewById(R.id.gridview); GridViewAdpter adapter = new GridViewAdpter(this,0,mUrlStrs); mGridView.setAdapter(adapter); } private class GridViewAdpter extends ArrayAdapter<String> { private Context mContext; public GridViewAdpter(Context context, int resource, String[] datas) { super(context, 0, datas); mContext = context; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate( R.layout.layout_gridview_item, parent, false); } ImageView imageview = (ImageView) convertView .findViewById(R.id.image_view); imageview.setImageResource(R.mipmap.img_default); TextView textview = (TextView)convertView.findViewById(R.id.text_pos); textview.setText(""+(position + 1)); mImageLoader.displayImage(getItem(position), imageview, true); return convertView; } } }
五.項目代碼目錄結構圖