Bitmaps与优化——位图采样及OOM异常

OOM异常

本来是没有这部分内容的,但是实际上下面的几个按钮都是关于Bitmap的OOM问题的优化,所以补充了(Ctrl+V)这部分的内容。

Out Of Memory(内存溢出),我们都知道Android系统会为每个APP分配一个独立的工作空间, 或者说分配一个单独的Dalvik虚拟机,这样每个APP都可以独立运行而不相互影响!而Android对于每个 Dalvik虚拟机都会有一个最大内存限制,如果当前占用的内存加上我们申请的内存资源超过了这个限制 ,系统就会抛出OOM错误!另外,这里别和RAM混淆了,即时当前RAM中剩余的内存有1G多,但是OOM还是会发生!别把RAM(物理内存)和OOM扯到一起!另外RAM不足的话,就是杀应用了,而不是仅仅是OOM了! 而这个Dalvik中的最大内存标准,不同的机型是不一样的,可以调用:

ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
Log.e("HEHE","最大内存:" + activityManager.getMemoryClass());

获得正常的最大内存标准,又或者直接在命令行键入:

adb shell getprop | grep dalvik.vm.heapgrowthlimit

你也可以打开系统源码/system/build.prop文件,看下文件中这一部分的信息得出:

dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=192m
dalvik.vm.heapsize=512m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=2m
dalvik.vm.heapmaxfree=8m

我们关注的地方有三个:heapstartsize堆内存的初始大小,heapgrowthlimit标准的应用的最大堆 内存大小,heapsize则是设置了使用android:largeHeap的应用的最大堆内存大小!

我这里试了下手头几个机型的正常最大内存分配标准:

你也可以试试自己手头的机子~

好啦,不扯了,关于OOM问题的产生,就扯到这里,再扯就到内存管理那一块了,可是个大块头, 现在还啃不动...下面我们来看下避免Bitmap OOM的一些技巧吧!

出处:http://www.runoob.com/w3cnote/android-tutorial-bitmap2.html

 

有效的处理较大的位图

图像有各种不同的形状和大小。在许多情况下,他们往往比一个典型应用程序的用户界面(UI)所需要的资源更大。

实际上安卓提供了一种技术允许你在创建位图(和分配内存)之前去读取图像的尺寸和类型,可以解决这个问题。

读取一个位图的尺寸和类型:
为了从多种资源来创建一个位图,BitmapFactory类提供了几个解码的方法,根据你的图像数据资源选最合适的解码方法(包括但不仅仅是):

  1. decodeByteArray()
  2. decodeFile()
  3. decodeResource()

这些方法试图请求分配内存来构造位图,因此很容易导致OutOfMemory异常。

每种类型的解码方法都有额外的特征,可以让你通过BitMapFactory.Options类指定解码选项。

当解码时,想避免内存分配可以设置inJustDecodeBounds属性为true,位图对象返回null但是设置了outwidth, outHeight和outMimeType。

示例

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources0, R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

加载一个缩小版本到内存中

如果图像尺寸都是已知的,我们可以根据尺寸来决定是否应该加载完整的图片到内存或者是否用一个缩小的版本去代替加载。
可以基于以下几点考虑:

  1. 估计加载完整图像所需要的内存
  2. 加载这个图片所需空间带给你的程序的其他内存需求
  3. 准备加载图像的目标lmageView或UI组件尺寸
  4. 当前设备的屏幕尺寸和密度

例如,如果1024*768像素的图像最终被缩略地显示在一个128*96像素的lmageView中,就不值得加载到内存中去。


告诉解码器去重新采样这个图像,加载一个更小的版本到内存中,在你的BitmapFactory .Option对象中设置inSampleSize为true。

例如,将一个分辨率为2048*1536的图像用inSampleSize值为4去编码将产生一个大小为大约512*384的位图。

加载这个到内存中仅使用0.75MB,而不是完整的12MB大小的图像(假设使用ARGB.8888位图的配置)。

这里有一个方法在目标的宽度和高度的基础上来计算一个SampleSize的值:

public 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) {
        if (width > height){
            inSampleSize = Math.round(float)height/(float)reqHeight);
        }else {
            inSampleSize = Math.round((float)width / (float)requlidth);
        }
     }return inSampleSize;
  }

使用2的幂数设置inSampleSize的值可以使解码器更快,更有效。

然而,如果你想在内存或硬盘中缓存一个图片调整后的版本,通常解码到合适的图像R寸更适合来节省空间。

要使用这种方法

  • 首先解码,将inJusiPecodeBounds设置为true, 将选项传递进去
  • 然后再次解码,在使用新的inSampleSize值并将inJustDecodeBounds设置为false
public Bitmap decodeSampledBitmapFromResource(Resources res,int resid,int reqWidth,int reqHeight){
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res,resid,options);
    options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res,resid,options);
    }

 完整案例 

先修改布局文件,创建一个imageView和button(自备图片资源)

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/img1"
        android:id="@+id/imageView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:onClick="button"
        android:text="显示图片"
        app:layout_constraintTop_toBottomOf="@+id/imageView"
        />

</android.support.constraint.ConstraintLayout>

完整代码:

