第三课:缓存Bitmap

2024-06-18 02:08
文章标签 第三课 bitmap 缓存

本文主要是介绍第三课:缓存Bitmap,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文地址 http://my.oschina.net/ryanhoo/blog/88443

声明:Ryan的博客文章欢迎您的转载,但在转载的同时,请注明文章的来源出处,不胜感激! :-) 

http://my.oschina.net/ryanhoo/blog/88443

译者:Ryan Hoo

来源:https://developer.android.com/develop/index.html

译者按: 在Google最新的文档中,提供了一系列含金量相当高的教程。因为种种原因而鲜为人知,真是可惜!Ryan将会细心整理,将之翻译成中文,希望对开发者有所帮助。

        本系列是Google关于展示大Bitmap(位图)的官方演示,可以有效的解决内存限制,更加有效的加载并显示图片,同时避免让人头疼的OOM(Out Of Memory)。

-------------------------------------------------------------------------------------

译文:

        加载一个Bitmap(位图)到你的UI界面是非常简单的,但是如果你要一次加载一大批,事情就变得复杂多了。在大多数的情况下(如ListView、GridView或者ViewPager这样的组件),屏幕上的图片以及马上要在滚动到屏幕上显示的图片的总量,在本质上是不受限制的。

        像这样的组件在子视图移出屏幕后会进行视图回收,内存使用仍被保留。但假设你不保留任何长期存活的引用,垃圾回收器也会释放你所加载的Bitmap。这自然再好不过了,但是为了保持流畅且快速加载的UI,你要避免继续在图片回到屏幕上的时候重新处理。使用内存和硬盘缓存通常能解决这个问题,使用缓存允许组件快速加载并处理图片。

        这节课将带你使用内存和硬盘缓存Bitmap,以在加载多个Bitmap的时候提升UI的响应性和流畅性。

使用内存缓存

        以牺牲宝贵的应用内存为代价,内存缓存提供了快速的Bitmap访问方式。LruCache类(可以在Support Library中获取并支持到API  Level 4以上,即1.6版本以上)是非常适合用作缓存Bitmap任务的,它将最近被引用到的对象存储在一个强引用的LinkedHashMap中,并且在缓存超过了指定大小之后将最近不常使用的对象释放掉。

        注意:以前有一个非常流行的内存缓存实现是SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,然而现在已经不推荐使用了。自Android2.3版本(API Level 9)开始,垃圾回收器更着重于对软/弱引用的回收,这使得上述的方案相当无效。此外,Android 3.0(API Level 11)之前的版本中,Bitmap的备份数据直接存储在本地内存中并以一种不可预测的方式从内存中释放,很可能短暂性的引起程序超出内存限制而崩溃。

        为了给LruCache选择一个合适的大小,要考虑到很多原因,例如:

  • 其他的Activity(活动)和(或)程序都是很耗费内存的吗?
  • 屏幕上一次会显示多少图片?有多少图片将在屏幕上显示?
  • 设备的屏幕大小和密度是多少?一个超高清屏幕(xhdpi)的设备如Galaxy Nexus,相比Nexus S(hdpi)来说,缓存同样数量的图片需要更大的缓存空间。
  • Bitmap的尺寸、配置以及每张图片需要占用多少内存?
  • 图片的访问是否频繁?有些会比其他的更加被频繁的访问到吗?如果是这样,也许你需要将某些图片一直保留在内存中,甚至需要多个LruCache对象分配给不同组的Bitmap。
  • 你能平衡图片的质量和数量么?有的时候存储大量低质量的图片更加有用,然后可以在后台任务中加载另一个高质量版本的图片。

        对于设置缓存大小,并没有适用于所有应用的规范,它取决于你在内存使用分析后给出的合适的解决方案。缓存空间太小并无益处,反而会引起额外的开销,而太大了又可能再次引起java.lang.OutOfMemory异常或只留下很小的空间给应用的其他程序运行。   

        这里有一个设置Bitmap的LruCache示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
     ...
     // Get memory class of this device, exceeding this amount will throw an
     // OutOfMemory exception.
     final int memClass = ((ActivityManager) context.getSystemService(
             Context.ACTIVITY_SERVICE)).getMemoryClass();
     // Use 1/8th of the available memory for this memory cache.
     final int cacheSize = 1024 * 1024 * memClass / 8 ;
     mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
         @Override
         protected int sizeOf(String key, Bitmap bitmap) {
             // The cache size will be measured in bytes rather than number of items.
             return bitmap.getByteCount();
         }
     };
     ...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
     if (getBitmapFromMemCache(key) == null ) {
         mMemoryCache.put(key, bitmap);
     }
}
public Bitmap getBitmapFromMemCache(String key) {
     return mMemoryCache.get(key);
}

        注意:在这个例子中,1/8的应用内存被分配给缓存。在一个普通的/hdpi设备上最低也在4M左右(32/8)。一个分辨率为800*480的设备上,全屏的填满图片的GridView占用的内存约1.5M(800*480*4字节),因此这个大小的内存可以缓存2.5页左右的图片。

        当加载一个Bitmap到ImageView中,先要检查LruCache。如果有相应的数据,则立即用来更新ImageView,否则将启动后台线程来处理这个图片。

