[安卓筆記]異步加載大量圖片

上一篇我們瞭解瞭android中幾種解決OOM的方法,下面總結下: 1.使用Bitmap.Options對圖片進行適度的縮放 2.圖片使用完後,記得將圖片置為null,並recycle 3.如果圖片是顯示到listView或GridView等控件上,應該使用ViewHolder+ConvertView的方式重用View對象 4.使用LruCache對圖片進行內存緩存 5.使用文件緩存 ———————————————- 本篇將綜合使用上述方式來實現一個小案例,異步加載大量圖片。 ———————————————– 1.首先建立一個數據源,存放圖片的網址,這裡采用tomcat作為服務器:

package cn.edu.chd.datasource;
/**
 * @author Rowand jj
 *提供圖片資源路徑的類
 */
public class Images
{
    public static final String[] imageThumbUrls = {
        "/wp-content/images1/20181210/1209.jpg",
          ... ...
        "/wp-content/images1/20181210/28210.jpg",
        "/wp-content/images1/20181210/29211.bmp",
    };
}

2.對Bitmap進行縮放的工具類:

package cn.edu.chd.utils;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/**
 * @author Rowand jj
 * 壓縮圖片
 */
public class BitmapUtils
{
    /**
     * 根據資源id獲取到圖片,並進行壓縮
     * @param res
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static Bitmap decodeSampledBitmapFromResource(Resources res,
            int resId, int reqWidth, int reqHeight)
    {
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, opts);
        int inSampleSize = cacluateInSampleSize(opts, reqWidth, reqHeight);
        opts.inSampleSize = inSampleSize;
        opts.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeResource(res, resId, opts);
        return bitmap;
    }

    /**
     * 從byte數組中獲取圖片並壓縮
     * @param data
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static Bitmap decodeSampledBitmapFromByteArray(byte[] data,
            int reqWidth, int reqHeight)
    {
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(data, 0, data.length, opts);
        int inSampleSize = cacluateInSampleSize(opts, reqWidth, reqHeight);
        opts.inJustDecodeBounds = false;
        opts.inSampleSize = inSampleSize;
        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length,
                opts);
        return bitmap;
    }
    
    private static int cacluateInSampleSize(BitmapFactory.Options opts,
            int reqWidth, int reqHeight)
    {
        if (opts == null)
            return 1;

        int inSampleSize = 1;
        int realWidth = opts.outWidth;
        int realHeight = opts.outHeight;

        if (realHeight > reqHeight || realWidth > reqWidth)
        {
            int heightRatio = realHeight / reqHeight;
            int widthRatio = realWidth / reqWidth;

            inSampleSize = (heightRatio > widthRatio) ? widthRatio
                    : heightRatio;
        }
        return inSampleSize;
    }
}

3.使用lrucache對bitmap進行內存緩存的類

package cn.edu.chd.utils;

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import android.util.Log;

/**
 * @author Rowand jj
 *
 *使用lrucache緩存圖片到內存,做成瞭單例模式
 */
public class BitmapLruCacheHelper
{
    private static final String TAG = null;
    private static BitmapLruCacheHelper instance = new BitmapLruCacheHelper();
    private LruCache cache = null;
    private BitmapLruCacheHelper()
    {
        int maxSize = (int) (Runtime.getRuntime().maxMemory()/8);
        cache = new LruCache(maxSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value)
            {
                return value.getRowBytes()*value.getHeight();
            }
        };
    }
    
    /**
     *加入緩存 
     * @param key
     * @param value
     */
    public void addBitmapToMemCache(String key,Bitmap value)
    {
        if(key == null || value == null)
        {
            return;
        }
        if(cache!=null && getBitmapFromMemCache(key)==null)
        {
            cache.put(key, value);
            Log.i(TAG,"put to lrucache success");
        }
    }
    
    /**
     * 從緩存中獲取圖片
     * @param key
     * @return
     */
    public Bitmap getBitmapFromMemCache(String key)
    {
        if(key == null)
        {
            return null;
        }
        Bitmap bitmap = cache.get(key);
        Log.i(TAG,"from lrucache,bitmap="+bitmap);
        return bitmap;
    }
    
    /**
     * 獲取實例
     * @return
     */
    public static BitmapLruCacheHelper getInstance()
    {
        return instance;
    }
}

4.文件緩存的類

package cn.edu.chd.utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Comparator;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.os.StatFs;
import android.util.Log;

/**
 * @author Rowand jj
 *
 *文件緩存
 */
public class FileCacheUtils
{
    /**
     *圖片緩存的相對路徑 
     */
    private static final String IMG_CACH_DIR = "/imgCache";
    
    /**
     * 手機緩存目錄
     */
    private static String DATA_ROOT_PATH = null;
    /**
     * sd卡根目錄
     */
    private static String SD_ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath();
    
