Android有效避免程序OOM-图片压缩和三级缓存

前言

我们都知道现在的手机应用APP真的是给我们的生活带来了巨大的便利,应用中的图片也是精美绝伦特别好看,并且随着科技的进步,相机的分辨率也越来越高了,手机拍出来的照片可能达到十几兆很正常,图片这么大,在实际的开发过程中,还经常会遇到图片的加载等消耗内存比较大的情况,因为图片看起来更加生动形象,引人注意,给用户一种视觉冲击,效果自然就好。虽然手机现在性能越来越好了,但是因为图片的加载,特别是图片比较多的时候仍然是一个非常耗费内存的工作,这样的话我们就不得不考虑如何解决图片加载消耗内存的的问题了,如何解决性能图片内存占用情况呢?

  • 使用图片压缩技术

一个ImageView显示一张图片,特别是当ImageView的尺寸比较小的时候,图片比较大的时候,这时候图片的大小对视觉效果影响不大,但是这个时候图片的大小对内存占用影响就大了,我们需要对图片进行压缩显示缩略图即可。BitmapFactory这个类提供了丰富多彩的API供我们使用
这里写图片描述

BitmapFactory有多种生成Bitmap的方式,其中根据不同的情景有decodeByteArray,decodeFile,decodeResource,decodeStream等多种方式生成Bitmap对象,其中decodeByteArray方法可以对图片的字节数据进行处理,decodeFile可以根据图片的路径处理,decodeResource根据资源文件中的文件进行,decodeStream根据流进行处理,比如网络中的图片。

BitmapFactory.Options这个类就比较有意思了,其中这个类有一个布尔值参数inJustDecodeBounds,当inJustDecodeBounds等于true时候,解析程序并不加载当前的图片进入内存,返回的Bitmap等于NULL,虽然Bitmap为空,但是能够从options中获取到当前图片的各种属性信息,比如图片的宽高等。当inJustDecodeBounds为false时候就一切正常,图片加载进入内存了。我们可以根据这一点在图片加载进入内存前对图片进行压缩处理,然后将inJustDecodeBounds=false,进行显示即可。

我这里写好了一个图片的压缩类,如下,首先设置options.inJustDecodeBounds=true,这样的话资源就不会加载进入内存中,但是我们可以获取到图片的宽高信息,对图片进行压缩处理。

需要对图片的压缩比例做一个说明,假如我们得到的图片宽高是500*400像素,目标宽高是100*100,那么压缩比例如果是按照宽度的5来压缩的话,那么高度就会变成400/5=80像素,小于目标的宽高导致图片失真,如果按照高度的压缩比4来压缩的话,图片是125*100满足要求。所以这里图片的压缩比要选择比率比较小的一个进行压缩。

等获取到图片的压缩比例之后options.inJustDecodeBounds = false,这样就能够获取到Bitmap对象了,返回此对象到方法的调用处即可。

public class ImageCompress {
    /**
     * 根据需要的图片宽高和图片的字节数组获取Bitmap对象
     * @param bs     图片资源
     * @param width  目标宽
     * @param height 目标高
     * @return
     */
    public static Bitmap getBitmap(byte[] bs, int width, int height) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //inJustDecodeBounds = true时,资源不加载进入内存,获取图片宽高等信息对图片进行处理
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(bs, 0, bs.length, options);
        //获取图片的压缩比例
        options.inSampleSize = getCalcSize(options, width, height);
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeByteArray(bs, 0, bs.length, options);
        return bitmap;
    }

    /**
     * 获取图片的压缩比例
     * @param options
     * @param picwidth
     * @param picheight
     * @return
     */
    private static int getCalcSize(BitmapFactory.Options options, int picwidth, int picheight) {
        //原始比例
        int calcSize = 1;
        int width = options.outWidth;
        int height = options.outHeight;
        if (width > picheight || height > picheight) {
            int widthRate = Math.round((float) width / picwidth);
            int HeightRate = Math.round((float) height / picheight);
            //这里根据三目运算符 返回的应该是压缩比例比较小的一个比例,
            //因为这样的话能够保证宽高都是大于或者等于目标宽高,不会低于目标宽高导致失真
            calcSize = widthRate > HeightRate ? HeightRate : widthRate;
            return calcSize;
        }
        return 0;
    }
}
  • 图片的三级缓存处理技术

上面使用了图片的压缩技术,使得单张图片进行了压缩,但是当我们在ListView或者GridView中加载图片太多的时候,程序占用总内存会随着图片的增加而增加,最终导致内存溢出,所以仅仅图片压缩是不够的,还是会出现内存溢出的情况。

下面设想一种场景,当我们在ListView中滑动加载图片,有的图片已经划出屏幕了,我们就得考虑到图片的回收问题,使用GC回收,但是当我们又划回来的时候又得考虑到图片的重新加载,再次下载的话肯定是不仅浪费用户流量,还还降低性能,这个时候使用三级缓存技术就会提升性能不少。我们这里的三级缓存一般包含三个层级,首先是内存缓存,然后是SD卡缓存,最后是网络下载。下面我们一个一个来说。

