【Android Developers Training】 58. 缓存位图

系统 1650 0

注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。

原文链接: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html


向你的应用中加载一个单一的位图是很直接的行为,然而当你需要一次性加载一组图像的大集合时,事情会变得更加复杂。在很多情况下(比如对于 ListView GridView 或者 ViewPager ),屏幕上显示的图片以及会因加载动作而进入屏幕的图片,这两者的总数加起来是无法限制的。

通过对移除屏幕区域的子View进行回收,可以让这类组件内存使用降低下来。垃圾回收器也会对那些假定你将不再需要的引用对象进行回收和释放。这些措施都很好,但是为了保持流畅地和快速地加载UI,你会希望避免多次连续地处理这些图片,当它们回到屏幕区域中来时。一个存储或磁盘缓存可以在这方面提供帮助,它可以让组件迅速的重新加载处理过的图片。

这节课将会教你使用一个存储和磁盘缓存,来提升你的UI加载多个图片时的响应和流畅性。


一). 使用一个内存缓存

一个内存缓存提供了快速访问位图的方法,但它的代价是需要消耗掉珍贵的应用内存。 LruCache 类(在 Support Library 也有,可以支持到API Level 4及以上的平台)对于缓存图片来说尤其适合,它能将最近引用的对象存储在一个基于强引用的 LinkedHashMap 中,并且在缓存超出它的特定大小后,将最近最迟被引用的对象去除。

Note:

在过去,一个流行的内存缓存实现是 SoftReference 或者 WeakReference 的位图缓存,然而,这并不是推荐的实现方法。从Android 2.3(API Level 9)开始,垃圾回收器对于软引用和弱引用的回收变得更加地激进,从而使得它们的效用正在下降。从Android 3.0(API Level 11)开始,存储于本机内存的位图数据并不是以一个可预测的形式释放的,这就有潜在的可能性导致一个应用超出它的内存限制进而崩溃。

为了为一个 LruCache 选择合适的大小,一些因素需要考量,例如:

  • 你的activity或应用剩余的存储压力是如何的?
  • 同一时间有多少应用显示在屏幕上?有多少需要准备就绪显示到屏幕上?
  • 设备的屏幕的尺寸和密度的大小是多少?一个极高密度的屏幕(xhdpi)的设备(比如 Galaxy Nexus )可能相对于其他比如hdpi的设备(比如 Nexus S )需要更大的缓存来容纳同样数量的照片。
  • 位图文件的尺寸和属性是怎样的,需要消耗多少大的内存空间?
  • 图片被访问的频率高不高?有没有一些图片被访问你的频率比其它的要高?如果有,也许你会期望让这些项目一直保留在内存或者为不同被访问频率的图片设置多组 LruCache 对象。
  • 能否做到数量和质量间的平衡?有些时候存储大量低质量的图片时很有用的,而将更高质量的图片加载任务放在后台执行。

没有什么特定的大小或者公式能够适合所有的应用,你应该自己分析并决定你的用法和解决方案。一个过小的缓存会导致大量无益处的执行操作,而太大的缓存会导致 java.lang.OutOfMemory 异常,或者让你剩下的应用只有有限的存储来工作。

下面是一个 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:

在这个例子中,八分之一的应用内存被分配给了我们的缓存。在一个标准或hdpi的设备上,这大约为4MB左右(32/8)。一个全屏的 GridView ,在一个分辨率为800x480的设备上,充满图片之后,会使用掉大约1.5MB( 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
      
      
        ) {

        mImageView.setImageBitmap(bitmap);

    } 
      
      
        else
      
      
         {

        mImageView.setImageResource(R.drawable.image_placeholder);

        BitmapWorkerTask task 
      
      = 
      
        new
      
      
         BitmapWorkerTask(mImageView);

        task.execute(resId);

    }

}
      
    

BitmapWorkerTask 也需要更新,并将相应字段添加到内存缓存中:

      
        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;

    }

    ...

}
      
    

二). 使用磁盘缓存

一个内存缓存对于加速访问最近查看的位图是很有效果的,然而你不能依赖于它,因为无法做到所有图片都放置在该缓存中。如 GridView 这样的组件其较大的数据集可以迅速填充内存缓存。同时,你的应用可能会被另一个事务打断,如一个来电,此时在后台中,它可能会被杀掉,这样的话内存缓存就被销毁了。一旦这个用户恢复了,你的应用不得不重新处理这些图片。

一个磁盘缓存可以在这种情况下发挥效用,它能保持处理过的位图文件,并减少在内存缓存中不再可以获得的加载时间。当然,从磁盘获取图片比从内存获取图片要慢,由于磁盘读写的速度有很多不确定性,故应该在后台线程中执行。

