Bitmap分析

Published: 23 Mar 2016 Category: 技术

Bitmap加载以及缓存

官方介绍

  • Bitmap高效显示 首先要明确为何需要对加载bitmap对象特别处理?

1、移动设备的系统资源有限。Android设备对于单个程序至少需要16MB的内存。Android Compatibility Definition Document (CDD), Section 3.7. Virtual Machine Compatibility 中给出了对于不同大小与密度的屏幕的最低内存需求。 应用应该在这个最低内存限制下去优化程序的效率。当然,大多数设备的都有更高的限制需求。此外编写的应用程序都存在一个最大内存限制,程序占用过高就容易造成OOM,可以通过调用Runtime.getRuntime().maxMemory()方法来验证。

2、Bitmap会消耗很多内存,特别是对于类似照片等内容更加丰富的图片。 例如,Galaxy Nexus的照相机能够拍摄2592x1936 pixels (5 MB)的图片。 如果bitmap的图像配置是使用ARGB_8888 (从Android 2.3开始的默认配置) ,那么加载这张照片到内存大约需要19MB(2592 * 1936 * 4 bytes) 的空间,从而迅速消耗掉该应用的剩余内存空间。

3、Android应用的UI通常会在一次操作中立即加载许多张bitmaps。 例如在ListView, GridView 与 ViewPager 等控件中通常会需要一次加载许多张bitmaps,而且需要预先加载一些没有在屏幕上显示的内容,为用户滑动的显示做准备。

由于上面多种限制,所以需要采用一些处理策略对bitmap对象进行高效加载。图片有不同的大小尺寸,而且在大多数的情况下它们的实际大小要比需呈现大小大很多,一方面考虑是在有限内存下工作,另一方面加载一个超过屏幕分辨率的高分辨率照片并没有很显然的好处,反而会占用更大的内存,基于此可考虑对图片进行压缩。 BitmapFactory提供了一些解码的方法,包括(decodeByteArray(), decodeFile(), decodeResource()等),用来从不同的资源中创建一个Bitmap。那到底这几个方法有什么区别,又该如何选择呢? 我们应该根据图片的数据来源来选择合适的解码方法,需要注意的是这些方法在构造位图时会尝试分配内存,因此会容易导致OOM的异常。对此每一种解码方法都可以通过BitmapFactory.Options设置一些附加的标记,以此来指定解码选项。设置inJustDecodeBounds 属性为true可以在解码的时候避免内存的分配,它会返回一个null的Bitmap(实际是在Native层解码了图片,但是没有生成Java层的Bitmap),但是可以获取到 outWidth, outHeight 与 outMimeType。该技术可以允许你在构造Bitmap之前优先读图片的尺寸与类型。这样就可以根据情况进行压缩。具体代码如下:

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

这里的关键点在于:为了避免java.lang.OutOfMemory 的异常,我们需要在真正解析图片之前检查它的尺寸(除非你能确定这个数据源提供了准确无误的图片且不会导致占用过多的内存)。

在接下来之前,我们来看一下前面提到的关于解码方法之间的区别的问题: 这里主要分析decodeResource()和decodeFile() 的区别: decodeResource(Resources res, int id, BitmapFactory.Options opts) decodeFile(String pathName, BitmapFactory.Options opts)

这里主要分析解码后图片尺寸在处理上的区别:

decodeFile()用于读取SD卡上的图,得到的是图片的原始尺寸 decodeResource()用于读取Res、Raw等资源,得到的是图片的原始尺寸 * 缩放系数

可以看出decodeResource比decodeFile多了一个缩放系数,缩放系数的计算依赖于屏幕密度,当然该参数也是可以调整的。(附:这里的屏幕像素密度即每英寸上的像素点数,单位dpi,具体关于屏幕适配的问题可参考赵凯强的博文) 关于BitmapFactory.Options的代码如下:

// 通过BitmapFactory.Options的这几个参数可以调整缩放系数
public class BitmapFactory {
    public static class Options {
        public boolean inScaled;     // 默认true
        public int inDensity;        // 无dpi的文件夹下默认160
        public int inTargetDensity;  // 取决具体屏幕
    }
}

