1. 程式人生 > >Android高效非同步圖片載入框架

Android高效非同步圖片載入框架

一.概述

目前為止,第三方的圖片載入框架挺多的,比如UIL , Volley Imageloader等等。但是最好能知道實現原理,所以下面就來看看設計並開發一個載入網路、本地的圖片框架。

總所周知,圖片框架中肯定需要用到快取,這裡我們和其他框架一樣,採用LruCache來管理圖片的快取,當然圖片的載入測量使用LIFO比較好點,因為要載入最新的給使用者。

我們採用非同步訊息處理機制來實現圖片非同步載入任務:用於UI執行緒當Bitmap載入完成後更新ImageView。

載入網路圖片的原理,就是如果啟用了硬碟快取,載入時,先從記憶體中載入,然後從硬碟載入,最後再從網路下載。下載完成後,寫入硬碟和記憶體快取。

如果沒有啟用硬碟快取,就直接從網路壓縮下載獲取,最後加入記憶體快取即可。

二.演示效果圖

01.png 322919-20150911170618778-1983462232.png

三.圖片載入框架實現解析

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;
        }
    }
}

五.專案程式碼目錄結構圖

imageloader.png

注:本文著作權歸作者,由demo大師發表,拒絕轉載,轉載需要作者授權