Note:

一个 ContentProvider 是一个比较合适的存储缓存图片的地方,对于那些访问频率较高的图片来说,例如在图库的应用中。

下面的代码使用了 DiskLruCache 的实现,它来自于 Android source 。并且添加到内存缓存的代码中,更新其功能:

      
        private
      
      
         DiskLruCache mDiskLruCache;


      
      
        private
      
      
        final
      
       Object mDiskCacheLock = 
      
        new
      
      
         Object();


      
      
        private
      
      
        boolean
      
       mDiskCacheStarting = 
      
        true
      
      
        ;


      
      
        private
      
      
        static
      
      
        final
      
      
        int
      
       DISK_CACHE_SIZE = 1024 * 1024 * 10; 
      
        //
      
      
         10MB
      
      
        private
      
      
        static
      
      
        final
      
       String DISK_CACHE_SUBDIR = "thumbnails"
      
        ;



@Override


      
      
        protected
      
      
        void
      
      
         onCreate(Bundle savedInstanceState) {

    ...

    
      
      
        //
      
      
         Initialize memory cache
      
      
            ...

    
      
      
        //
      
      
         Initialize disk cache on background thread
      
      

    File cacheDir = getDiskCacheDir(
      
        this
      
      
        , DISK_CACHE_SUBDIR);

    
      
      
        new
      
      
         InitDiskCacheTask().execute(cacheDir);

    ...

}




      
      
        class
      
       InitDiskCacheTask 
      
        extends
      
       AsyncTask<File, Void, Void>
      
         {

    @Override

    
      
      
        protected
      
      
         Void doInBackground(File... params) {

        
      
      
        synchronized
      
      
         (mDiskCacheLock) {

            File cacheDir 
      
      = params[0
      
        ];

            mDiskLruCache 
      
      =
      
         DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);

            mDiskCacheStarting 
      
      = 
      
        false
      
      ; 
      
        //
      
      
         Finished initialization
      
      

            mDiskCacheLock.notifyAll(); 
      
        //
      
      
         Wake any waiting threads
      
      
                }

        
      
      
        return
      
      
        null
      
      
        ;

    }

}




      
      
        class
      
       BitmapWorkerTask 
      
        extends
      
       AsyncTask<Integer, Void, Bitmap>
      
         {

    ...

    
      
      
        //
      
      
         Decode image in background.
      
      
            @Override

    
      
      
        protected
      
      
         Bitmap doInBackground(Integer... params) {

        
      
      
        final
      
       String imageKey = String.valueOf(params[0
      
        ]);



        
      
      
        //
      
      
         Check disk cache in background thread
      
      

        Bitmap bitmap =
      
         getBitmapFromDiskCache(imageKey);



        
      
      
        if
      
       (bitmap == 
      
        null
      
      ) { 
      
        //
      
      
         Not found in disk cache

            
      
      
        //
      
      
         Process as normal
      
      
        final
      
       Bitmap bitmap =
      
         decodeSampledBitmapFromResource(

                    getResources(), params[
      
      0], 100, 100
      
        ));

        }



        
      
      
        //
      
      
         Add final bitmap to caches
      
      
                addBitmapToCache(imageKey, bitmap);



        
      
      
        return
      
      
         bitmap;

    }

    ...

}




      
      
        public
      
      
        void
      
      
         addBitmapToCache(String key, Bitmap bitmap) {

    
      
      
        //
      
      
         Add to memory cache as before
      
      
        if
      
       (getBitmapFromMemCache(key) == 
      
        null
      
      
        ) {

        mMemoryCache.put(key, bitmap);

    }



    
      
      
        //
      
      
         Also add to disk cache
      
      
        synchronized
      
      
         (mDiskCacheLock) {

        
      
      
        if
      
       (mDiskLruCache != 
      
        null
      
       && mDiskLruCache.get(key) == 
      
        null
      
      
        ) {

            mDiskLruCache.put(key, bitmap);

        }

    }

}




      
      
        public
      
      
         Bitmap getBitmapFromDiskCache(String key) {

    
      
      
        synchronized
      
      
         (mDiskCacheLock) {

        
      
      
        //
      
      
         Wait while disk cache is started from background thread
      
      
        while
      
      
         (mDiskCacheStarting) {

            
      
      
        try
      
      
         {

                mDiskCacheLock.wait();

            } 
      
      
        catch
      
      
         (InterruptedException e) {}

        }

        
      
      
        if
      
       (mDiskLruCache != 
      
        null
      
      
        ) {

            
      
      
        return
      
      
         mDiskLruCache.get(key);

        }

    }

    
      
      
        return
      
      
        null
      
      
        ;

}




      
      
        //
      
      
         Creates a unique subdirectory of the designated app cache directory. Tries to use external


      
      
        //
      
      
         but if not mounted, falls back on internal storage.
      
      
        public
      
      
        static
      
      
         File getDiskCacheDir(Context context, String uniqueName) {

    
      
      
        //
      
      
         Check if media is mounted or storage is built-in, if so, try and use external cache dir

    
      
      
        //
      
      
         otherwise use internal cache dir
      
      
        final
      
       String cachePath =
      
        

            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 
      
      ||

                    !isExternalStorageRemovable() ?
      
         getExternalCacheDir(context).getPath() :

                            context.getCacheDir().getPath();



    
      
      
        return
      
      
        new
      
       File(cachePath + File.separator +
      
         uniqueName);

}
      
    