下面分具体情况来讨论,现在有一张720 * 720的图片: inScaled属性 如果inScaled设置为false,则不进行缩放,解码后图片大小为720x720; 否则请往下看。如果inScaled设置为true或者不设置,则根据inDensity和inTargetDensity计算缩放系数。 默认情况 把这张图片放到drawable目录下, 默认: 以720p的红米3为例子,缩放系数 = inTargetDensity(具体320 / inDensity(默认160)= 2 = density,解码后图片大小为1440x1440。 以1080p的MX4为例子,缩放系数 = inTargetDensity(具体480 / inDensity(默认160)= 3 = density, 解码后图片大小为2160x2160。 dpi文件夹的影响 把图片放到drawable或者draw这样不带dpi的文件夹,会按照上面的算法计算。如果放到xhdpi会怎样呢? 在MX4上,放到xhdpi,解码后图片大小为1080 x 1080。因为放到有dpi的文件夹,会影响到inDensity的默认值,放到xhdpi为160 x 2 = 320; 所以缩放系数 = 480(屏幕) / 320 (xhdpi) = 1.5; 所以得到的图片大小为1080 x 1080。 手动设置缩放系数 如果你不想依赖于这个系统本身的density,你可以手动设置inDensity和inTargetDensity来控制缩放系数:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = 1;
options.inDensity = 160;
options.inTargetDensity = 160;
bitmap = BitmapFactory.decodeResource(getResources(),
        R.drawable.origin, options);
// MX4上,虽然density = 3
// 但是通过设置inTargetDensity / inDensity = 160 / 160 = 1
// 解码后图片大小为720x720
System.out.println("w:" + bitmap.getWidth()
        + ", h:" + bitmap.getHeight());

对于上述问题,可参考博文 说完这个插曲,重新回到前面,在获取到图片的尺寸之后,就可以依据此来决定应该加载整个图片到内存中还是加载一个缩小的版本。需要考虑如下几个因素: 1、评估加载完整图片所需要耗费的内存 2、程序在加载这张图片时可能涉及到的其他内存需求 3、呈现这张图片的控件的尺寸大小 4、屏幕大小与当前设备的屏幕密度 比如,你的ImageView只有128 * 96像素的大小,只是为了显示一张缩略图,这时候把一张1024768像素的图片完全加载到内存中显然是不值得的。 那我们怎样才能对图片进行压缩呢?通过设置BitmapFactory.Options中inSampleSize的值就可以实现。当inSampleSize大于1时, 比如2,那么采样后的图片其宽高均为原图的1/2, 而像素数则为原来的1/4,即像素数的缩放比例为1/(inSampleSize的2次方)。比如我们有一张2048 * 1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

    public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image 源图片的高度和宽度 
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {
        //实现1
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }

        //实现2

        // 计算出实际宽高和目标宽高的比率           
        final int heightRatio = Math.round((float) height / (float) reqHeight);  
        final int widthRatio = Math.round((float) width / (float) reqWidth);  
        // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高  一定都会大于等于目标的宽和高。  
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; 
    }

    return inSampleSize;
}

Note: 设置inSampleSize为2的幂是因为解码器最终还是会对非2的幂的数进行向下处理,获取到最靠近2的幂的数。详情参考inSampleSize的文档。这个官方建议经测试并非所有的android版本都成立,因此作为建议即可。

为了使用该方法,首先需要设置 inJustDecodeBounds 为 true, 把options的值传递过来,然后设置 inSampleSize 的值并设置 inJustDecodeBounds 为 false,之后重新调用相关的解码方法。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

使用上面这个方法可以简单地加载一张任意大小的图片。如下面的代码样例显示了一个接近 100x100像素的缩略图:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

我们可以通过替换合适的BitmapFactory.decode*方法来实现一个类似的方法,从其他的数据源解析Bitmap。 可以看到前面的这种方法是用于处理未知来源图片的压缩方法,如果某个图片已经存在于内存中,我们希望对图片进行压缩后重新保存到本地,比如可以将一张图片从10M压缩到10k大小,这样就可以节省本地空间。这种方法的核心思想就是首先将图片转换成一个输出流,并记录输出流的字节数组大小,然后通过bitmap对象的compress方法,对图片做一次压缩以及格式化,并将byte数组大小与期望压缩目标大小比对,达到压缩比率,并调用bitmap的缩放方法,最后就得到最终压缩的bitmap对象。这个方法和我之前处理圆角图片所用到的缩放方法是完全一致的。具体代码如下:

/**
 * 图片压缩方法:(使用compress的方法)
 * 
 * @explain 如果bitmap本身的大小小于maxSize,则不作处理
 * @param bitmap 要压缩的图片
 * @param maxSize 压缩后的大小,单位kb
 */