    /**
     *緩存的擴展名 
     */
    private static final String CACHE_TAIL = ".cach";
    
    /**
     * 最大緩存空間,單位是mb
     */
    private static final int CACHE_SIZE = 4;
    
    /**
     * sd卡內存低於此值時將會清理緩存,單位是mb
     */
    private static final int NEED_TO_CLEAN = 10;

    /**
     * 上下文
     */
    private Context context;
    
    private static final String TAG = "BitmapFileCacheUtils";
    
    
    public FileCacheUtils(Context context)
    {
        this.context = context;
        DATA_ROOT_PATH = context.getCacheDir().getAbsolutePath();
    }
    /**
     * 從緩存中獲取一張圖片
     */
    public Bitmap getBitmapFromFile(String key)
    {
        if(key==null)
        {
            return null;
        }
        String filename = getCacheDirectory()+File.separator+convertKeyToFilename(key);
        File file = new File(filename);
        if(file.exists())
        {
            Bitmap bitmap = BitmapFactory.decodeFile(filename);
            if(bitmap == null)
            {
                file.delete();
            }
            else
            {
                updateFileModifiedTime(filename);
                Log.i(TAG,"get file from sdcard cache success...");
                return bitmap;
            }
        }
        return null;
    }
    /**
     * 將圖片存入文件緩存
     */
    public void addBitmapToFile(String key,Bitmap bm)
    {
        if(bm == null || key == null)
        {
            return;
        }
        //視情況清除部分緩存
        removeCache(getCacheDirectory());
        
        String filename = convertKeyToFilename(key);
        File dir = new File(getCacheDirectory());
        if(!dir.exists())
        {
            dir.mkdirs();
        }
        File file = new File(dir, filename);
        try
        {
            OutputStream out = new FileOutputStream(file);//這裡需要註意,如果指定目錄不存在,應該先調用mkdirs生成目錄,否則可能創建文件失敗
            bm.compress(CompressFormat.JPEG,100, out);
            out.close();
            Log.i(TAG,"add file to sdcard cache success...");
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    /**
     * 獲取文件緩存路徑
     * @return
     */
    private String getCacheDirectory()
    {
        String cachePath = null;
        if(isSdcardAvailable())
        {
            cachePath = SD_ROOT_PATH+IMG_CACH_DIR;
        }else
        {
            cachePath = DATA_ROOT_PATH+IMG_CACH_DIR;
        }
        return cachePath;
    }
    /**
     * 
     * 清除40%的緩存,這些緩存被刪除的優先級根據近期使用時間排列,越久沒被使用,越容易被刪除
     */
    private void removeCache(String dirPath)
    {
        File dir = new File(dirPath);
        File[] files = dir.listFiles();
        if(files == null)
        {
            return;
        }
        double total_size = 0;
        for(File file : files)
        {
            total_size+=file.length();
        }
        total_size = total_size/1024/1024;
        if(total_size > CACHE_SIZE || getSdCardFreeSpace() <= NEED_TO_CLEAN)
        {
            Log.i(TAG,"remove cache from sdcard cache...");
            int removeFactor = (int) (files.length*0.4);
            Arrays.sort(files, new FileLastModifiedComparator());
            for(int i = 0; i < removeFactor; i++)
            {
                files[i].delete();
            }
        }
    }
    
    /**
     *獲取sd卡可用空間
     */
    private int getSdCardFreeSpace()
    {
        StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());
        double freespace = stat.getAvailableBlocks()*stat.getBlockSize();
        return (int) (freespace/1024/1024);
    }
    /**
     *判斷sd卡是否可用
     * @return
     */
    private boolean isSdcardAvailable()
    {
        return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
    }
    /**
     * 將關鍵字轉化為文件名
     */
    private String convertKeyToFilename(String key)
    {
        if(key == null)
        {
            return "";
        }
        return key.hashCode()+CACHE_TAIL;
    }
    /**
     * 更新文件最後修改時間
     */
    private void updateFileModifiedTime(String path)
    {
        File file = new File(path);
        file.setLastModified(System.currentTimeMillis());
    }

    private class FileLastModifiedComparator implements Comparator
    {
        @Override
        public int compare(File lhs, File rhs)
        {
            if(lhs.lastModified() > rhs.lastModified())
            {
                return 1;
            }else if(lhs.lastModified() == rhs.lastModified())
            {
                return 0;
            }else
            {
                return -1;
            }
        }
    }
}

5.一個圖片下載器的類

package cn.edu.chd.myimageloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Message;
import cn.edu.chd.utils.BitmapLruCacheHelper;
import cn.edu.chd.utils.BitmapUtils;
import cn.edu.chd.utils.FileCacheUtils;

/**
 * @author Rowand jj
 *下載圖片的工具類
 *    通過downloadImage方法下載圖片,並將圖片保存到緩存中(使用線程池)。對下載得到的圖片交由一個回調接口OnImageDownloadListener處理
 *    通過showCacheImage方法獲取緩存中的圖片
 */
public class ImageDownloader
{
    /**
     * 下載image的線程池
     */
    private ExecutorService mImageThreadPool = null;