1 内存缓存

内存缓存主要是LruCache类,这个类的算法原理是最近最少使用算法,即把最近经常使用的对象使用强的引用缓存到一个LinkedHashMap中,把最近最少使用的对象在缓存值达到峰值之前移除内存。我们需要设置一个合适的缓存大小作为程序的缓存,如果过大会导致OOM,过小就会频繁的回收和创建对象,降低程序的性能。

或许有的小伙伴会说到使用弱引用或者软引用也可以在内存缺乏时候释放资源啊,但是因为从API9开始GC垃圾回收器更加倾向于回收弱引用和软引用的对象,这样的话可能正在使用时候因为内存不够就将资源回收了,岂不是用户体验很差了嘛,所以不建议使用这种方式。

下面是使用LruCache缓存的类,我们一般设置为当前程序的八分之一作为缓存大小,这个设置的值不宜多大不宜过小,上面已经说过原因了。一般情况下系统给我们的程序分配的内存大小是16M,在高配置手机中会有32M的内存,那么在八分之一的缓存就会有4M大小。


public class MemoryCache {
    private LruCache<String, Bitmap> mLruCache;
    //当前程序分配的内存大小
    private int memorySize = 0;
    //缓存的内存大小
    private int cacheSize = 0;

    public MemoryCache() {
        memorySize = (int) Runtime.getRuntime().maxMemory();
        //设置缓存大小为程序内存的八分之一
        cacheSize = memorySize / 8;
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    /**
     * 向LruCache中添加Bitmap
     *
     * @param url
     * @param bitmap
     */
    public void addBitmapToLruCache(String url, Bitmap bitmap) {
        if (!TextUtils.isEmpty(url)) {
            if (bitmap != null) {
                mLruCache.put(url, bitmap);
            }
        }
    }

    /**
     * 从LruCache中获取图片Bitmap
     *
     * @param url
     * @return
     */
    public Bitmap getBitmapFromLruCache(String url) {
        if (!TextUtils.isEmpty(url) && mLruCache.get(url) != null) {
            return mLruCache.get(url);
        }
        return null;
    }

    /**
     * 删除某一个Bitmap
     *
     * @param key
     */
    public synchronized void removeBitmapFromLruCache(String key) {
        if (key != null) {
            Bitmap bitmap = mLruCache.get(key);
            if (bitmap != null) {
                bitmap.recycle();
            }
        }
    }

    /**
     * 清除LruCache中的内容
     */
    public void clear() {
        if (mLruCache.size() > 0) {
            mLruCache.evictAll();
        }
        mLruCache = null;
    }
}

2 SD卡缓存

因为我们的内存中会因为程序资源不足而被清除掉,这个时候如果我们想要在不重新下载图片的基础上快速显示图片的话,我们就要进行SD卡缓存了,SD卡的缓存策略其实比较简单,下面一个类有注解,比较简单,主要的就是添加方法addFileToSDCard和获取方法getFileFromSDCard。

public class SDCardCache {
    private static final String TAG = "aaa";
    private static final String SDPATH = Environment.getExternalStorageDirectory().getAbsolutePath();
    //SD卡是否挂载正常
    private boolean isMounted = false;
    //SD卡缓存的目录文件
    private String file = "fileCache";
    //缓存的总目录
    private File dirFile = null;

    /**
     * 构造方法中检查SD卡挂载情况 检查创建缓存目录
     */
    public SDCardCache() {
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            isMounted = true;
            dirFile = new File(SDPATH, file);
            if (!dirFile.exists()) {
                dirFile.mkdirs();
            }
        } else {
            Log.i(TAG, "SDCardCache: sdcard出错了");
        }
    }