public static void imageZoom(Bitmap bitmap, double maxSize) {
    // 将bitmap放至数组中,意在获得bitmap的大小(与实际读取的原文件要大)
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // 格式、质量、输出流
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] b = baos.toByteArray();
    // 将字节换成KB
    double mid = b.length / 1024;
    // 获取bitmap大小 是允许最大大小的多少倍
    double i = mid / maxSize;
    // 判断bitmap占用空间是否大于允许最大空间 如果大于则压缩 小于则不压缩
    if (i > 1) {
        // 缩放图片 此处用到平方根 将宽带和高度压缩掉对应的平方根倍
        // (保持宽高不变,缩放后也达到了最大占用空间的大小)
        bitmap = scale(bitmap, bitmap.getWidth() / Math.sqrt(i),
                bitmap.getHeight() / Math.sqrt(i));
    }
}
/***
 * 图片的缩放方法
 * 
 * @param src:源图片资源
 * @param newWidth:缩放后宽度
 * @param newHeight:缩放后高度
 */
public static Bitmap scale(Bitmap src, double newWidth, double newHeight) {
    // 记录src的宽高
    float width = src.getWidth();
    float height = src.getHeight();
    // 创建一个matrix容器
    Matrix matrix = new Matrix();
    // 计算缩放比例
    float scaleWidth = ((float) newWidth) / width;
    float scaleHeight = ((float) newHeight) / height;
    // 开始缩放
    matrix.postScale(scaleWidth, scaleHeight);
    // 创建缩放后的图片
    return Bitmap.createBitmap(src, 0, 0, (int) width, (int) height,
            matrix, true);
}
  • 后台线程处理bitmap以及并发处理 这里官方采用了AsyncTask来演示整个过程。AsyncTask 类提供了一个在后台线程执行一些操作的简单方法,它还可以把后台的执行结果呈现到UI线程中。下面是加载大图的实例代码:
class BitmapWorkerTask extends AsyncTask {
    private final WeakReference imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

仔细研究代码可以看到这里采用了一个弱引用来引用ImageView控件实例,是为了确保AsyncTask所引用的资源可以被垃圾回收掉。此外在onPostExecute中,由于当任务结束时无法确定ImageView是否还存在,所以加入了对引用的检查。ImageView对象有可能在一些情况下已经不存在了,比如任务完成之前用户按回退键退出或者屏幕旋转等。 异步加载只需要创建一个task并执行它即可。

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

注:上面对于异常情况的条件判断以及弱引用的处理值得借鉴

采用AsyncTask进行异步调用使用起来很方便,但是对于ListView中按照上面的处理存在如下这样的问题:

However, a ListView-specific behavior reveals a problem with our current implementation. Indeed, for memory efficiency reasons, ListView recycles the views that are displayed when the user scrolls. If one flings the list, a given ImageView object will be used many times. Each time it is displayed the ImageView correctly triggers an image download task, which will eventually change its image. So where is the problem? As with most parallel applications, the key issue is in the ordering. In our case, there's no guarantee that the download tasks will finish in the order in which they were started. The result is that the image finally displayed in the list may come from a previous item, which simply happened to have taken longer to download. This is not an issue if the images you download are bound once and for all to given ImageViews, but let's fix it for the common case where they are used in a list.

通常类似ListView与GridView等视图控件在使用上面演示的AsyncTask 方法时,会同时带来并发的问题。首先为了更高的效率,ListView与GridView的子Item视图会在用户滑动屏幕时被循环使用。如果每一个子视图都触发一个AsyncTask,那么就无法确保关联的视图在结束任务时,分配的视图已经进入循环队列中,给另外一个子视图进行重用。而且, 无法确保所有的异步任务的完成顺序和他们本身的启动顺序保持一致。

简单的说就是滑动图片显示错位的问题,没有对每次加载的图片进行唯一性绑定,为了解决上述问题,就需要记住下载的顺序,也即上次启动的task返回的图片就是要显示的这个。下面的方式采用了一个专用的Drawable子类在加载过程中来临时绑定ImageView。ImageView保存最近使用的AsyncTask的引用,这个引用可以在任务完成的时候再次读取检查。使用这种方式, 就可以对前面提到的AsyncTask进行扩展。 下面就是这个子类的实现:

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

从代码行super(res, bitmap)可以看到当下载正在进行时ImageView将显示一张默认图。 在执行BitmapWorkerTask 之前,你需要创建一个AsyncDrawable并且将它绑定到目标控件ImageView中:

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

在上面的代码示例中,cancelPotentialWork 方法检查是否有另一个正在执行的任务与该ImageView关联了起来,如果的确是这样,它通过执行cancel()方法来取消另一个任务。在少数情况下, 新创建的任务数据可能会与已经存在的任务相吻合,这样的话就不需要进行下一步动作了。下面是 cancelPotentialWork方法的实现 。

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData == 0 || bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

在上面的代码中有一个辅助方法:getBitmapWorkerTask(),它被用作检索AsyncTask是否已经被分配到指定的ImageView:

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

最后一步是在BitmapWorkerTask的onPostExecute() 方法里面做更新操作:

class BitmapWorkerTask extends AsyncTask {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

这个方法不仅仅适用于ListView与GridView控件,在那些需要循环利用子视图的控件中同样适用:只需要在设置图片到ImageView的地方调用 loadBitmap方法。例如,在GridView 中实现这个方法可以在 getView()中调用。