Note:

因为初始化磁盘缓存也需要磁盘操作所以它也不能再主线程中执行。然而,这其实意味着缓存有可能在还未初始化的时候就被访问了。为了解决这个问题,在上面的代码实现中,一个信号量(lock)保证了应用会在初始化完成之后才去读取缓存。

虽然内存缓存在UI线程中检查,磁盘缓存是在后台线程中检查。磁盘操作不应该发生在UI线程中执行。当图片处理完成了,最后位图将会同时添加到内存和磁盘缓存中,以备将来使用。


三). 处理配置变更

运行时的配置变更,如屏幕方向变化,会导致Android销毁当前activity,并以新的配置重启activity(可以阅读: Handling Runtime Changes )。你一定希望避免重复处理图像,这样的话用户就能在配置改变时,拥有平滑快速地使用体验。

幸运的是,你在之前的章节中,已经拥有了一个很出色的图片内存缓存了。这个缓存可以通过使用一个 Fragment (该 Fragment 通过调用 setRetainInstance(true) 将其自身保留),传递给新的activity实例。在activity重新创建之后,这个保留的 Fragment 就完成了重新依附( reattach ),同时你获得了现有缓存对象的访问,允许图片快速提取并填充到 ImageView 对象中。

下面是一个使用 Fragment ,在配置变更发生时保留 LruCache 对象的例子:

      
        private
      
       LruCache<String, Bitmap>
      
         mMemoryCache;



@Override


      
      
        protected
      
      
        void
      
      
         onCreate(Bundle savedInstanceState) {

    ...

    RetainFragment retainFragment 
      
      =
      
        

            RetainFragment.findOrCreateRetainFragment(getFragmentManager());

    mMemoryCache 
      
      =
      
         retainFragment.mRetainedCache;

    
      
      
        if
      
       (mMemoryCache == 
      
        null
      
      
        ) {

        mMemoryCache 
      
      = 
      
        new
      
       LruCache<String, Bitmap>
      
        (cacheSize) {

            ... 
      
      
        //
      
      
         Initialize cache here as usual
      
      
                }

        retainFragment.mRetainedCache 
      
      =
      
         mMemoryCache;

    }

    ...

}




      
      
        class
      
       RetainFragment 
      
        extends
      
      
         Fragment {

    
      
      
        private
      
      
        static
      
      
        final
      
       String TAG = "RetainFragment"
      
        ;

    
      
      
        public
      
       LruCache<String, Bitmap>
      
         mRetainedCache;



    
      
      
        public
      
      
         RetainFragment() {}



    
      
      
        public
      
      
        static
      
      
         RetainFragment findOrCreateRetainFragment(FragmentManager fm) {

        RetainFragment fragment 
      
      =
      
         (RetainFragment) fm.findFragmentByTag(TAG);

        
      
      
        if
      
       (fragment == 
      
        null
      
      
        ) {

            fragment 
      
      = 
      
        new
      
      
         RetainFragment();

            fm.beginTransaction().add(fragment, TAG).commit();

        }

        
      
      
        return
      
      
         fragment;

    }



    @Override

    
      
      
        public
      
      
        void
      
      
         onCreate(Bundle savedInstanceState) {

        
      
      
        super
      
      
        .onCreate(savedInstanceState);

        setRetainInstance(
      
      
        true
      
      
        );

    }

}
      
    

要测试这段代码,尝试分别在保留 Fragment 和不保留 Fragment 的情况下旋转设备。你应该能注意到当保留了缓存时,图片填充到activity时几乎没有延迟。那些在内存缓存中找不到的图片一般都会在磁盘缓存中找到,如果找不到,这些图片就会像平常一样处理。

【Android Developers Training】 58. 缓存位图


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论