    /**
     * 向SD卡中添加图片
     *
     * @param bs  图片资源
     * @param url 图片的url地址
     */
    public void addFileToSDCard(byte[] bs, String url) {
        if (isMounted) {
            if (!dirFile.exists()) {
                return;
            }
            String fileName = url.substring(url.indexOf("/") + 1);
            try {
                File file = new File(dirFile, fileName);
                Log.i(TAG, "addFileToSDCard: " + file.getAbsolutePath());
                FileOutputStream outputStream = new FileOutputStream(file);
                outputStream.write(bs, 0, bs.length);
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从SD卡中获取图片
     *
     * @param url
     * @return
     */
    public Bitmap getFileFromSDCard(String url) {
        Bitmap bitmap = null;
        if (isMounted && !TextUtils.isEmpty(url)) {
            String fileName = url.substring(url.indexOf("/") + 1);
            File file = new File(dirFile, fileName);
            bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
            return bitmap;
        }
        return null;
    }

    /**
     * 从SD卡中移除某一个固定的图片
     *
     * @param url
     * @return
     */
    private boolean removeFromSDCard(String url) {
        if (isMounted && !TextUtils.isEmpty(url)) {
            String fileName = url.substring(url.indexOf("/") + 1);
            File file = new File(dirFile, fileName);
            if (file.exists()) {
                return file.delete();
            }
        }
        return false;
    }

    /**
     * 清除文件中的缓存
     */
    private void chear() {
        if (isMounted) {
            File[] files = dirFile.listFiles();
            for (File file : files) {
                file.delete();
            }
        }
    }
}

3 网络获取

网络下载就不应该叫做缓存了,应该叫做下载,这里我使用了线程池和接口回调。

public class WebCache {
    /**
     * 根据url从网络下载,这里使用了线程池和接口回调,因为等我们获取到资源时候还不一定立马进行处理,在回调中处理
     *
     * @param poolExecutor
     * @param url
     * @param callback
     */
    public void getWebCache(ThreadPoolExecutor poolExecutor, final String url, final CallBacks callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    byte[] bs = getBytes(url);
                    if (bs != null) {
                        callback.getResult(bs);
                    } else {
                        Log.i("aaa", "run: 获取数据为空");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        poolExecutor.execute(runnable);
    }

    /**
     * 通过流的方式获取图片的二进制数据
     *
     * @param stringUrl
     * @return
     */
    private byte[] getBytes(String stringUrl) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        try {
            URL url = new URL(stringUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            //设置主机读取时间超时
            connection.setReadTimeout(10000);
            //设置主机连接时间超时
            connection.setConnectTimeout(10000);
            //设置请求方法为GET
            connection.setRequestMethod("GET");
            //设置以后可以使用conn.getInputStream().read();
            connection.setDoInput(true);
            //连接
            connection.connect();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                int len = 0;
                byte[] bytes = new byte[1024];
                InputStream inputStream = connection.getInputStream();
                while ((len = inputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                    outputStream.flush();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
                return outputStream.toByteArray();
            }

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    interface CallBacks {
        void getResult(byte[] bs);
    }
}

4 三级缓存CacheManager类

具体的使用场景是这样的,如果我们有一个图片的url地址,首先会检查内存缓存中是否缓存了这个图片,如果有的话就获取显示,没的话就会走下一步SD卡缓存;如果SD卡缓存中有这个资源的话就获取显示,然后缓存到内存缓存中,如果没有的话就走下一个网络缓存;如果走进了网络缓存的话,直接通过网络下载,然后分别缓存进入内存缓存和SD卡缓存中即可,CacheManager类我是用了最常用的单例模式,如下

public class CacheManager {
    private MemoryCache mMemoryCache = new MemoryCache();
    private SDCardCache mSDCardCache = new SDCardCache();
    private WebCache mWebCache = new WebCache();
    private Handler mHandler = new Handler();
    private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(128));

    /**
     * 构造方法私有化
     */
    private CacheManager() {
    }

    /**
     * 单例模式
     * @return
     */
    public static CacheManager getInstance() {
        return SingleTonHolder.INSTANCE;
    }

    private static class SingleTonHolder {
        private static final CacheManager INSTANCE = new CacheManager();
    }

    public void getCache(final String url, final ImageView imageView) {
        if (mMemoryCache.getBitmapFromLruCache(url) != null) {
            Log.i("aaa", "1 内存中执行了。。。");
            imageView.setImageBitmap(mMemoryCache.getBitmapFromLruCache(url));
        } else if (mSDCardCache.getFileFromSDCard(url) != null) {
            Log.i("aaa", "2 SD卡中执行了。。。");
            imageView.setImageBitmap(mSDCardCache.getFileFromSDCard(url));
            mMemoryCache.addBitmapToLruCache(url, mSDCardCache.getFileFromSDCard(url));
        } else {
            Log.i("aaa", "3 网络下载数据执行了");
            mWebCache.getWebCache(poolExecutor, url, new WebCache.CallBacks() {
                @Override
                public void getResult(byte[] bs) {
                    final Bitmap bitmap = ImageCompress.getBitmap(bs, 400, 400);
                    mSDCardCache.addFileToSDCard(Bitmap2Bytes(bitmap), url);
                    mMemoryCache.addBitmapToLruCache(url, bitmap);
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            imageView.setImageBitmap(bitmap);
                            Log.i("aaa", "run: " + "图片显示了");
                        }
                    });
                }
            });
        }
    }

    public byte[] Bitmap2Bytes(Bitmap bm) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
        return baos.toByteArray();
    }
}

5 三级缓存的使用

上面讲解了那么多的图片压缩和三级缓存,下面终于到了最后的使用阶段了,其实使用是最最简单的了。如果想要显示一张图片到ImageView中的话,只需要一句话就可调用,我们比较一下和Glide的使用方式,是不是有点相似呢。

public void onClick(View view) {
        /**
         * 三级缓存调用方式
         */
        CacheManager.getInstance().getCache(imageUrl3, (ImageView) findViewById(R.id.image));
        /**
         * Glide的调用方式
         */
        //Glide.with(this).load(imageUrl3).into((ImageView) findViewById(R.id.image));
    }

总结:有了上面的图片压缩策略和三级缓存策略,无论是加载单张或者多张图片,就不用担心程序OOM了。(完)

猜你喜欢

转载自blog.csdn.net/oman001/article/details/79060006