  • Bitmap缓存 1、内存缓存 内存缓存以花费宝贵的程序内存为前提来快速访问位图。LruCache类(在API Level 4的Support Library中也可以找到)特别适合用来缓存Bitmaps,它使用一个强引用(strong referenced)的LinkedHashMap保存最近引用的对象,并且在缓存超出设置大小的时候剔除(evict)最近最少使用到的对象。 > Note: 在过去,一种比较流行的内存缓存实现方法是使用软引用(SoftReference)或弱引用(WeakReference)对Bitmap进行缓存,然而我们并不推荐这样的做法。从Android 2.3 (API Level 9)开始,垃圾回收机制变得更加频繁,这使得释放软(弱)引用的频率也随之增高,导致使用引用的效率降低很多。而且在Android 3.0 (API Level 11)之前,备份的Bitmap会存放在Native Memory中,它不是以可预知的方式被释放的,这样可能导致程序超出它的内存限制而崩溃。

为了给LruCache选择一个合适的大小,需要考虑到下面一些因素:

1、应用剩下了多少可用的内存? 2、多少张图片会同时呈现到屏幕上?有多少图片需要准备好以便马上显示到屏幕? 3、设备的屏幕大小与密度是多少?一个具有特别高密度屏幕(xhdpi)的设备,像Galaxy Nexus会比Nexus S(hdpi)需要一个更大的缓存空间来缓存同样数量的图片。 Bitmap的尺寸与配置是多少,会花费多少内存? 4、图片被访问的频率如何?是其中一些比另外的访问更加频繁吗?如果是,那么我们可能希望在内存中保存那些最常访问的图片,或者根据访问频率给Bitmap分组,为不同的Bitmap组设置多个LruCache对象。 5、是否可以在缓存图片的质量与数量之间寻找平衡点?某些时候保存大量低质量的Bitmap会非常有用,加载更高质量图片的任务可以交给另外一个后台线程。 下面是一个为Bitmap建立LruCache的示例:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            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);
}

Note:在上面的例子中, 有1/8的内存空间被用作缓存。 这意味着在常见的设备上(hdpi),最少大概有4MB的缓存空间(32/8)。如果一个填满图片的GridView控件放置在800x480像素的手机屏幕上,大概会花费1.5MB的缓存空间(800x480x4 bytes),因此缓存的容量大概可以缓存2.5页的图片内容。

当加载Bitmap显示到ImageView 之前,会先从LruCache 中检查是否存在这个Bitmap。如果确实存在,它会立即被用来显示到ImageView上,如果没有找到,会触发一个后台线程去处理显示该Bitmap任务。

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

上面的程序中 BitmapWorkerTask 需要把解析好的Bitmap添加到内存缓存中:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

2、磁盘缓存 内存缓存能够提高访问最近用过的Bitmap的速度,但是我们无法保证最近访问过的Bitmap都能够保存在缓存中。像类似GridView等需要大量数据填充的控件很容易就会用尽整个内存缓存。另外,我们的应用可能会被类似打电话等行为而暂停并退到后台,因为后台应用可能会被杀死,那么内存缓存就会被销毁,里面的Bitmap也就不存在了。一旦用户恢复应用的状态,那么应用就需要重新处理那些图片。

磁盘缓存可以用来保存那些已经处理过的Bitmap,它还可以减少那些不再内存缓存中的Bitmap的加载次数。当然从磁盘读取图片会比从内存要慢,而且由于磁盘读取操作时间是不可预期的,读取操作需要在后台线程中处理。

Note:如果图片会被更频繁的访问,使用ContentProvider或许会更加合适,比如在图库应用中。

这里采用的是DiskLruCache实现的,该类并没有正式提供,可下载 这里由于篇幅原因不再详细描述了,具体实现原理可参见代码,针对此处的实例代码参见原文

Note:因为初始化磁盘缓存涉及到I/O操作,所以它不应该在主线程中进行。但是这也意味着在初始化完成之前缓存可以被访问。为了解决这个问题,在上面的实现中,有一个锁对象(lock object)来确保在磁盘缓存完成初始化之前,应用无法对它进行读取。

内存缓存的检查是可以在UI线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在UI线程中发生。当图片处理完成后,Bitmap需要添加到内存缓存与磁盘缓存中,方便之后的使用。

  • bitmap回收 这部分分析引用自杰风居的观点 就是recycle()方法:对于该方法的调用时机比较难把握,这里先简单说一下官方的建议:Android对于Bitmap内存(像素数据)的分配区域在不同版本上是有区别的 > As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

从3.0开始,Bitmap像素数据和Bitmap对象一起存放在Dalvik堆中,而在3.0之前,Bitmap像素数据存放在Native内存中。 所以,在3.0之前,Bitmap像素数据在Nativie内存的释放是不确定的,容易内存溢出而Crash,官方强烈建议调用recycle()(当然是在确定不需要的时候);而在3.0之后,则无此要求。 参考链接:Managing Bitmap Memory 一点讨论: 3.0之后官方无recycle()建议,是不是就真的不需要recycle()了呢? 在医生的这篇文章:Bitmap.recycle引发的血案 最后指出:“在不兼容Android2.3的情况下,别在使用recycle方法来管理Bitmap了,那是GC的事!”。文章开头指出了原因在于recycle()方法的注释说明:

/**
 * ... This is an advanced call, and normally need not be called,
 * since the normal GC process will free up this memory when
 * there are no more references to this bitmap.
 */
public void recycle() {}

事实上这个说法是不准确的,是不能作为recycle()方法不调用的依据的。 因为从commit history中看,这行注释早在08年初始化代码的就有了,但是早期的代码并没有因此不需要recycle()方法了。 图片 如果3.0之后真的完全不需要主动recycle(),最新的AOSP源码应该有相应体现,我查了SystemUI和Gallery2的代码,并没有取缔Bitmap的recycle()方法。 所以,我个人认为,如果Bitmap真的不用了,recycle一下又有何妨? PS:至于医生说的那个bug,显然是一种优化策略,APP开发中加个两个bitmap不相等的判断条件即可。

  • Bitmap到底占用多大内存 参考腾讯Bugly团队的分析:Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
  • inBitmap 从Android 3.0 (API Level 11)开始,引进了BitmapFactory.Options.inBitmap字段。 如果使用了这个设置字段,decode方法会在加载Bitmap数据的时候去重用已经存在的Bitmap。这意味着Bitmap的内存是被重新利用的,这样可以提升性能,并且减少了内存的分配与回收。然而,使用inBitmap有一些限制,特别是在Android 4.4 (API level 19)之前,只有同等大小的位图才可以被重用。对于Android4.4+则只要不大于重用Bitmap即可。 官方说明:

As of KITKAT, any mutable bitmap can be reused by BitmapFactory to decode any other bitmaps as long as the resulting byte count of the decoded bitmap is less than or equal to the allocated byte count of the reused bitmap. This can be because the intrinsic size is smaller, or its size post scaling (for density / sample size) is smaller. Prior to KITKAT additional constraints apply: The image being decoded (whether as a resource or as a stream) must be in jpeg or png format. Only equal sized bitmaps are supported, with inSampleSize set to 1. Additionally, the configuration of the reused bitmap will override the setting of inPreferredConfig, if set.

示例代码:

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found,
            // set it as the value of inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use
        // if the byte size of the new bitmap is smaller than
        // the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height =
            targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height
            * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions,
    // the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
        && candidate.getHeight() == targetOptions.outHeight
        && targetOptions.inSampleSize == 1;
}

参考链接: Managing Bitmap Memory Bitmap对象的复用

结语

上述对于Bitmap对象的分析参考并引用了大量的博文,但是仍然有许多内容还没有写上,对此可以参考文中相关引用链接以及后面的参考链接,此文多为引用总结,只为学习~~

参考博文

张涛-开源实验室 Android官方培训课程中文版(v0.9.5) 英文版 杰风居-Android Bitmap面面观

风之谷

comments powered by Disqus

About me

面朝大海,春暖花开。哈喽,大家好,我是Randy!