package com.example.a4_23bitmaps;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

    private ImageView imageView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = findViewById(R.id.imageView);
    }

    public void button(View view){
        Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),R.mipmap.timg,100,100);
        imageView.setImageBitmap(bitmap);
    }

    /**
     * 位图采样
     * @param res
     * @param resid
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampledBitmapFromResource(Resources res, int resid, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //解码
        options.inJustDecodeBounds = true;
        //只解码边界
        BitmapFactory.decodeResource(res, resid, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        //再次解码,图片加载到内存
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resid, options);
    }


    /**
     * 计算位图的采样比例
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        //获取位图的原宽高
        int w = options.outWidth;
        int h = options.outHeight;
        System.out.println("原宽:"+w);
        System.out.println("原高:"+h);
        int inSampleSize = 1;
        //原图宽高只要有一项比需求要大
        if (w > reqWidth || h > reqHeight) {
            //按小的那项缩放
            if (w > h) {
                inSampleSize = Math.round((float) h / (float) reqHeight);
            } else {
                inSampleSize = Math.round((float) w / (float) reqWidth);
            }
        }
        System.out.println("采样比例:"+inSampleSize);
        return inSampleSize;
    }

}

测试效果:

 


缓存位图

?的案例是对本地资源而言的,实际上上述问题在加载位图网络资源更为明显。而缓存就是解决上述问题的很好办法,将网络资源缓存下来,下次访问明显会迅速很多。

实际上在Volley网络编程的其中一个案例就用到了这块的知识——LruChache类

https://blog.csdn.net/nishigesb123/article/details/89356776#t14

内存缓存

内存缓冲提供了可以快速访问位图LruCache类用于缓存位图的任务,最近被引用的对象保存在一个强引用LinkedHashMap中,以及在缓存超过了其指定的大小之前释放最近很少使用的对象的内存。

LRU(Least Recently Used)—— 最近最少使用算法,学过操作系统的朋友应该都知道,就不专门叙述了。

注意:

  • 在过去,一个常用的内存缓存实现是一个SoftReference或WeakReference的位图缓存,然而现在不推荐使用。
  • android2.3 (API 级别9)开始,垃圾回收器更加注重于回收软/弱引用,这使得使用以上引用很大程度上无效。
  • android3.0 (API级别11)之前的 位图的备份很有可能会导致应用程序内存溢出和崩溃。

完整案例

在之前的代码上修改...(案例还是加载的本地资源,但是缓存的思想已经体现出来了,无论是网络还是本地资源都是一样的)

package com.example.a4_23bitmaps;

import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.LruCache;
import android.view.View;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

    private ImageView imageView;
    private LruCache<String, Bitmap> lruCache;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = findViewById(R.id.imageView);


        //先看A当前ctivity占用内存
        ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = am.getMemoryClass();
        //1/8的内存作为缓存大小 单位为字节
        final int cacheSize = memoryClass / 8 * 1024 * 1024;
        lruCache = new LruCache<>(cacheSize);
    }

    //添加缓存的方法
    public void addBitmapToCache(String key, Bitmap bitmap) {
        if (getBitmapFromCahce(key) == null) {
            lruCache.put(key, bitmap);
        }
    }

    //读取缓存的方法
    public Bitmap getBitmapFromCahce(String key) {
        return lruCache.get(key);
    }

    public void button(View view) {
        String key = String.valueOf(R.mipmap.timg);
        Bitmap bitmap = getBitmapFromCahce(key);
        if(bitmap == null){
            bitmap = decodeSampledBitmapFromResource(getResources(), R.mipmap.timg, 100, 100);
            addBitmapToCache(key,bitmap);
        }else {
            System.out.println("LruCatch中有位图");
        }
        imageView.setImageBitmap(bitmap);
    }

    /**
     * 位图采样
     *
     * @param res
     * @param resid
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampledBitmapFromResource(Resources res, int resid, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        //解码
        options.inJustDecodeBounds = true;
        //只解码边界
        BitmapFactory.decodeResource(res, resid, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        //再次解码,图片加载到内存
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resid, options);
    }


    /**
     * 计算位图的采样比例
     *
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        //获取位图的原宽高
        int w = options.outWidth;
        int h = options.outHeight;
        System.out.println("原宽:" + w);
        System.out.println("原高:" + h);
        int inSampleSize = 1;
        //原图宽高只要有一项比需求要大
        if (w > reqWidth || h > reqHeight) {
            //按小的那项缩放
            if (w > h) {
                inSampleSize = Math.round((float) h / (float) reqHeight);
            } else {
                inSampleSize = Math.round((float) w / (float) reqWidth);
            }
        }
        System.out.println("采样比例:" + inSampleSize);
        return inSampleSize;
    }

}

效果:当加载完一次之后再次点击,只会输出提示内容,并不会再次加载。

 

磁盘缓存

这部分也有更好的参考去处:

http://www.runoob.com/w3cnote/android-tutorial-exercise-3.html

使用磁盘缓存来持续处理位图,并且有助于在图片在内存缓存中不再可用时缩短加载时间。

当然,从磁盘获取图片比从内存加载更慢并且应当在后台线程中处理,因为磁盘读取的时间是不可预知的。

注意:如果它们频繁地访问,那么一个ContentProvider可能是一个更合适的地方来存储缓存中的图像,例如在一个图片库应用程序里。


DiskLruCache是一个虽然第三方解决方案,但是被官方承认且推荐使用。

使用

下载地址:https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

项目地址:https://github.com/JakeWharton/DiskLruCache

或者在Gradle添加:

compile 'com.jakewharton:disklrucache:2.0.2'

也可以参考一下:

https://www.jianshu.com/p/0c56dc217917 


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云无心鸟知还

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值