接上一篇文章,首先我们再来复习一个listview的缓存优化方法
1,对Imageview使用setTag()方法来解决图片错位问题,这个Tag中设置的是图片的url,然后在加载的时候取得这个url和要加载那position中的url对比,如果不相同就加载,相同就是复用以前的就不加载了
2,对于要加载的图片资源,先在内存缓存中找(原始的方法是使用SoftRefrence,最新的方法是使用android提供的Lrucache),如果找不到,则在本地缓存(可以使用DiskLrucache类)中找(也就是读取原先下载过的本地图片),还找不到,就开启异步线程去下载图片,下载以后,保存在本地,内存缓存也保留一份引用
3,在为imagview装载图片时,先测量需要的图片大小,按比例缩放
4,使用一个Map保存异步线程的引用,key->value为url->AsyncTask,这样可以避免已经开启了线程去加载图片,但是还没有加载完时,又重复开启线程去加载图片的情况
5,在快速滑动的时候不加载图片,取消所有图片加载线程,一旦停下来,继续可见图片的加载线程
下面来看第三个例子:
FileUtils 文件操作的工具类,提供保存图片,获取图片,判断图片是否存在,删除图片的一些方法,这个类比较简单
package com.example.asyncimageloader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.os.Environment; public class FileUtils { /** * sd卡的根目录 */ private static String mSdRootPath = Environment.getExternalStorageDirectory().getPath(); /** * 手机的缓存根目录 */ private static String mDataRootPath = null; /** * 保存Image的目录名 */ private final static String FOLDER_NAME = "/AndroidImage"; public FileUtils(Context context){ mDataRootPath = context.getCacheDir().getPath(); } /** * 获取储存Image的目录 * @return */ private String getStorageDirectory(){ return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ? mSdRootPath + FOLDER_NAME : mDataRootPath + FOLDER_NAME; } /** * 保存Image的方法,有sd卡存储到sd卡,没有就存储到手机目录 * @param fileName * @param bitmap * @throws IOException */ public void savaBitmap(String fileName, Bitmap bitmap) throws IOException{ if(bitmap == null){ return; } String path = getStorageDirectory(); File folderFile = new File(path); if(!folderFile.exists()){ folderFile.mkdir(); } File file = new File(path + File.separator + fileName); file.createNewFile(); FileOutputStream fos = new FileOutputStream(file); bitmap.compress(CompressFormat.JPEG, 100, fos); fos.flush(); fos.close(); } /** * 从手机或者sd卡获取Bitmap * @param fileName * @return */ public Bitmap getBitmap(String fileName){ return BitmapFactory.decodeFile(getStorageDirectory() + File.separator + fileName); } /** * 判断文件是否存在 * @param fileName * @return */ public boolean isFileExists(String fileName){ return new File(getStorageDirectory() + File.separator + fileName).exists(); } /** * 获取文件的大小 * @param fileName * @return */ public long getFileSize(String fileName) { return new File(getStorageDirectory() + File.separator + fileName).length(); } /** * 删除SD卡或者手机的缓存图片和目录 */ public void deleteFile() { File dirFile = new File(getStorageDirectory()); if(! dirFile.exists()){ return; } if (dirFile.isDirectory()) { String[] children = dirFile.list(); for (int i = 0; i < children.length; i++) { new File(dirFile, children[i]).delete(); } } dirFile.delete(); } }
ImageDownLoader类,异步下载的核心类,保存图片到手机缓存,将图片加入LruCache中等等
package com.example.asyncimageloader; import java.io.IOException; 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.graphics.BitmapFactory; import android.os.Handler; import android.os.Message; import android.support.v4.util.LruCache; public class ImageDownLoader { /** * 缓存Image的类,当存储Image的大小大于LruCache设定的值,系统自动释放内存 */ private LruCache<String, Bitmap> mMemoryCache; /** * 操作文件相关类对象的引用 */ private FileUtils fileUtils; /** * 下载Image的线程池 */ private ExecutorService mImageThreadPool = null; public ImageDownLoader(Context context){ //获取系统分配给每个应用程序的最大内存,每个应用系统分配32M int maxMemory = (int) Runtime.getRuntime().maxMemory(); int mCacheSize = maxMemory / 8; //给LruCache分配1/8 4M mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){ //必须重写此方法,来测量Bitmap的大小 @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; fileUtils = new FileUtils(context); } /** * 获取线程池的方法,因为涉及到并发的问题,我们加上同步锁 * @return */ public ExecutorService getThreadPool(){ if(mImageThreadPool == null){ synchronized(ExecutorService.class){ if(mImageThreadPool == null){ //为了下载图片更加的流畅,我们用了2个线程来下载图片 mImageThreadPool = Executors.newFixedThreadPool(2); } } } return mImageThreadPool; } /** * 添加Bitmap到内存缓存 * @param key * @param bitmap */ public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null && bitmap != null) { mMemoryCache.put(key, bitmap); } } /** * 从内存缓存中获取一个Bitmap * @param key * @return */ public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); } /** * 先从内存缓存中获取Bitmap,如果没有就从SD卡或者手机缓存中获取,SD卡或者手机缓存 * 没有就去下载 * @param url * @param listener * @return */ public Bitmap downloadImage(final String url, final onImageLoaderListener listener){ //替换Url中非字母和非数字的字符,这里比较重要,因为我们用Url作为文件名,比如我们的Url //是Http://xiaanming/abc.jpg;用这个作为图片名称,系统会认为xiaanming为一个目录, //我们没有创建此目录保存文件就会报错 final String subUrl = url.replaceAll("[^\\w]", ""); Bitmap bitmap = showCacheBitmap(subUrl); if(bitmap != null){ return bitmap; }else{ final Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); listener.onImageLoader((Bitmap)msg.obj, url); } }; getThreadPool().execute(new Runnable() { @Override public void run() { Bitmap bitmap = getBitmapFormUrl(url); Message msg = handler.obtainMessage(); msg.obj = bitmap; handler.sendMessage(msg); try { //保存在SD卡或者手机目录 fileUtils.savaBitmap(subUrl, bitmap); } catch (IOException e) { e.printStackTrace(); } //将Bitmap 加入内存缓存 addBitmapToMemoryCache(subUrl, bitmap); } }); } return null; } /** * 获取Bitmap, 内存中没有就去手机或者sd卡中获取,这一步在getView中会调用,比较关键的一步 * @param url * @return */ public Bitmap showCacheBitmap(String url){ if(getBitmapFromMemCache(url) != null){ return getBitmapFromMemCache(url); }else if(fileUtils.isFileExists(url) && fileUtils.getFileSize(url) != 0){ //从SD卡获取手机里面获取Bitmap Bitmap bitmap = fileUtils.getBitmap(url); //将Bitmap 加入内存缓存 addBitmapToMemoryCache(url, bitmap); return bitmap; } return null; } /** * 从Url中获取Bitmap * @param url * @return */ private Bitmap getBitmapFormUrl(String url) { Bitmap bitmap = null; HttpURLConnection con = null; try { URL mImageUrl = new URL(url); con = (HttpURLConnection) mImageUrl.openConnection(); con.setConnectTimeout(10 * 1000); con.setReadTimeout(10 * 1000); con.setDoInput(true); con.setDoOutput(true); bitmap = BitmapFactory.decodeStream(con.getInputStream()); } catch (Exception e) { e.printStackTrace(); } finally { if (con != null) { con.disconnect(); } } return bitmap; } /** * 取消正在下载的任务 */ public synchronized void cancelTask() { if(mImageThreadPool != null){ mImageThreadPool.shutdownNow(); mImageThreadPool = null; } } /** * 异步下载图片的回调接口 * @author len * */ public interface onImageLoaderListener{ void onImageLoader(Bitmap bitmap, String url); } }
ImageDownLoader中有几个方法比较重要
- 首先我们需要重写sizeOf(String key, Bitmap value)来计算图片的大小,默认返回图片的数量
- downloadImage(final String url, final onImageLoaderListener listener)先去LruCache查看Image,没有再去手机缓存中查看,在没有则开启线程下载,这里我们提供了一个回调接口,回调方法中我们将Bitmap和图片Url作为参数,String subUrl = url.replaceAll("[^\\w]", "") 我在代码中注释写的比较清楚
- showCacheBitmap(String url)方法,此方法在Adapter中的getView()当中调用,如果getView()中不调用此方法试试你就知道效果了
- ImageAdapter GridView的适配器类,主要是GridView滑动的时候取消下载任务,静止的时候去下载当前显示的item的图片,其他也没什么不同了
package com.example.asyncimageloader; 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 com.example.asyncimageloader.ImageDownLoader.onImageLoaderListener; public class ImageAdapter extends BaseAdapter implements OnScrollListener{ /** * 上下文对象的引用 */ private Context context; /** * Image Url的数组 */ private String [] imageThumbUrls; /** * GridView对象的应用 */ private GridView mGridView; /** * Image 下载器 */ private ImageDownLoader mImageDownLoader; /** * 记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。 * 参考http://blog.csdn.net/guolin_blog/article/details/9526203#comments */ private boolean isFirstEnter = true; /** * 一屏中第一个item的位置 */ private int mFirstVisibleItem; /** * 一屏中所有item的个数 */ private int mVisibleItemCount; public ImageAdapter(Context context, GridView mGridView, String [] imageThumbUrls){ this.context = context; this.mGridView = mGridView; this.imageThumbUrls = imageThumbUrls; mImageDownLoader = new ImageDownLoader(context); mGridView.setOnScrollListener(this); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务 if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){ showImage(mFirstVisibleItem, mVisibleItemCount); }else{ cancelTask(); } } /** * GridView滚动的时候调用的方法,刚开始显示GridView也会调用此方法 */ @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { mFirstVisibleItem = firstVisibleItem; mVisibleItemCount = visibleItemCount; // 因此在这里为首次进入程序开启下载任务。 if(isFirstEnter && visibleItemCount > 0){ showImage(mFirstVisibleItem, mVisibleItemCount); isFirstEnter = false; } } @Override public int getCount() { return imageThumbUrls.length; } @Override public Object getItem(int position) { return imageThumbUrls[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ImageView mImageView; final String mImageUrl = imageThumbUrls[position]; if(convertView == null){ mImageView = new ImageView(context); }else{ mImageView = (ImageView) convertView; } mImageView.setLayoutParams(new GridView.LayoutParams(150, 150)); mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); //给ImageView设置Tag,这里已经是司空见惯了 mImageView.setTag(mImageUrl); /*******************************去掉下面这几行试试是什么效果****************************/ Bitmap bitmap = mImageDownLoader.showCacheBitmap(mImageUrl.replaceAll("[^\\w]", "")); if(bitmap != null){ mImageView.setImageBitmap(bitmap); }else{ mImageView.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_empty)); } /**********************************************************************************/ return mImageView; } /** * 显示当前屏幕的图片,先会去查找LruCache,LruCache没有就去sd卡或者手机目录查找,在没有就开启线程去下载 * @param firstVisibleItem * @param visibleItemCount */ private void showImage(int firstVisibleItem, int visibleItemCount){ Bitmap bitmap = null; for(int i=firstVisibleItem; i<firstVisibleItem + visibleItemCount; i++){ String mImageUrl = imageThumbUrls[i]; final ImageView mImageView = (ImageView) mGridView.findViewWithTag(mImageUrl); bitmap = mImageDownLoader.downloadImage(mImageUrl, new onImageLoaderListener() { @Override public void onImageLoader(Bitmap bitmap, String url) { if(mImageView != null && bitmap != null){ mImageView.setImageBitmap(bitmap); } } }); //if(bitmap != null){ // mImageView.setImageBitmap(bitmap); //}else{ // mImageView.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_empty)); //} } } /** * 取消下载任务 */ public void cancelTask(){ mImageDownLoader.cancelTask(); } }
MainActivity 里面一个GridView,然后提供一个系统菜单来删除手机上的缓存图片,直接上代码,比较简单所以里面也没有注释
package com.example.asyncimageloader; import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.GridView; import android.widget.Toast; public class MainActivity extends Activity { private GridView mGridView; private String [] imageThumbUrls = Images.imageThumbUrls; private ImageAdapter mImageAdapter; private FileUtils fileUtils; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fileUtils = new FileUtils(this); mGridView = (GridView) findViewById(R.id.gridView); mImageAdapter = new ImageAdapter(this, mGridView, imageThumbUrls); mGridView.setAdapter(mImageAdapter); } @Override protected void onDestroy() { mImageAdapter.cancelTask(); super.onDestroy(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add("删除手机中图片缓存"); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case 0: fileUtils.deleteFile(); Toast.makeText(getApplication(), "清空缓存成功", Toast.LENGTH_SHORT).show(); break; } return super.onOptionsItemSelected(item); } }
上面的代码比较完善也比较复杂,我们再来看一下别人的思路:基本思路也是内存缓存加文件缓存,内存缓存使用了LruCache<String, Bitmap>来让虚拟机自己管理,文件缓存则在写入文件时进行了压缩bitmap.compress(CompressFormat.JPEG, 100, fos),这都是比较好的思路。另外异步线程使用了线程池,加了锁,这样会使每次只能开两个线程去加载图片,这样确实避免了重复开启线程去加载图片的问题,但是效率也比较低(指下载效率)。另外图片下载完毕以后,也是交给handler去处理,从而更新UI。
在图片写入本地时也注意到了要替换图片路径中的非字母数字符号的问题。
另外给adapter添加了ScrollListener接口,在onScrollStateChanged(AbsListView view, int scrollState)方法中,判断当前滑动状态,如果状态为AbsListView.OnScrollListener.SCROLL_STATE_IDLE(也就是静止时),采取加载图片,否则取消所有下载线程。
在onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount)方法中,获取当前第一条可见的item的id,和可见item总数目,从而在下载时只下载看见item对应的图片。
使用imageview.gettag()方法避免图片错位的问题。
缺点是效率问题,每次最多有两个线程去下载图片,其他下载线程阻塞,另外没有对图片缩放进行处理,但是思路基本完善。
下面来看第四个例子:
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; } } }
思路与前一个例子相识,这里的重点是使用了DiskLrucache类来做文件缓存。
缺点较多,例如也会出现多个线程下载同一图片的问题,没有监听滑动状况。
看了上面的所有例子,我们现在来总结最佳优化方法的具体实现。首先上面所有的例子都没有处理图片的缩放问题。
可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;
那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源图片的高度和宽度 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // 计算出实际宽高和目标宽高的比率 final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 // 一定都会大于等于目标的宽和高。 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
也就说缩放是图片处理的第一步。
接着就是使用Lrucache,DiskLruCache来分别进行内存缓存和硬盘缓存。
如果缓存没有,开启线程去下载,我们要把每个线程保存在一个Map中,防止出现多个线程下载一张图片的线程(上面的所有例子都没有做到这一点)。另外也可以使用线程池,但是适当的将线程池的容量设置大一些,避免效率问题。
最后Adapter要继承OnScrollListener接口,在静止时才加载图片,否则取消所有下载线程。
使用Imageview.getTag()方法防止图片错位。
OK,到此为止,每个优化点的具体实现步骤我都说清楚,虽然有点复杂,但是大家思路正确,就一定可以写好。