Android性能优化—Bitmap的内存管理与长图加载

1.如何计算Bitmap所占内存?
2.Bitmap在内存中的缓存管理
3.长图加载需要注意的地方

Bitmap是App中内存使用的 “大户”,如何更好的使用Bitmap,减少其对App内存的使用,是我们开发中不可回避的问题。

如何得到 bitmap 对象?

Bitmap 是 Android 系统中的图像处理中最重要类之一。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。

有两种方法可以创建 Bitmap 对象,分别是通过 Bitmap.createBitmap() 和 BitmapFactory 的 decode 系列静态方法创建 Bitmap 对象。

下面我们主要介绍 BitmapFactory 的 decode 方式创建 Bitmap 对象。

  • decodeFile() 从文件系统中加载
    • 通过 Intent 打开本地图片或照片
    • 根据 uri 获取图片的路径
    • 根据路径解析 Bitmap:Bitmap bm = BitmapFactory.decodeFile(path);
  • decodeResource() 以 R.drawable.xxx 的形式从本地资源中加载
    • Bitmap bm = BitmapFactory.decodeResource(getResources(),R.drawable.icon);
  • decodeStream() 从输入流加载
    • Bitmap bm = BitmapFactory.decodeStream(stream);
  • decodeByteArray() 从字节数组中加载
    • Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);

BitmapFactory.Options

  • inSampleSize:采样率,这是表示采样大小。用于将图片缩小加载出来的,以免占用太大内存,适合缩略图。

  • inJustDecodeBounds:当 inJustDecodeBounds 为 true 时,执行 decodexxx 方法时,BitmapFactory 只会解析图片的原始宽高信息,并不会真正的加载图片

  • inPreferredConfig:用于配置图片解码方式,对应的类型 Bitmap.Config。如果非null,则会使用它来解码图片。默认值为是 Bitmap.Config.ARGB_8888

  • inBitmap:在 Android 3.0 开始引入了 inBitmap 设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存,避免再次创建一个Bitmap。在 Android4.4,新增了允许 inBitmap 设置的图片与需要加载的图片的大小不同的情况,只要 inBitmap 的图片比当前需要加载的图片大就好了。

  • inDensity
    表示这个bitmap的的像素密度,这个值跟这张图片的放置的drawable目录有关。
    inDensity赋值:
    drawable-ldpi 120
    drawable-mdpi 160
    drawable-hdpi 240
    drawable-xhdpi 320
    drawable-xxhdpi 480

  • inTargetDensity
    表示要被画出来时的目标(屏幕)的像素密度,
    代码中获取的方式:getResources().getDisplayMetrics().densityDpi

  • inScreenDensity

    /**
     * Decode a new Bitmap from an InputStream. This InputStream was obtained from
     * resources, which we pass to be able to scale the bitmap accordingly.
     * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
     *         is {@link android.graphics.Bitmap.Config#HARDWARE}
     *         and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
     *         is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
     *         function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
     */
    @Nullable
    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
        	//value就是读取资源文件时,资源文件的一些数据信息
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
            	//inDensity赋值的是资源所在文件夹对应的密度
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
        	//inTargetDensity赋值的是手机屏幕的像素密度densityDpi
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

通过 BitmapFactory.Options 的这些参数,我们就可以按一定的采样率来加载缩小后的图片,然后在 ImageView 中使用缩小的图片这样就会降低内存占用,避免OOM,提高了 Bitamp 加载时的性能。

这其实就是我们常说的图片尺寸压缩。尺寸压缩是压缩图片的像素,一张图片所占内存大小的计算方式: 图片的像素的色彩类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。

Bitmap.Config

public static enum Config {
    ALPHA_8,//每个像素占用1byte内存
    RGB_565,//每个像素占用2byte内存
    ARGB_4444,//每个像素占用2byte内存
    ARGB_8888;//每个像素占用4byte内存;默认模式
}

Bitmap内存占用计算