    /**
     * 文件緩存的工具類
     */
    private FileCacheUtils fileCacheUtils = null;

    /**
     * 線程池中線程的數量
     */
    private static final int THREAD_NUM = 2;
    
    /**
     * 縮略圖的寬
     */
    private static final int REQ_WIDTH = 90;
    /**
     * 縮略圖的高
     */
    private static final int REQ_HEIGHT = 90;

    protected static final int DOWNLOAD = 1;

    private Context context;

    /**
     * 構造器
     * @param context
     */
    public ImageDownloader(Context context)
    {
        this.context = context;
        fileCacheUtils = new FileCacheUtils(context);
    }
    
    /**
     * 下載一張圖片,先從內存緩存中找,如果沒有則去文件緩存中找,如果還沒有就從網絡中下載
     * @param url
     * @param listener
     * @return
     */
    public Bitmap downloadImage(final String url,final OnImageDownloadListener listener)
    {
        final String subUrl = url.replaceAll("[^\\w]", "");
        Bitmap bitmap = showCacheBitmap(subUrl);
        if(bitmap!=null)//緩存中找到
        {
            return bitmap;
        }else//緩存中未找到,則開啟線程下載
        {
//            new AsyncTask()
//            {
//                @Override
//                protected Bitmap doInBackground(String... params)
//                {
//                    Bitmap bitmap = getImageFromUrl(url);//從網絡上下載圖片
//                    fileCacheUtils.addBitmapToFile(subUrl,bitmap);//加到文件緩存
//                    BitmapLruCacheHelper.getInstance().addBitmapToMemCache(subUrl, bitmap);//加到內存緩存
//                    return bitmap;
//                }
//                protected void onPostExecute(Bitmap result) 
//                {
//                    listener.onImageDownload(url, result);
//                }
//            }.execute(url);
            
            final Handler handler = new Handler()
            {
                @Override
                public void handleMessage(Message msg)
                {
                    if(msg.what == DOWNLOAD)
                    {
                        listener.onImageDownload(url,(Bitmap)msg.obj);//對下載後的圖片的操作交由listener實現類處理
                    }
                }
            };
            getThreadPool().execute(new Runnable()//從線程池中獲取一個線程執行下載操作並將下載後的圖片加到文件緩存和內存緩存
            {
                @Override
                public void run()
                {
                    Bitmap bitmap = getImageFromUrl(url);//從網絡上下載圖片
                    Message msg = Message.obtain(handler, DOWNLOAD, bitmap);
                    msg.sendToTarget();//發送消息
                    
                    //加到緩存中
                    fileCacheUtils.addBitmapToFile(subUrl,bitmap);
                    BitmapLruCacheHelper.getInstance().addBitmapToMemCache(subUrl, bitmap);
                }
            });
            
        }
        return null;
    }
    
    /**
     * 顯示緩存中的圖片
     * @param url
     * @return
     */
    public Bitmap showCacheBitmap(String url)
    {
        Bitmap bitmap = BitmapLruCacheHelper.getInstance().getBitmapFromMemCache(url);
        if(bitmap!=null)//首先從內存緩存中找
        {
            return bitmap;
        }else
        {
            bitmap = fileCacheUtils.getBitmapFromFile(url);
            if(bitmap!=null)//在文件緩存中找到
            {
                BitmapLruCacheHelper.getInstance().addBitmapToMemCache(url, bitmap);//加入內存緩存
                return bitmap;
            }
        }
        return null;
    }
    /**
     * 獲取線程池實例
     */
    public ExecutorService getThreadPool()
    {
        if (mImageThreadPool == null)
        {
            synchronized (ExecutorService.class)
            {
                if (mImageThreadPool == null)
                {
                    mImageThreadPool = Executors.newFixedThreadPool(THREAD_NUM);
                }
            }
        }
        return mImageThreadPool;
    }

