当前位置: 首页 > 科技观察

AndroidBitmapCachePool详解

时间:2023-03-11 20:40:31 科技观察

本文介绍了如何使用缓存来提高UI加载输入和滑动的流畅度。使用内存缓存、使用磁盘缓存、处理配置更改事件等方法将有效解决这个问题。在您的UI中显示单个图像非常简单,但如果您需要一次显示多张图像,则会稍微复杂一些。在很多情况下(如使用ListView、GridView或ViewPager控件),显示在屏幕上的图片和要显示在屏幕上的图片数量非常多(如浏览图库中的大量图片)。在这些控件中,当某个子控件没有被显示时,系统会重用该控件来循环它,以减少内存消耗。同时,垃圾回收机制也会释放已经加载到内存中的Bitmap资源(假设你没有强引用这些Bitmaps)。总的来说这样很好,但是当用户来回滑动屏幕时,为了保证UI的流畅性和加载图片的效率,需要避免对这些需要显示的图片进行重复处理。使用内存缓存和磁盘缓存可以解决这个问题,使用缓存可以让控件快速加载处理过的图片。本文介绍如何使用缓存来提高UI加载输入和滑动的流畅度。使用内存缓存内存缓存提高了访问图片的速度,但占用内存较大。LruCache类(API4之前可以使用SupportLibrary中的类)特别适合缓存Bitmaps,用强引用保存最近使用过的Bitmap对象(保存在LinkedHashMap中)。当缓存数量达到预定值时,将不常用的对象删除。注意:以往常见的内存缓存实现方式是使用SoftReference或者WeakReference位图缓存,但是不推荐使用这种方式。从Android2.3(APILevel9)开始,垃圾回收开始强制回收软/弱引用,导致这些缓存没有效率提升。另外,在Android3.0(APILevel11)之前,这些缓存的Bitmap数据是存储在底层内存(nativememory)中的,当满足预定条件时,这些对象不会被释放,这可能会导致程序超出内存限制和崩溃。在使用LruCache时,需要考虑以下因素来选择合适的缓存数量参数:程序中有多少可用内存,同时在屏幕上显示多少张图片?要先缓存多少张图片才能显示在屏幕上?设备的屏幕尺寸和屏幕密度是多少?非常高的屏幕密度(xhdpi,例如GalaxyNexus)设备比低屏幕密度(hdpi,例如NexusS)设备需要更多的内存来显示相同??的图像。图片的大小和格式决定了每张图片占用的内存量。质量和数量之间的平衡点是什么?在某些情况下,保存大量低质量图像并在需要时使用后台线程添加图像的高质量版本非常有用。没有一刀切的方法,您需要分析您的使用情况并指定您自己的缓存策略。使用太小的缓存将达不到应有的效果,使用太大的缓存会消耗更多内存并可能导致java.lang.OutOfMemory异常或为程序的其他功能留下很少的内存使用。下面是使用LruCache缓存的例子:privateLruCachemMemoryCache;@OverrideprotectedvoidonCreate(BundlesavedInstanceState){...//Getmemoryclassofthisdevice,exceedingthisamountwillthrowan//OutOfMemoryexception.finalintmemClass=((ServiceManager(Context.getSystem)ACTIVITY_SERVICE)).getMemoryClass();//Use1/8thoftheavailablememoryforthissmemorycache.finalintcacheSize=1024*1024*memClass/8;mMemoryCache=newLruCache(cacheSize){@OverrideprotectedintsizeOf(Stringkey,Bitmapbitmap){//缓存大小将以字节而不是项目数来衡量。returnbitmap.getByteCount();}};...}publicvoidaddBitmapToMemoryCache(Stringkey,Bitmapbitmap){if(getBitmapFromMemCache(key)==null){mMemoryCache.put(key,bitmap);}}publicBitmapgetBitmapFromMemCache(Stringkey){returnmMemoryCache.get(key);}注意:在本例中,程序内存的1/8用于缓存。在普通/hdpi设备中,这至少是4MB(32/8)内存。在分辨率为800×480的设备上,一个充满图像的全屏GridView将使用大约1.5MB(800*480*4字节)的内存,因此将近2.5页图像缓存在内存中。在ImageView中显示图片时,首先检查LruCache中是否存在。如果存在,则使用缓存的图像。如果不存在,启动后台线程加载图片并缓存:{mImageView.setImageBitmap(bitmap);}else{mImageView.setImageResource(R.drawable.image_placeholder);BitmapWorkerTasktask=newBitmapWorkerTask(mImageView);task.execute(resId);}}BitmapWorkerTask需要添加新图片到缓存中:classBitmapWorkerTaskextendsAsyncTask<整数,无效,=""位图="">{...//Decodeimageinbackground.@OverrideprotectedBitmapdoInBackground(整数...参数){finalBitmapbitmap=decodeSampledBitmapFromResource(getResources(),params[0],100,100));addBitmapToMemoryCache(String.valueOf(params[0]),bitmap);returnbitmap;}...}下一页将介绍另外两种使用磁盘缓存和处理配置更改事件的方法使用磁盘缓存访问最近使用过的图片,内存缓存很快,但你不能被su如果图片存在于缓存中。像GridView这样的控件可能有很多图像要显示,图像数据很快就会填满缓存容量。同时,你的程序可能会被其他任务打断,比如来电——当你的程序在后台运行时,系统可能会清除这些图像缓存。一旦用户继续使用您的程序,您还需要重新处理图片。在这种情况下,可以使用磁盘缓存来保存这些处理过的图片。当内存缓存中没有这些图片时,可以从磁盘缓存中加载,省去图片处理过程。当然,从磁盘加载图像比从内存读取要慢得多,磁盘图像应该在非UI线程中加载。注意:如果缓存图像经常使用,例如在图库程序中,请考虑使用ContentProvider。示例代码中有一个简单的DiskLruCache实现。然后,在Android4.0中包含了一个更可靠和推荐的DiskLruCache(libcore/luni/src/main/java/libcore/io/DiskLruCache.java)。您可以轻松地将此实现移植到4.0之前的版本(转到href="http://www.google.com/search?q=disklrucache">Google看看是否其他人已经这样做了!)。这里是一个更新版本的DiskLruCache:privateDiskLruCachemDiskCache;privatestaticfinalintDISK_CACHE_SIZE=1024*1024*10;//10MBprivatestaticfinalStringDISK_CACHE_SUBDIR="thumbnails";@OverrideprotectedvoidonCreate(BundlesavedInstanceState){...//Initializememorycache...FilecacheDir=getCacheDir(this,DISK_CACHE_SUBDIR);mDiskCache=DiskLruCache.openCache(this,cacheDir,DISK_CACHE_SIZE);...}classBitmapWorkerTaskextendsAsyncTask<整数,void,=""bitmap="">{...//Decodeimageinbackground.@OverrideprotectedBitmapdoInBackground(Integer...params){finalStringimageKey=String.valueOf(params[0]);//CheckdiskcacheinbackgroundthreadBitmapbitmap=getBitmapFromDiskCache(imageKey);if(bitmap==null){//Notfoundindiskcache//ProcessasnormalfinalBitmapbitmap=decodeSampledBitmapFromResource(getResources(),params[0],100,100));}//AddFinalbitmaptocachesaddBitmapToCache(String.valueOf(imageKey,bitmap);returnbitmap;}...}publicvoidaddBitmapToCache(Stringkey,Bitmapbitmap){//Addtomemorycacheasbeforeif(getBitmapFromMemCache(key)==null){mMemoryCache.put(key,bitmap);}//同时添加到diskcacheif(!mDiskCache.containsKey(key)){mDiskCache.put(key,bitmap);}}publicBitmapgetBitmapFromDiskCache(Stringkey){returnmDiskCache.get(key);}//Createsauniquesubdirectoryofthedesignatedappcachedirectory.Triestouseexternal//butifnotmounted,fallsbackoninternalstorage.publicstaticFilegetCacheDir(Contextcontext,StringuniqueName){//Checkifmediaismountedorstorageisbuilt-in,ifso??,tryanduseexternalcachedir//otherwiseuseinternalcachedirfinalStringcachePath=Environment.getExternalStorageState()==Environment.MEDIA_MOUNTED||!Environment.isExternalStorageRemovable()?context.getExternalCacheDir().getPath():context.getCacheDir().getPath();returnnewFile(cachePath+File.separator+uniqueName);}在UI线程Memcache中检测,在后台线程中检查磁盘缓存磁盘操作不应该在UI线程中实现。当图像被处理时,最终的结果将被添加到内存缓存和磁盘缓存中以备将来使用。处理配置更改事件运行时配置更改(例如屏幕方向更改)会导致Android销毁正在运行的Activity并使用新配置重新启动它(有关详细信息,请参阅此处的处理运行时更改)。需要注意不要在配置变化时导致所有图片都重新处理,以提高用户体验。幸运的是,您在使用内存缓存部分已经有了一个很好的图像缓存。缓存可以通过Fragment传递给新的Activity(Fragment会被setRetainInstance(true)函数保存)。当Activity重启时,Fragment重新附加到Activity上,可以通过Fragment获取缓存对象。下面是一个在Fragment中保存缓存的示例:privateLruCachemMemoryCache;@OverrideprotectedvoidonCreate(BundlesavedInstanceState){...RetainFragmentmRetainFragment=RetainFragment.findOrCreateRetainFragment(getFragmentManager());mMemoryCache=RetainFragment.mRetainedCache;if(mMemoryCache==null){mMemoryCache=newLruCache(cacheSize){...//Initializecachehereasusual}mRetainFragment.mRetainedCache=mMemoryCache;}...}classRetainFragmenttextendsFragment{privatestaticfinalStringTAG="RetainFragment";publicLruCache<字符串,bitmap="">mRetainedCache;publicRetainFragment(){}publicstaticRetainFragmentfindOrCreateRetainFragment(FragmentManagerfm){RetainFragmentfragment=(RetainFragment)fm.findFragmentByTag(TAG);if(fragment==null){fragment=newRetainFragment();}returnfragment;}@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setRetainInstance(真e);}}另外可以尝试使用和不使用Fragment来旋转设备的屏幕方向,看具体的图片加载情况