Bitmap作为位图,需要读入图片在每个像素点上的数据,其主要占据内存的地方,也就是这些像素数据。一张图片像素数据的总大小为,图片的像素大小 * 每个像素点的字节大小,通常你就可以把这个值理解为Bitmap对象所占内存的大小。而图片的像素大小为横向像素值 * 纵向像素值。所以就有了下面这个公式:

Bitmap内存 ≈ 像素数据总大小 = 横向像素值 * 纵向像素值 * 每个像素的内存

没有持有Bitmap对象,计算drawable资源目录中的图片加载为Bitmap后占用多少内存

BitmapFactory.decodeResource加载图片到内存时,生成的Bitmap的宽和高并不是等于原始图片的宽和高,而是会根据当前屏幕的密度进行缩放的。

Bitmap 在内存当中占用的大小取决于:

  • 色彩格式,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
  • 原始图片文件存放的资源目录
  • 目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)
int realWidth = (int) (rawWidth * targetDensity / (float) rawDensity + 0.5f)
int realHeight = (int) (rawHeight * targetDensity / (float) rawDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

rawWidth就是资源图片的原始宽度
targetDensity就是当前屏幕的density(Android源代码中就是inTargetDensity变量)
rawDensity就是资源图片所在的资源文件夹对应的density(Android源代码中就是inDensity变量)
bytes_for_current_colorMode就是当前色彩格式下每个像素对应的字节数

比如:
ARGB_8888色彩格式:
ARGB各占8位,则每个像素占用内存(8bit + 8bit + 8bit + 8bit)/8=4Byte,所以Bitmap占用内存为 realWidth * realHeight * 4
RGB_565色彩格式:
R5位,G6位,B5位,则每个像素占用内存(5bit + 6bit + 5bit)/8=2Byte,所以Bitmap占用内存为 realWidth * realHeight * 2

对应到Android源代码中的变量:

int realWidth = (int) (rawWidth * inTargetDensity / (float) inDensity + 0.5f)
int realHeight = (int) (rawHeight * inTargetDensity / (float) inDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

我们通常的理解方式是 直接拿图片的宽乘以高,再乘以当前Bitmap格式下单个像素占用的内存大小 。 这种算法忽视了两点:
1、Android设备加载Bitmap时本身会对存放在drawable-hdpi、drawable-xhdpi、drawable-xxhdpi… 等这种目录下的图片进行缩放,所以这里需要拿图片的原始宽高进行缩放计算。

2、如果考虑到第1点,最后计算的出来bitmap占用内存大小与bitmap.getByteCount()有微小的差异。 这个差异就是因为 “(rawWidth * inTargetDensity / (float) inDensity + 0.5f)” 这样计算的结果是float类型, 而图片的像素个数必须是整数。 所以这里有一个 四舍五入的过程,误差来源于这里。

理解了上述原理后,我们可以得出以下结论:
1.在同一台设备上,图片文件存放的资源目录对应的密度(dpi)越小,则加载后的Bitmap宽和高越大,占用的内存也越大。同理,图片所在资源目录的dpi越大,生成的bitmap尺寸越小。
2.设备屏幕的像素密度越大,生成的bitmap尺寸越大
3.res/drawable目录对应的density值和res/drawable-mdpi目录一样,等于1,dpi值为160。
4.如果图片文件存放的资源目录的像素密度与设备屏幕的像素密度相同,则生成的bitmap不会缩放,尺寸是原始大小。
因此,之前的bitmap内存的计算公式可以演化成:
bitmap内存 ≈ 像素数据总大小 = 图片的像素宽 * 图片的像素高 * (设备屏幕的像素密度/图片文件存放的资源目录的像素密度)^2 * 每个像素的内存 = 图片的像素宽 * 图片的像素高 * 每个像素的内存

Bitmap的内存优化:
从上面的公式,不难看出,Bitmap的内存优化,主要有三种方式:

  • 加载Bitmap时,选择低色彩的质量参数(Bitmap.Config),如RGB_5665,这样相比默认的ARGB_8888,占用内存缩小一半。适用于对色彩多样性要求比较低的场景。
  • 将图片放在合理的资源目录下,尽可能保持和屏幕密度一致。但也不要全都放在最高密度的资源目录下,资源目录的像素密度高于屏幕密度,加载的Bitmap尺寸会小于原始尺寸,甚至小于显示区域的尺寸,这也不能满足有些需求。
  • 根据目标控件的尺寸,在加载图片时,对bitmap的尺寸进行缩放。比如在像素密度为480dpi的屏幕上,width为300dp,height为200dp的ImageView,能显示的无缩放的图片分辨率为900*600,如果图片分辨率大于这个尺寸,解析时就要考虑按比例缩小。

已有Bitmap对象,计算Bitmap所占内存大小

通过调用Bitmap的getByteCount()方法即可。

getByteCount():返回可用于存储此位图像素的最小字节数。它内部的计算方式: 每一行的字节大小 * 总行数(即高度)

    /**
     * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
     *
     * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
     * no longer be used to determine memory usage of a bitmap. See {@link
     * #getAllocationByteCount()}.</p>
     */
    public final int getByteCount() {
        if (mRecycled) {
            Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
            return 0;
        }
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

Bitmap内存压缩

不同格式的相同宽高的图片的内存占用一样吗

我们在使用图片的时候,选择 jpg、png或者webp,对内存会不会有影响呢?
只要图片的原始宽和高一样,并且放在同一个文件夹,则加载后Bitmap占用的内存是一样的。比如:R.drawable.icon_mv_jpg,R.drawable.icon_mv_png,R.drawable.icon_mv_webp三张图片的宽和高都一样,且放在同一个文件夹,因此
BitmapFactory.decodeResource加载三张图片在同一个手机屏幕上的宽和高都是一样的,而且BitmapFactory.Options配置默认的色彩质量参数都是ARGB_8888,
因此这三张图片载到内存中时占用内存的大小是一样的,虽然这三张图片占用的磁盘大小不一样。

图片压缩实现

主要用到的BitmapFactory.options的参数:
inJustDecodeBounds
为true时,decoder将返回null,但是会解析出 outxxx 字段

inPreferredConfig
设置图片解码后的像素格式,如ARGB_8888/RGB_565

inSampleSize
设置图片解码缩放比,如值为2,则加载图片的宽高是原来的 1/2,
整个图片所占内存的大小就是原图的 1/4

/**
 * 图片压缩
 */
public class ImageResize {


    /**
     * 返回压缩图片
     *
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @param hasAlpha
     * @return
     */
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha, Bitmap reusable) {

        Resources resources = context.getResources();

        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true后,再去解析,就只解析 out 参数
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(resources, id, options);

        int w = options.outWidth;
        int h = options.outHeight;


        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        options.inJustDecodeBounds = false;

        // 复用, inMutable 为true 表示易变
        options.inMutable = true;
        options.inBitmap = reusable;


        return BitmapFactory.decodeResource(resources, id, options);

    }

    /**
     * 计算 缩放系数
     *
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return
     */
    private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {

        int inSampleSize = 1;

        if (w > maxW && h > maxH) {
            inSampleSize = 2;

            while (w / inSampleSize > maxW && h / inSampleSize > maxH) {
                inSampleSize *= 2;
            }

        }

        return inSampleSize;
    }
}

Bitmap内存优化—内存复用

Bitmap自带的inBitmap内存复用机制,主要就是指的复用内存块,不需要在重新给这个bitmap申请一块新的内存,避免了一次内存的分配和回收,从而避免OOM和内存抖动,改善了运行效率。

inBitmap类似对象池的技术原理,避免内存的频繁的创建和销毁带来性能的损耗。使用inBitmap能高提升bitmap的循环使用效率。

在Google发布的第二季性能优化都有提到inBitmap技术 https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE(需翻墙)

使用inBitmap前,每创建一个bitmap需要独占一块内存
在这里插入图片描述
使用inBitmap后,多个bitmap会复用同一块内存
在这里插入图片描述
所以使用inBitmap能够大大提高内存的利用效率,但是它也有几个限制条件:
1.inBitmap只能在SDK 11(Android 3.0)以后使用。Android 2.3上,bitmap的数据是存储在native的内存区域,并不是在Dalvik的内存堆上。
2.在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。
从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
3.新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是ARGB_8888的,如果前面的bitmap是ARGB_8888,那么就不能支持ARGB_4444与RGB_565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。

这里最好的方法就是使用LRUCache来缓存bitmap,后面缓存新的bitmap时,可以从cache中按照api版本找到最适合重用的bitmap,来重用它的内存区域。

google官方的Bitmap相关教程:
http://developer.android.com/training/displaying-bitmaps/manage-memory.html
http://developer.android.com/training/displaying-bitmaps/index.html

需要内存复用的Bitmap不能调用recycle()回收内存

如下图:

在这里插入图片描述

LruCache 移除图片的时候回调 entryRemoved 方法,在这个方法中我们应该分情况处理:

  1. 能够复用的时候(oldValue.isMutable() 就是判断能不能复用),我们就通过复用池来复用
  2. 如果不能复用,我们就直接调用 recycle() 回收

而上图这样处理,就是不管什么情况,都会调用recycle()去回收我们的bitmap,释放占用的内存 ,这就导致我们后面从复用池中取出该Bitmap进行复用时发现该Bitmap的内存已经被回收了,所以报错。

所以上面应该改成下面这样:

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
				if (oldValue.isMutable()) {
				    // < 3.0  bitmap 内存在 native
				    // >= 3.0 在 java
				    // >= 8.0 又变为 native
				    // 如果从内存缓存中移除,将其放入复用池
				    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
				} else {
				    oldValue.recycle();
				}
			}

Bitmap内存缓存

使用LruCache 缓存工具类:android.util.LruCache

lru(least recently used),即最近最少使用,核心思想是:最近使用过的数据在将来被使用的概率也更高。

下图展示了依次访问ABDDBCAE数据项时内存缓存中的数据项:
在这里插入图片描述

实现方式:
1.用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。

2.利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。

3.利用链表+HashMap(即LinkedHashMap)。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

对于第一种方法,需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。
对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。
所以在一般使用第三种方式来是实现LRU算法。

android.util.LruCache源码分析

LruCache在android.util包下,顾名思义,可以翻译为最近最少使用缓存,它用强引用保存需要缓存的对象,它内部维护一个队列(实际上是LinkedHashMap内部的双向链表),当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(也就是最近最少被访问的)被丢弃,之后可以被垃圾回收。

LruCache比较重要的几个方法:

  • public final V get (K key):
    返回cache中key对应的值,调用这个方法后,被访问的值会移动到队列的尾部
  • public final V put (K key, V value):
    根据key存放value,存放的value会移动到队列的尾部
  • protected int sizeOf (K key, V value) :
    返回每个缓存对象的大小,这个用来判断缓存是否快要满了的情况,这个方法必须重写
  • protected void entryRemoved (boolean evicted, K key, V oldValue, V newValue):
    当一个缓存对象被丢弃时候调用的方法,这个是个空方法,可以重写,不是必需的。
    如果第一个参数为true:当缓存对象是为了腾出空间而被清理时(trimToSize时)。如果第一个参数为false:缓存对象的entry被remove移除或者被put覆盖时。
  • public void trimToSize (int maxSize):
    检测当前缓存队列是否已满,如果满了,移除队列头部的缓存对象

比较重要的成员变量:

  • private int size;
    size是当前所有缓存的对象的总大小。
  • private int maxSize;
    maxSize是构造方法传入的参数,指定了总缓存的大小上限。

构造方法:

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

可以看到,其实它的内部维护了一个LinkedHashMap,LinkedHashMap构造第三个参数为true,表明它内部的双向链表是根据元素被访问顺序来排序的。

put方法:


    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }
        V previous;
        synchronized (this) {
            putCount++;
            //维护size变量
            size += safeSizeOf(key, value);
           //看这个key之前有没有被使用过,如果用过则返回值不为null
            previous = map.put(key, value);
           //如果之前的缓存被现在的覆盖了,减去它原来的大小
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        //如果之前的entry被覆盖了,第一个对被覆盖的值和新值的处理交给子类覆写
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //整理空间
        trimToSize(maxSize);
        return previous;

put中用到的方法:safeSizeOf


    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

可以看到其实就是调用了sizeOf方法,而sizeOf是需要重写的,用来计算每个缓存对象的大小。

put中用到的方法:trimToSize:


public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {
            if (size < 0 || (map.isEmpty() && size != 0)) {
                throw new IllegalStateException(getClass().getName()
                        + ".sizeOf() is reporting inconsistent results!");
            }
            //空间够用时,跳出循环,否则一直移除内容
            if (size <= maxSize || map.isEmpty()) {
                break;
            }
            //移除的操作
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

        entryRemoved(true, key, value, null);
    }
}