?
1
2
3
4
5
6
7
8
9
10
11
12
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也需要更新内存中的数据:

?
1
2
3
4
5
6
7
8
9
10
11
12
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;
     }
     ...
}

使用硬盘缓存

        一个内存缓存对加速访问最近浏览过的Bitmap非常有帮助,但是你不能局限于内存中的可用图片。GridView这样有着更大的数据集的组件可以很轻易消耗掉内存缓存。你的应用有可能在执行其他任务(如打电话)的时候被打断,并且在后台的任务有可能被杀死或者缓存被释放。一旦用户重新聚焦(resume)到你的应用,你得再次处理每一张图片。

        在这种情况下,硬盘缓存可以用来存储Bitmap并在图片被内存缓存释放后减小图片加载的时间(次数)。当然,从硬盘加载图片比内存要慢,并且应该在后台线程进行,因为硬盘读取的时间是不可预知的。

        注意:如果访问图片的次数非常频繁,那么ContentProvider可能更适合用来存储缓存图片,例如Image Gallery这样的应用程序。

        这个类中的示例代码使用DiskLruCache(来自Android源码)实现。在示例代码中,除了已有的内存缓存,还添加了硬盘缓存。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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);
}

        注意:即便是硬盘缓存初始化也需要硬盘操作,因此不应该在主线程执行。但是,这意味着硬盘缓存在初始化前就能被访问到。为了解决这个问题,在上面的实现中添加了一个锁对象(lock object),以确保在缓存被初始化之前应用无法访问硬盘缓存。

        在UI线程中检查内存缓存,相应的硬盘缓存检查应在后台线程中进行。硬盘操作永远不要在UI线程中发生。当图片处理完成后,最终的Bitmap要被添加到内存缓存和硬盘缓存中,以便后续的使用。

 处理配置更改

        运行时的配置会发生变化,例如屏幕方向的改变,会导致Android销毁并以新的配置重新启动Activity(关于此问题的更多信息,请参阅Handling Runtime Changes)。为了让用户有着流畅而快速的体验,你需要在配置发生改变的时候避免再次处理所有的图片。

        幸运的是,你在“使用内存缓存”一节中为Bitmap构造了很好的内存缓存。这些内存可以通过使用Fragment传递到信的Activity(活动)实例,这个Fragment可以调用setRetainInstance(true)方法保留下来。在Activity(活动)被重新创建后,你可以在上面的Fragment中访问到已经存在的缓存对象,使得图片能快加载并重新填充到ImageView对象中。

        下面是一个使用FragmentLruCache对象保留在配置更改中的示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
     ...
     RetainFragment mRetainFragment =
             RetainFragment.findOrCreateRetainFragment(getFragmentManager());
     mMemoryCache = RetainFragment.mRetainedCache;
     if (mMemoryCache == null ) {
         mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
             ... // Initialize cache here as usual
         }
         mRetainFragment.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();
         }
         return fragment;
     }
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setRetainInstance( true );
     }
}
        为了测试这个,可以在不适用Fragment的情况下旋转设备屏幕。在保留缓存的情况下,你应该能发现填充图片到Activity中几乎是瞬间从内存中取出而没有任何延迟的感觉。任何图片优先从内存缓存获取,没有的话再到硬盘缓存中找,如果都没有,那就以普通方式加载图片。  

