Android高级学习之图片缓存

防止多图OOM的解决思路是LRU算法
LRU(Least Recently Used), 即近期最少使用算法.
使用缓存策略, 对网络上下载的图片等资源文件进行缓存, 当再次请求同一个资源url时, 首先从缓存中查找是否存在, 当不存在时再从网络上下载。采用缓存, 除了提高获取资源的速度, 也对减少使用用户手机上的流量有很好的作用. 核心思想是当缓存满时,会优先淘汰那些最少使用的缓存对象。采用LRU算法的缓存有两种,LruCache用于内存缓存, DiskLruCache用于存储设备缓存, 它通过把对象写入文件系统从而实现缓存的效果.
由于LruCache只是在内存上管理图片的存储与释放,如果内存被清理的话那又得需要重新从网络上加载,非常耗时。而DiskLruCache是写入
存储设备的,相对来讲,速度会慢一点,另外,如果sd卡已卸载的话,那也不能缓存图片。所以,一般是综合使用。
他们内部都是使用LinkedHashMap来存储要缓存的对象。因为get一个元素后,这个元素被加到最后(使用了LRU 最近最少被使用的调度算法).
对LinkedHashMap调用get(k)和put(k,v), 会把最新访问的对象移到链表头部,这样链表尾部就成为了最久没有使用的数据结点。这样当缓存空间达到最大值时,删除链表的第一个元素就可以减少缓存所占用的空间了, 这就实现了LRU的核心算法.

DiskLruCache的使用

DiskLruCache大多使用非常简单,下载源码包,然后只需要在项目中新建一个libcore.io包,然后将DiskLruCache.Java文件复制到这个包中即可。

打开缓存

直接调用类中的静态open()方法

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。
缓存地址通常都会存放在 /sdcard/Android/data/application package/cache 这个路径下面,所以在使用之前需要判断sd卡的状态
需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。

写入缓存

写入的操作是借助DiskLruCache.Editor这个类完成的,使用的是静态方法edit()

public Editor edit(String key) throws IOException 

key为存储的文件名,由于图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。为解决这个问题,可以将URL进行MD5编码:

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

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

有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能了。注意newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。

读取缓存

直接调用DiskLruCache的静态方法get()即可:

 public synchronized Snapshot get(String key) throws IOException 

key就是将图片URL进行MD5编码后的值,这里我们获取到的是Snapshot对象,接着只需要调用它的getInputStream()方法就可以得到缓存文件的输入流了。同样地,getInputStream()方法也需要传一个index参数,这里传入0就好。有了文件的输入流之后,想要把缓存图片显示到界面上就轻而易举了

 Bitmap bitmap = BitmapFactory.decodeStream(is);  
 mImage.setImageBitmap(bitmap);  

移除缓存

直接调用DiskLruCache的remove()方法即可

    public synchronized boolean remove(String key) throws IOException  

其他API

size()

获取缓存数据的所有字节数

flush()

将内存中的操作记录同步到日志文件(也就是journal文件),在Activity的onPause()方法中去调用一次flush()方法就可以了。

close()

关闭硬盘存储的功能,关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。

delete()

这个方法用于将所有的缓存数据全部删除,比如说网易新闻中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。

最佳使用

如果仅仅是使用硬盘缓存的话,程序是有明显短板的。而如果只使用内存缓存的话,程序当然也会有很大的缺陷。因此,一个优秀的程序必然会将内存缓存和硬盘缓存结合到一起使用。可以这样使用:
每次加载图片的时候都优先去内存缓存当中读取,当读取不到的时候则回去硬盘缓存中读取,而如果硬盘缓存仍然读取不到的话,就从网络上请求原始数据。不管是从硬盘缓存还是从网络获取,读取到了数据之后都应该添加到内存缓存当中,这样的话我们下次再去读取图片的时候就能迅速从内存当中读取到,而如果该图片从内存中被移除了的话,那就重复再执行一遍上述流程就可以了。

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

        }  

    }  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值