作用是,在当前缓存大小超过或等于最大上限时,移除map中链表头部的缓存对象(对于按照元素被访问顺序来排序的LinkedHashMap,最近访问的元素被放在内部链表的尾部,最早访问的元素在链表头部)。

get方法:


    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }
 
        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            //需要获取的缓存存在,正常返回
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
 
        /*
         * Attempt to create a value. This may take a long time, and the map
         * may be different when create() returns. If a conflicting value was
         * added to the map while create() was working, we leave that value in
         * the map and release the created value.
         * 如果需要获取的缓存不存在,即 mapValue = map.get(key);为null,则调用create方法,
         * create方法需要覆写,但不是必需的。create方法存在一个执行过程,如果在这个执行过程中,
         * map发生了变化(create方法不是线程安全的,一个冲突的值被put,比如原来get("abc")==null,
         * 但是执行create的时候发生了put("abc","xxx")的操作,  则这个create的值会被移除)
         */
 
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }
 
        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);
            //产生了冲突,create的时候在相同的key上发生了put操作
            if (mapValue != null) {
                // 还原上次put的值
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }
 
        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

Bitmap磁盘缓存

使用DiskLruCache
https://github.com/JakeWharton/DiskLruCache

Bitmap长图加载

使用:android.graphics.BitmapRegionDecoder

