如何避免OOM 和提高ListView流畅度

1 篇文章 0 订阅
1 篇文章 0 订阅

加载大图和多图开发是Android应用时会经常遇到的,如果处理不当就会出现OOM(OutOfMemory)异常,这是由于加载图片时所需的内存超过Android设备的内存所造成的。如何解决呢?


加载大图片之前我们可以查看APP能使用的最大内存:  

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");

在加载高分辨率图片的时候我们最好先进行压缩处理,压缩后的图片大小应该与展示它的控件大小差不多.

首先我们需要知道BitmapFactory这个类,BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:

    

BitmapFactory.Options options = new BitmapFactory.Options();  
options.inJustDecodeBounds = true;//true表示先不加载图片到内存,只是获取它的宽高,图片类型等信息  这个属性默认是false
BitmapFactory.decodeResource(getResources(), R.id.test, options);  
int imageHeight = options.outHeight; //获取量得的高 
int imageWidth = options.outWidth;  //获取量得的宽
String imageType = options.outMimeType; //获取图片类型

获取图片由三种方式:

1.从资源文件中获取:

public Bitmap FromRecourse(){
    Bitmap rawBitmap = null;
    try{
        rawBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
    }catch (OutOfMemoryError e){
        Toast.makeText(BitmapActivity.this,"内存溢出",Toast.LENGTH_LONG).show();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024/1024);
        Toast.makeText(this,"最大内存"+maxMemory+"M",Toast.LENGTH_LONG).show();
    }
    return rawBitmap;
}

2.从SD卡获取

public Bitmap FromSDcard1(){
    String SDcardPath = Environment.getExternalStorageDirectory().toString();
    String bitmapFile = SDcardPath+"/"+"test.jpg";
    Bitmap rawBitmap2 = null;
    try{
        rawBitmap2 = BitmapFactory.decodeFile(bitmapFile, null);
    }catch (OutOfMemoryError e){
        Toast.makeText(BitmapActivity.this,"内存溢出",Toast.LENGTH_LONG).show();
    }
    return rawBitmap2;
}

3.从SD卡获取

public Bitmap FromSDcard2(){
    InputStream stream = getBitmapInputStreamFromSDCard("test.jpg");
    Bitmap rawBitmap3 = BitmapFactory.decodeStream(stream);
    return rawBitmap3;
}

//SD卡获取Bitmap图片
public InputStream getBitmapInputStreamFromSDCard(String fileName){
    if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
        String SDcardPath = Environment.getExternalStorageDirectory().toString();
        String filePath = SDcardPath+"/"+fileName;
        File file = new File(filePath);

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            return fileInputStream;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }
    return null;
}

我们可以先将原始图片压缩到指定宽高的比例后再加载到内存

/**
 * 计算需要压缩尺寸的图片的压缩比
 * @param options
 * @param reqWidth 需要将图片的宽压缩到指定的值
 * @param reqHeight 需要将图片的高压缩到指定的值
 * @return 压缩比
 */
public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
    //源图片的宽 高
    int width = options.outWidth;
    int height = options.outHeight;
    int inSamleSize = 1;
    if(width > reqWidth || height > reqHeight){
        int scaleHeirht =  Math.round((float)height/reqHeight);
        int scaleWidth = Math.round((float)width/reqWidth);
        inSamleSize = scaleHeirht<scaleWidth?scaleHeirht:scaleWidth;
    }

    return inSamleSize;
}

大概就是这么个使用过程吧

private ImageView imageView ;

private Bitmap copyBitmap;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bitmap);
    imageView = (ImageView) findViewById(R.id.image_test);
    copyBitmap =  FromRecourse();
    if(copyBitmap!=null){
       //不为空说明直接加载(没有经过压缩)没用发送内存溢出
        compressBitmap(copyBitmap);
    }else{
        //先获取内存溢出图片的尺寸
     BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;//只加载高宽图片类型等信息
        BitmapFactory.decodeResource(getResources(), R.mipmap.test, options);
        int inSampleSize = calculateInSampleSize(options, 1080/2, 1920/2);//将图片压缩到宽高为1080/2 * 1920/2时的缩放因子
        options.inSampleSize = inSampleSize;//赋给options
        options.inJustDecodeBounds = false;//必须置为false,下载加载才会加载图片到内存
        copyBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test, options);
    }

    if(copyBitmap!=null){
        Drawable drawable = new BitmapDrawable(copyBitmap);
        imageView.setImageDrawable(drawable);
    }


}


使用图片缓存技术
在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。

为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
1、你的设备可以为每个应用程序分配多大的内存?
2、设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
3、你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
4、图片的尺寸和大小,还有每张图片会占据多少内存空间。
5、图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
6、你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。

下面是一个使用 LruCache 来缓存图片的例子:
private LruCache<String, Bitmap> mMemoryCache;  

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。  
    // LruCache通过构造函数传入缓存值,以KB为单位。  
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
    // 使用最大可用内存值的1/8作为缓存的大小。  
    int cacheSize = maxMemory / 8;  
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
        @Override  
        protected int sizeOf(String key, Bitmap bitmap) {  
            // 重写此方法来衡量每张图片的大小,默认返回图片数量。  
            return bitmap.getByteCount() / 1024;  
        }  
    };  
}  

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
    if (getBitmapFromMemCache(key) == null) {  
        mMemoryCache.put(key, bitmap);  
    }  
}  

public Bitmap getBitmapFromMemCache(String key) {  
    return mMemoryCache.get(key);  
}

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。
public void loadBitmap(int resId, ImageView imageView) {  
    final String imageKey = String.valueOf(resId);  
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);  
    if (bitmap != null) {  
        imageView.setImageBitmap(bitmap);  
    } else {  
        imageView.setImageResource(R.drawable.image_placeholder); //加载未完成之前先用默认图片 
        BitmapWorkerTask task = new BitmapWorkerTask(imageView);  //AsyncTask
        task.execute(resId);  
    }  
}




异步任务加载完之后,将加载的图片加到缓存中以便下次使用.

通过这两种方式,可以有效避免OOM,以及提高ListView等流畅度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值