    /**
     * 從url中獲取bitmap
     * @param url
     * @return
     */
    public Bitmap getImageFromUrl(String url)
    {
        HttpURLConnection conn = null;
        try
        {
            URL target = new URL(url);
            conn = (HttpURLConnection) target.openConnection();
            conn.setReadTimeout(3000);
            conn.setConnectTimeout(10 * 1000);
            conn.setDoInput(true);

            if (conn.getResponseCode() == 200)
            {
                InputStream is = conn.getInputStream();
                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                int len = 0;
                byte[] buf = new byte[1024];
                while((len = is.read(buf))!=-1)
                {
                    bout.write(buf, 0, len);
                }
                is.close();
                byte[] data = bout.toByteArray();
                return BitmapUtils.decodeSampledBitmapFromByteArray(data,REQ_WIDTH, REQ_HEIGHT);//返回的是壓縮後的縮略圖
            }

        } catch (Exception e)
        {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 取消當前的任務
     */
    public synchronized void cancellTask()
    {
        if(mImageThreadPool != null)
        {
            mImageThreadPool.shutdownNow();
            mImageThreadPool = null;
        }
    }
    /**
     *操作下載後的圖片的回調接口
     */
    public interface OnImageDownloadListener
    {
        void onImageDownload(String url,Bitmap bitmap);
    }
}

6.GridView的適配器: 當GridView滑動時停止下載圖片,GridView停止滑動時下載圖片。

package cn.edu.chd.myimageloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import cn.edu.chd.myimageloader.ImageDownloader.OnImageDownloadListener;
 
public class ImageAdapter extends BaseAdapter implements OnScrollListener
{
    private GridView gridView;
    private Context context;
    private String[] imageThumUrls;
    private ImageDownloader mImageDownloader;
    private boolean isFirstEnter = true;
    private int mFirstVisibleItem;
    private int mVisibleItemCount;
    
    public ImageAdapter(Context context,String[] imageThumUrls,GridView gridView)
    {
        this.context = context;
        this.gridView = gridView;
        this.imageThumUrls = imageThumUrls;
        this.mImageDownloader = new ImageDownloader(context);
        gridView.setOnScrollListener(this);
    }
    @Override
    public int getCount()
    {
        return imageThumUrls.length;
    }

    @Override
    public Object getItem(int position)
    {
        return imageThumUrls[position];
    }

    @Override
    public long getItemId(int position)
    {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        ImageView mImageView;
        String imageUrl = imageThumUrls[position];
        if(convertView == null)
        {
            mImageView = new ImageView(context);
        }else
        {
            mImageView = (ImageView) convertView;
        }
        mImageView.setLayoutParams(new GridView.LayoutParams(90,90));
        mImageView.setTag(imageUrl);
        
        //隻顯示緩存圖片,如果緩存中沒有則設置一張默認的圖片
        Bitmap bitmap = mImageDownloader.showCacheBitmap(imageUrl.replaceAll("[^\\w]",""));
        if(bitmap != null)
        {
            mImageView.setImageBitmap(bitmap);
        }else
        {
            mImageView.setImageResource(R.drawable.ic_launcher);
        }
        return mImageView;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState)
    {
        if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE)//滑動停止時啟動下載圖片
        {
            showImage(mFirstVisibleItem, mVisibleItemCount);
        }else
        {
            cancellTask();
        }
    }
    
    /**
     * 滾動時執行此方法
     * 第一次進入會調用showImage顯示圖片
     * */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount)
    {
        mFirstVisibleItem = firstVisibleItem;
        mVisibleItemCount = visibleItemCount;
        
        if(isFirstEnter && visibleItemCount>0)
        {
            showImage(firstVisibleItem, visibleItemCount);
            isFirstEnter = false;
        }
    }
    
    /**
     * 顯示圖片,先從緩存中找,如果沒找到就開啟線程下載
     * @param firstVisibleItem 第一個可見項的id
     * @param visibleItemCount 可見項的總數
     */
    private void showImage(int firstVisibleItem,int visibleItemCount)
    {
        for(int i = firstVisibleItem; i < firstVisibleItem+visibleItemCount;i++)
        {
            String mImageUrl = imageThumUrls[i];
            final ImageView mImageView = (ImageView) gridView.findViewWithTag(mImageUrl);
            mImageDownloader.downloadImage(mImageUrl, new OnImageDownloadListener()
            {
                @Override
                public void onImageDownload(String url, Bitmap bitmap)
                {
                    if(mImageView != null && bitmap!=null)
                    {
                        mImageView.setImageBitmap(bitmap);//下載後直接設置到view對象上
                    }
                }
            });
        }
    }
    
    /**
     * 取消下載任務
     */
    public void cancellTask()
    {
        mImageDownloader.cancellTask();
    }
    
}

7MainActivity

package cn.edu.chd.myimageloader;

import cn.edu.chd.datasource.Images;
import android.app.Activity;
import android.os.Bundle;
import android.widget.GridView;

public class MainActivity extends Activity
{
    private GridView gridView = null;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        gridView = (GridView) findViewById(R.id.gridView);
        gridView.setAdapter(new ImageAdapter(this, Images.imageThumbUrls, gridView));
    }
}

8.佈局:


    
    

9.權限:


 

大功告成~

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *