Android 从具体实例分析Bit地图使用时候内存注意点

Android 从具体实例分析Bitmap使用时候内存注意点

根据Android官方提供给我们的Sample例子实实在在的分析Bitmap使用时候的注意点。

在分析Bitmap的使用之前先简单的了解下BitmapFactory 类,BitmapFactory类可以根据各种不同的数据来源(文件,流,字节数组等)来构建Bitmap位图对象,BitmapFactory有一个内部类BitmapFactory.Options,大概看下BitmapFactory.Options里面各个参数的作用(不一定全部对哦)
1). inBitmap: 如果设置,在加载Bitmap的时候会尝试去重用这块内存(内存复用),不能重用的时候会返回null,否则返回bitmap。
2). inDensity: 原始图片的Bitmap的像素密度
3). inDiter: 是否采用抖动解码(举个例子来理解抖动解码,如果一张颜色很丰富的图,用一个位数比较低的颜色模式来解码的话,那么一个直观的感觉就是颜色不够用,那么这张图解出来之后,在一些颜色渐变的区域上就会有一些很明显的断裂色带,如果采用抖动解码,那么就会在这些色带上采用随机噪声色来填充,目的是让这张图显示效果更好,色带不那么明显)
4). inInputShareable: 与inPurgeable一起使用,如果inPurgeable为false那该设置将被忽略,如果为true,那么它可以决定位图是否能够共享一个指向数据源的引用,或者是进行一份拷贝
5). inJustDecodeBounds: 当为true的时候bitmap返回null但是其他的一些option信息还是会返回的。(比如有些情况下我们只是想要获取图片的宽度和高度就可以把这个参数设置为true)。
6). inMutable: 如果为true,解码方法将始终返回一个可变的位图
7). inPreferQualityOverSpeed: 如果设置为true,解码器将尝试重建图像以获得更高质量的解码,甚至牺牲解码速度。现在只是对JPEG有用。
8). inPreferredConfig: 如果不为空,解码器将尝试解码成这个内部配置,如果为空将尝试挑选最好的匹配配置基于系统的屏幕。
9). inPremultiplied: 默认true,如果设置为true返回的bitmap有alpha的颜色通道。 一般不会去设置这个值直接用默认的。
10). inPurgeable: 如果设置为true,则由此产生的位图将分配其像素,以便系统需要回收内存时可以将它们清除。
11). inSampleSize: 如果设置的值大于1,解码器将等比缩放图像以节约内存。
12). inScaled: 如果设置true,当inDensity和inTargetDensity不为0,加载时该位图将被缩放,以匹配inTargetDensity,而不是依靠图形系统缩放每次将它绘制到画布上。
13). inScreenDensity: 当前正在使用的实际屏幕的像素密度。
14). inTargetDensity: 这个位图将被画到的目标的像素密度。
15). inTempStorage: 解码的时候临时存储用的 建议设置16K。
16). mCancel: 用于指示已经调用了这个对象的取消方法的标志。
17). outHeight: 图像的高度。
18). outMimeType: 如果知道,这个字符串将被设置为解码图像的MIME类型。
19). outWidth: 图像的宽度。

在分析Bitmap的使用之前还得知道,在Android3.0之前,Bitmap的内存分配分为两部分,一部分是分配在Dalvik的VM堆中,而像素数据的内存是分配在Native堆中,而到了Android3.0之后,Bitmap的内存则已经全部分配在VM堆上,这两种分配方式的区别在于,Native堆的内存不受Dalvik虚拟机的管理,必须手动调用Recycle方法释放Bitmap的内存,而到了Android 3.0之后,我们就可以将Bitmap的内存完全放心的交给虚拟机管理了,我们只需要保证Bitmap对象遵守虚拟机的GC Root Tracing的回收规则即可。

Bitmap使用的时候主要从下面几个方面优化

  1. 缓存:内存缓存+文件缓存 这个应该不能算是内存方面的优化,应该算性能上面的优化。
  2. 及时释放Bitmap的内存:正如上面说到的Android 3.0之前有部分内存是分配在native上的,必须手动去释放。
  3. 复用内存:BitmapFactory.Options 参数inBitmap的使用。inMutable设置为true,并且配合SoftReference软引用使用(内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些软引用对象的内存)。有一点要注意Android4.4以下的平台,需要保证inBitmap和即将要得到decode的Bitmap的尺寸规格一致,Android4.4及其以上的平台,只需要满足inBitmap的尺寸大于要decode得到的Bitmap的尺寸规格即可。
  4. 降低采样率,BitmapFactory.Options 参数inSampleSize的使用,从而减少内存的使用。

接下来就该进入正题了

对应上面四点Bitmap使用的优化点,下面结合Android官方提供给我们的Sample例子来简单的看看具体是怎么做的,对应的Sample例子在android-sdk目录/sample/android版本/ui/DisplayingBitmaps。上面的每个优化点在这个例子里面都有体现,在分析的时候会一一的指出来,给我们以后使用Bitmap的时候提供一个参照的作用。(例子下载地址)

在分析这个实例之前先对几个类做大概的了解,方便后续代码分析。
实例简单类图如下

Android 从具体实例分析Bit地图使用时候内存注意点

ImageWorker类:加载图片,从内存直接加载或异步加载(从磁盘缓存或processBitmap(这个要看ImageFetcher类中processBitmap()函数的的具体实现))。
1). ImageCache类: 缓存的具体实现类。ImageCache里面用到了两种缓存,内存缓存LruCache,文件缓存DiskLruCache,在对图片进行缓存的时候内存和文件都会缓存的,但是内存缓存的优先级要高于文件缓存的优先级,先读内存缓存然后在读文件缓存。
2). BitmapWorkerTask类: 异步加载(读文件缓存或者调用processBitmap()函数, 因为这两种读取Bitmap信息都是要耗费时间)的具体实现。里面会先去文件缓存里面读Bitmap,如果没读到就调用ImageFetcher类的processBitmap()函数去加载Bitmap,然后又根据需要又把Bitmap加入到缓存当中去。
3). ImageResizer类: 根据给定的大小对Image做调整,在降低采样率的时候用到。会根据原图的大小和要显示的图片的大小按照一定的算法计算出inSampleSize的值(关于inSampleSize的计算我们以后是可以直接搬去用的)。
4). ImageFetcher类: 从网络下载图片,processBitmap在ImageWorker中是一个抽象方法,并没有实现体。为什么这么做呢,因为图片的来源不确定可能是网络也可能是本地数据库。所以ImageWorker中processBitmap直接用了抽象的方法,让他的子类根据需求做不同的实现。在这里做的是去网络上面读取图片。

只是分析大概的过程哦,方便以后使用Bitmap的时候更加容易入手点,还是按照具体的使用流程来直接跳到调用的地方。
ImageGridFragment类中内部类ImageAdapter的getView()函数中mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView)。两个参数第一个参数url,第二个参数ImageView。 直接跟进去。
ImageWorker类的loadImage()函数

    public void loadImage(Object data, ImageView imageView) {
        if (data == null) {
            return;
        }

        BitmapDrawable value = null;

        if (mImageCache != null) {
            value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
        }

        if (value != null) {
            // Bitmap found in memory cache
            imageView.setImageDrawable(value);
        } else if (cancelPotentialWork(data, imageView)) {

            final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(mResources, mLoadingBitmap, task);
            imageView.setImageDrawable(asyncDrawable);

            // NOTE: This uses a custom version of AsyncTask that has been pulled from the
            // framework and slightly modified. Refer to the docs at the top of the class
            // for more info on what was changed.
            task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);

        }
    }

8-10行,从内存缓存中去读图片资源。
12-15行,内存缓存读到了,直接设置给ImageView。
16-27行,内存缓存里面没有图片资源,先调用cancelPotentialWork()判断当前ImageView对应的BitmapWorkerTask是否还在异步请求当前url对应的图像资源,如果是则返回true不用再重新请求了,不是则返回false。返回false的时候我们就要新建一个BitmapWorkerTask去异步请求url对应的图像资源了(可能是文件缓存也可能从网络上读在接下来的分析中会体现出来)。然后通过AsyncDrawable把BitmapWorkerTask和ImageView关联起来(注意AsyncDrawable是继承BitmapDrawable的,所以是可以setImageDrawble的,正好在cancelPotentialWork()函数里面会get出来去判断)。
接下来就是BitmapWorkerTask类的具体实现了,重头戏了(注意 通过上面内存缓存已经读过了哦,在内存缓存里面没读到才会进入BitmapWorkerTask异步类哦)。

那就该去看BitmapWorkerTask类的doInBackground()函数了。(异步,负责读文件缓存,从网络上读图片信息)。

        @Override
        protected BitmapDrawable doInBackground(Void... params) {

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doInBackground - starting work");
            }

            final String dataString = String.valueOf(mData);
            Bitmap bitmap = null;
            BitmapDrawable drawable = null;

            // Wait here if work is paused and the task is not cancelled
            synchronized (mPauseWorkLock) {
                while (mPauseWork && !isCancelled()) {
                    try {
                        mPauseWorkLock.wait();
                    } catch (InterruptedException e) {}
                }
            }

            // If the image cache is available and this task has not been cancelled by another
            // thread and the ImageView that was originally bound to this task is still bound back
            // to this task and our "exit early" flag is not set then try and fetch the bitmap from
            // the cache
            if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
                    && !mExitTasksEarly) {
                bitmap = mImageCache.getBitmapFromDiskCache(dataString);
            }

            // If the bitmap was not found in the cache and this task has not been cancelled by
            // another thread and the ImageView that was originally bound to this task is still
            // bound back to this task and our "exit early" flag is not set, then call the main
            // process method (as implemented by a subclass)
            if (bitmap == null && !isCancelled() && getAttachedImageView() != null
                    && !mExitTasksEarly) {
                bitmap = processBitmap(mData);
            }

            // If the bitmap was processed and the image cache is available, then add the processed
            // bitmap to the cache for future use. Note we don't check if the task was cancelled
            // here, if it was, and the thread is still running, we may as well add the processed
            // bitmap to our cache as it might be used again in the future
            if (bitmap != null) {
                if (Utils.hasHoneycomb()) {
                    // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable
                    drawable = new BitmapDrawable(mResources, bitmap);
                } else {
                    // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable
                    // which will recycle automagically
                    drawable = new RecyclingBitmapDrawable(mResources, bitmap);
                }

                if (mImageCache != null) {
                    mImageCache.addBitmapToCache(dataString, drawable);
                }
            }

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doInBackground - finished work");
            }

            return drawable;

        }

25-28行,从文件缓存去读图片资源
34-37行,文件缓存没有读到,调用processBitmap()函数从网络上面读图片资源,processBitmap()函数的具体实现在ImageFetcher类中。这个我们等下再看。
43-56行,拿到了图片资源,44-47行,Android3.0之后的设备上面说过3.0之后的是itmap的内存则已经全部分配在VM堆上,不需要我们手动去释放,这部分还是按照我们正常的使用流程。47-51行,Android3.0之前的设备Bitmap有一部分内存是分配在Native堆中需要手动去释放,在这里对应Android 3.0之前的设备用了RecyclingBitmapDrawable,RecyclingBitmapDrawable继承BitmapDrawable并且里面用了两个计数器mDisplayRefCount,mCacheRefCount。mDisplayRefCount当ImageView显示的时候加一如果显示别的时候先把之前的减一,mCacheRefCount缓存的时候加一从缓存里面移除的时候减一。这样当这个RecyclingBitmapDrawable的mDisplayRefCount和mCacheRefCount都是0的时候说明这个资源不需要使用了调用getBitmap().recycle();了。

接着该看下上面说到的ImageFetcher类processBitmap()函数。

    private Bitmap processBitmap(String data) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "processBitmap - " + data);
        }

        final String key = ImageCache.hashKeyForDisk(data);
        FileDescriptor fileDescriptor = null;
        FileInputStream fileInputStream = null;
        DiskLruCache.Snapshot snapshot;
        synchronized (mHttpDiskCacheLock) {
            // Wait for disk cache to initialize
            while (mHttpDiskCacheStarting) {
                try {
                    mHttpDiskCacheLock.wait();
                } catch (InterruptedException e) {}
            }

            if (mHttpDiskCache != null) {
                try {
                    snapshot = mHttpDiskCache.get(key);
                    if (snapshot == null) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "processBitmap, not found in http cache, downloading...");
                        }
                        DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
                        if (editor != null) {
                            if (downloadUrlToStream(data,
                                    editor.newOutputStream(DISK_CACHE_INDEX))) {
                                editor.commit();
                            } else {
                                editor.abort();
                            }
                        }
                        snapshot = mHttpDiskCache.get(key);
                    }
                    if (snapshot != null) {
                        fileInputStream =
                                (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                        fileDescriptor = fileInputStream.getFD();
                    }
                } catch (IOException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } catch (IllegalStateException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } finally {
                    if (fileDescriptor == null && fileInputStream != null) {
                        try {
                            fileInputStream.close();
                        } catch (IOException e) {}
                    }
                }
            }
        }

        Bitmap bitmap = null;
        if (fileDescriptor != null) {
            bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,
                    mImageHeight, getImageCache());
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {}
        }
        return bitmap;
    }

10-53行,通过mHttpDiskCache把从网络上面(downloadUrlToStream)读取到的图片资源缓存到文件里面去,同时拿到了缓存文件的描述符fileDescriptor。(这个时候还不会有内存问题,因为网络上面拿到的图片资源直接放到文件里面去了)
56-59行,bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,mImageHeight, getImageCache()); 准备从文件里面去读图片信息了,这个时候就要读到内存里面来了,在这个函数里面就会对内存的优化做处理了。
decodeSampledBitmapFromDescriptor()函数四个参数,文件描述符,要显示的宽度,要显示的高度,缓存类对象(肯定是在图片显示完之后要做缓存)。

    public static Bitmap decodeSampledBitmapFromDescriptor(
            FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);

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

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;

        // If we're running on Honeycomb or newer, try to use inBitmap
        if (Utils.hasHoneycomb()) {
            addInBitmapOptions(options, cache);
        }

        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

6行,options.inJustDecodeBounds 设置只是先只是去解析图片的宽度和高度信息,不去加载具体的图片资源。
10行,根据calculateInSampleSize()函数计算出合适的inSampleSize值。具体是怎么计算的可以直接看下calculateInSampleSize()函数的具体实现,反正这个函数如果以后我们要用是可以直接搬来用的。通过这个函数就降低了图片的采样率等下读图片资源的时候大大的减少了内存的消耗了。
16-18行,android 3.0以上的版本。做内存复用处理。那就的看下addInBitmapOptions()函数了

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    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 and find a bitmap to use for inBitmap
            Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

            if (inBitmap != null) {
                options.inBitmap = inBitmap;
            }
        }

    }

6行 options.inMutable设置为true。
10行 去cache里面找是否有适合的Bitmap给复用。如果有直接赋值给options.inBitmap。 这下就得去看下怎么去找复用的Bitmap了。

    protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {

        Bitmap bitmap = null;

        if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
            synchronized (mReusableBitmaps) {
                final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
                Bitmap item;

                while (iterator.hasNext()) {
                    item = iterator.next().get();

                    if (null != item && item.isMutable()) {
                        // Check to see it the item can be used for inBitmap
                        if (canUseForInBitmap(item, options)) {
                            bitmap = item;

                            // Remove from reusable set so it can't be used again
                            iterator.remove();
                            break;
                        }
                    } else {
                        // Remove from the set if the reference has been cleared.
                        iterator.remove();
                    }
                }
            }
        }

        return bitmap;

    }

遍历mReusableBitmaps(Bitmap的软引用Set),通过canUseForInBitmap()函数去判断是否可以复用 判断的规则是如果Android 小于4.4的要保证宽度和高度都要相同并且inSampleSize为1,高于4.4的版本只要要显示的图片占的内存比复用的那个Bitmap的内存小就可以。
到这里我们清楚了复用条件的判断,但是我们不知道mReusableBitmaps这个set是什么时候在哪里把可以复用的Bitmap加入进去的,直接看ImageCache的init()函数。mMemoryCache初始化部分

            mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

                /**
                 * Notify the removed entry that is no longer being cached
                 */
                @Override
                protected void entryRemoved(boolean evicted, String key,
                        BitmapDrawable oldValue, BitmapDrawable newValue) {
                    if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
                        // The removed entry is a recycling drawable, so notify it
                        // that it has been removed from the memory cache
                        ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
                    } else {
                        // The removed entry is a standard BitmapDrawable

                        if (Utils.hasHoneycomb()) {
                            // We're running on Honeycomb or later, so add the bitmap
                            // to a SoftReference set for possible use with inBitmap later
                            mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
                        }
                    }
                }

看到了吧,在这里加入进来的,看到是当一个Bitmap从内存缓存中拿掉的时候,这个Bitmap会加入到mReusableBitmaps中去,这里我们知道某个Bitmap从内存缓存中拿掉的时候这个Bitmap对应的内存不一定会这么快释放掉的,我们还是可以用的所以这里用了软应用。

到此整个流程非常简单的分析完了,但是感觉还是很乱。接下来做一个总结,总结下上面提到的Bitmap的优化处理的几点载实例中是怎么做的。
1. 缓存
实例中采用的方法,ImageCache使用了内存缓存,文件缓存双缓存机制。
2. 及时释放Bitmap的内存(针对Android 3.0之前的设备)
实例中借助RecyclingBitmapDrawable类和RecyclingImageView类的配合使用实现Bitmap内存的及时释放

RecyclingBitmapDrawable继承BitmapDrawable同时里面有两个计数器mDisplayRefCount,mCacheRefCount。mDisplayRefCount当RecyclingBitmapDrawable要显示的时候加一换别的显示的时候减一,mCacheRefCount缓存的时候加一从缓存里面移除的时候减一,这样当这个RecyclingBitmapDrawable的mDisplayRefCount和mCacheRefCount都是0的时候说明这个资源不需要使用了调用getBitmap().recycle()释放掉。

RecyclingImageView继承ImageView, 当RecyclingImageView setImageDrawable的时候先通知之前的Drawble不显示了同时通知当前的Drawable我要显示了这里就要看你Drawable对应的是不是RecyclingBitmapDrawable了。
3. 复用内存
BitmapFactory.Options 参数inBitmap的使用。inMutable设置为true,并且配合SoftReference软引用使用(内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存)。当一个Bitmap从内存缓存中移除掉的时候,把这个Bitmap加入到复用的Set集合里面去。判断是否有Bitmap可以复用的时候先去这个集合里面拿到Bitmap,然后按照复用图片的规则(Android4.4以下的平台,需要保证inBitmap和即将要得到decode的Bitmap的尺寸规格一致,Android4.4及其以上的平台,只需要满足inBitmap的尺寸大于要decode得到的Bitmap的尺寸规格即可)判断是否可以复用。
4. 降低采样率
BitmapFactory.Options 参数inSampleSize的使用,先把options.inJustDecodeBounds设为true,只是去读取图片的大小,在拿到图片的大小之后和要显示的大小做比较通过calculateInSampleSize()函数计算出inSampleSize的具体值,得到值之后。options.inJustDecodeBounds设为false读图片资源。