使用方式:
InputStream is = getAssets().open(“big.png”);

// 长图使用BigmapRegionDecoder 加载
// true:输入流共享,关闭输入流就用不了了,所以这个地方用false
BigmapRegionDecoder decoder = BigmapRegionDecoder.newInstance(is,false);

// 获取指定 Rect 区域的图片,相当于从大图上截取一块下来
// options 设置为null
Bitmap bitmap = decoder.decodeRegion(Rect,null);

比如:右边的长图怎么加载到手机中
在这里插入图片描述
如果直接放,会导致里面的内容看不清:
在这里插入图片描述

正确方案:
将长图的缩放放大到和屏幕一样宽,然后长度进行等比缩放,显示长图的一块区域,然后滑动时不断移动长图的区域。
在这里插入图片描述

总结

(1) 要及时手动调用recycle()回收Bitmap的内存
(2) 正确捕获OOM异常
catch 的是 OutOfMemoryError,而不是 catch Exception。catch Exception是捕获不到OOM异常的。
(3) 使用Bitmap的内存复用机制
如果是新建Bitmap对象,然后回收旧的Bitmap对象,可能会导致新建Bitmap对象时内存不足了,从而导致OOM。可以复用内存中已经存在的Bitmap对象,而不用新建Bitmap对象,从而避免OOM。
(4)Bitmap图片压缩
根据据视图控件的大小以及图片的大小计算适当的采样率,加载图片时不用把整张图片加载到内存中,而是根据采样率加载压缩后的数据到内存中。

参考:
【Android 内存优化】Bitmap 内存占用计算 ( Bitmap 图片内存占用分析 | Bitmap 内存占用计算 | Bitmap 不同像素密度间的转换 )
Android性能优化:Bitmap详解&你的Bitmap占多大内存?
图片加载和Bitmap的内存优化
Bitmap占用内存大小的准确计算公式
[Android] Android开发优化之——对Bitmap的内存优化
Bitmaps in Android

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值