这篇关于第三课:缓存Bitmap的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1071010

相关文章

设置Nginx缓存策略

详细信息 Nginx服务器的缓存策略设置方法有两种:add_header或者expires。 1. add_header 1)语法:add_header name value。 2)默认值:none。 3)使用范围:http、server、location。 配置示例如下: add_header cache-control "max-age=86400";#设置缓存时间为1天。add

【MyBatis学习7】MyBatis中的一级缓存

缓存的作用是减轻数据库的压力,提高数据库的性能的。mybatis中提供了一级缓存和二级缓存,先来看一下两个缓存的示意图:    从图中可以看出: 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。二级缓存是mappe

吴恩达机器学习 第三课 week2 推荐算法(上)

目录 01 学习目标 02 推荐算法 2.1 定义       2.2 应用 2.3 算法 03 协同过滤推荐算法 04 电影推荐系统 4.1 问题描述 4.2 算法实现 05 总结 01 学习目标      (1)了解推荐算法      (2)掌握协同过滤推荐算法(Collaborative Filtering Recommender Algorithm)原理

java NIO 缓存区之内核空间、用户空间和虚拟地址

IO是基于缓存区来做的,所谓的输入和输出就是从缓存区中移入和移出数据。以IO输入为例,首先是用户空间进程向内核请求某个磁盘空间数据,然后内核将磁盘数据读取到内核空间的buffer中,然后用户空间的进程再将内核空间buffer中的数据读取到自身的buffer中,然后进程就可以访问使用这些数据。     内核空间是指操作系统内核运行的空间,是为了保证操作系统内核的能够安全稳定地运行而为内核专

Android性能优化系列之Bitmap图片优化

在Android开发过程中,Bitmap往往会给开发者带来一些困扰,因为对Bitmap操作不慎,就容易造成OOM(Java.lang.OutofMemoryError - 内存溢出),本篇博客,我们将一起探讨Bitmap的性能优化。 为什么Bitmap会导致OOM? 1.每个机型在编译ROM时都设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,用来限定每个应用可

黑马苍穹外卖6 清理redis缓存+Spring Cache+购物车的增删改查

缓存菜品 后端服务都去查询数据库,对数据库访问压力增大。 解决方式:使用redis来缓存菜品,用内存比磁盘性能更高。 key :dish_分类id String key= “dish_” + categoryId; @RestController("userDishController")@RequestMapping("/user/dish")@Slf4j@Api(tags = "

Linux - 利用/proc/sys/vm/drop_caches实现手工清理系统缓存

文章目录 现象`buff/cache` 的作用和含义分析 `buff/cache` 占用大量内存的原因是否需要清理缓存及其方法 命令清理缓存方法1. `sync` 命令2. `echo 3>/proc/sys/vm/drop_caches` 命令 注意事项小结 现象 使用free 命令,看到 buff/cache 占用很多 。 free 命令用于显示系统内存的使用情

腾讯视频客户端缓存提取mp4文件工具

在腾讯视频客户端看过视频之后,可以利用本软件,从缓存中提取看过的视频。提取的视频将会保存为MP4文件。 下载地址:http://download.csdn.net/detail/u012107143/9769624 代码如下: @echo offsetlocal ENABLEDELAYEDEXPANSION:INPUT_PATHset video_path=F:\16

LeetCode 算法:LRU 缓存 c++

原题链接🔗: 难度:中等⭐️⭐️ 题目 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key)如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, i

mybatis的延迟加载,一级缓存,二级缓存

MyBatis 延迟加载,一级缓存,二级缓存设置 什么是延迟加载          resultMap中的association和collection标签具有延迟加载的功能。         延迟加载的意思是说,在关联查询时,利用延迟加载,先加载主信息。使用关联信息时再去加载关联信息。 设置延迟加载         需要在SqlMapConfig.xml文件中,在<se