ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android官方培训课程中文版:缓存Bitmap实战——用内存缓存与磁盘缓存提升多图加载的响应速度与流畅度

Android官方培训课程中文版:缓存Bitmap实战——用内存缓存与磁盘缓存提升多图加载的响应速度与流畅度 文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载本文基于《Android官方培训课程中文版》高效显示Bitmap 章节中的《缓存Bitmap》一课展开并结合同章节其余课程内容进行源码级扩充。在 ListView、GridView、ViewPager 这类需要一次性加载大量图片的控件中屏幕内外的图片数量几乎没有上限。循环利用子视图RecyclerView 的复用机制与垃圾回收GC虽然能缓解内存压力但每次滑动回来都重新解码一遍图片UI 会明显卡顿。本文的核心方案是用内存缓存LruCache与磁盘缓存DiskLruCache把处理过的 Bitmap保存下来让控件在滑动回来时能瞬间复用从而在有限的设备内存下同时兼顾响应速度与 UI 流畅度。读完本文你将掌握缓存大小的估算方法、LruCache/DiskLruCache 的完整接入代码、后台线程初始化磁盘缓存的线程安全写法以及旋转屏幕等配置改变时如何用 Fragment 保留缓存避免重复加载。为什么要缓存 Bitmap将单个 Bitmap 加载到 UI 是简单直接的但在 ListView、GridView、ViewPager 等滚动场景下需要显示的图片和即将滑动显示的图片数量是不可控的。即使通过循环利用子视图与 GC 释放不再使用的 Bitmap也仍然存在一个体验问题每次从缓存视图滑回来时都要重新处理解码、缩放、甚至从网络拉取那些图片。内存与磁盘缓存正是为了解决这个问题缓存允许控件快速重新加载已经处理过的图片避免重复解码带来的 CPU 开销与 I/O 开销。本文所在章节的课程脉络为高效加载大图通过 inJustDecodeBounds 与 inSampleSize 加载按比例缩小的 Bitmap避免 OutOfMemory非UI线程处理Bitmap用 AsyncTask 在后台线程处理 Bitmap并解决并发问题缓存Bitmap本文主体在内存与磁盘两个层级缓存处理结果管理Bitmap的内存使用针对不同 Android 版本优化 Bitmap 内存recycle() 与 inBitmap在UI上显示Bitmap把上述技巧综合应用到 ViewPager 与 GridView。使用内存缓存Use a Memory Cache内存缓存以花费宝贵的程序内存为代价换取对 Bitmap 的快速访问。LruCache类API Level 4 起可在 Support Library 中找到特别适合缓存 Bitmap它内部使用一个强引用strong referenced的LinkedHashMap保存最近引用的对象并在缓存超出设定大小maxSize时剔除evict最近最少使用到的对象——即标准的 LRU 淘汰策略。为什么不用软引用/弱引用过去流行用SoftReference或WeakReference缓存 Bitmap官方明确不推荐这种做法从 Android 2.3API Level 9开始垃圾回收机制变得更加频繁软弱引用被释放的频率随之增高缓存命中率大幅下降导致引用方案效率极低在 Android 3.0API Level 11之前Bitmap 的像素数据存放在 Native Memory 中释放时机不可预测容易导致程序超出内存限制而崩溃。因此从 Android 2.3 时代开始基于 LruCache 的强引用 显式淘汰模型就成了 Android 官方推荐的缓存实现。这也与仓库中 管理Bitmap的内存使用 一课的版本演进描述一致早期版本像素数据存于 Native 堆、无法预测释放时机而自 Android 3.0 起像素数据并入 Dalvik 堆GC 才能统一管理。如何确定 LruCache 的合适大小没有通用的公式需要结合以下因素综合分析考量因素说明应用剩余可用内存缓存占用的正是应用进程的堆内存heap取多少取决于预算同时呈现的图片数量屏幕上显示多少张、还需要预加载多少张准备滚动显示屏幕大小与密度xhdpi 设备如 Galaxy Nexus缓存同样数量的图片比 hdpi 设备如 Nexus S需要更大的空间Bitmap 的尺寸与配置每张图占用多少字节直接决定缓存能存多少张图片的访问频率若部分图片访问更频繁可考虑按访问频率分组为不同组配置多个 LruCache 对象质量与数量的平衡某些场景保存大量低质量 Bitmap 更有用高质量版本交由后台线程另行加载缓存太小会导致额外的解码开销而收益甚微缓存太大则可能抛出java.lang.OutOfMemory并挤占应用其余功能所需的内存。官方的经验做法是取Runtime.getRuntime().maxMemory()虚拟机最大可用内存的 1/8 作为缓存预算。建立 LruCache 的完整示例private LruCacheString, Bitmap mMemoryCache; Override protected void onCreate(Bundle savedInstanceState) { ... // 获取虚拟机最大可用内存超过此值将抛出 OutOfMemory 异常。 // 单位换算为 KB因为 LruCache 构造函数的参数是 int。 final int maxMemory (int) (Runtime.getRuntime().maxMemory() / 1024); // 将可用内存的 1/8 用作内存缓存。 final int cacheSize maxMemory / 8; mMemoryCache new LruCacheString, Bitmap(cacheSize) { Override protected int sizeOf(String key, Bitmap bitmap) { // 缓存大小按 KB 计量而不是按条目数量计量。 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); }关键点sizeOf()决定了1 个缓存单元如何计算这里用getByteCount() / 1024把 Bitmap 的实际内存占用折算成 KB使缓存上限cacheSize与内存占用直接挂钩addBitmapToMemoryCache()先判空再写入避免重复覆盖以资源 ID 字符串作为 key如String.valueOf(resId)简单且稳定。一个直观的容量参考上述示例把 1/8 内存用作缓存。在常见 hdpi 设备上heap 约 32MB最少约有 4MB 缓存空间一个填满图片的 800x480 手机屏幕 GridView按 800x480x4 bytes 计算约占用 1.5MB因此该缓存大约可支撑 2.5 页的图片内容。加载流程先查内存缓存未命中再后台处理在把 Bitmap 显示到 ImageView 之前先检查 LruCache 是否已存在该图命中则立即显示未命中则设置占位图并触发后台线程处理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 AsyncTaskInteger, Void, Bitmap { ... // 在后台线程解码图片。 Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } ... }这里用到的decodeSampledBitmapFromResource()来自 高效加载大图 一课先用inJustDecodeBounds true读取图片原始宽高再通过calculateInSampleSize()计算 2 的幂次采样率最后真正解码出接近目标尺寸如 100x100的缩略图。缓存配合采样解码才能保证单张 Bitmap 的内存占用可控。完整背景解码与并发处理的实现WeakReference 持有 ImageView、AsyncDrawable 记录任务、cancelPotentialWork 取消过期任务等见 非UI线程处理Bitmap。使用磁盘缓存Use a Disk Cache内存缓存能加速最近访问过的 Bitmap但无法保证所有 Bitmap 都常驻内存GridView 这类大容量控件很容易耗尽整个内存缓存应用被电话等行为暂停退到后台后进程可能被系统杀死内存缓存随之销毁、Bitmap 全部丢失用户恢复应用时又得重新处理所有图片。磁盘缓存用于保存已经处理过的 Bitmap能显著减少不在内存缓存中的 Bitmap的重复加载次数。代价是从磁盘读取比内存慢得多且读取时间不可预期因此磁盘读取必须放到后台线程执行。若图片会被非常频繁地访问如系统图库应用使用ContentProvider可能是比磁盘缓存更合适的方案。DiskLruCache 的初始化与线程安全这一节的示例使用了从 Android 源码libcore 中的DiskLruCache剥离出来的实现。改进后的示例在已有内存缓存基础上叠加磁盘缓存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) { ... // 初始化内存缓存 ... // 在后台线程初始化磁盘缓存 File cacheDir getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTaskFile, Void, Void { Override protected Void doInBackground(File... params) { synchronized (mDiskCacheLock) { File cacheDir params[0]; mDiskLruCache DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); mDiskCacheStarting false; // 初始化完成 mDiskCacheLock.notifyAll(); // 唤醒所有等待中的线程 } return null; } }两个核心设计磁盘缓存初始化放在后台线程因为涉及文件 I/O绝不能在主线程执行用锁对象保证初始化完成前不可读初始化是异步的期间其他线程可能尝试访问磁盘缓存。mDiskCacheStarting标志 wait()/notifyAll()确保读取线程会一直等待到初始化完成。后台任务同时检查磁盘缓存BitmapWorkerTask在解码前先查磁盘缓存未命中才走正常解码流程最终结果同时写入两级缓存class BitmapWorkerTask extends AsyncTaskInteger, Void, Bitmap { ... // 在后台线程解码图片。 Override protected Bitmap doInBackground(Integer... params) { final String imageKey String.valueOf(params[0]); // 在后台线程检查磁盘缓存 Bitmap bitmap getBitmapFromDiskCache(imageKey); if (bitmap null) { // 磁盘缓存未命中 // 按正常流程处理 final Bitmap bitmap decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // 将最终 Bitmap 写入两级缓存 addBitmapToCache(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // 先写入内存缓存与之前相同 if (getBitmapFromMemCache(key) null) { mMemoryCache.put(key, bitmap); } // 再写入磁盘缓存 synchronized (mDiskCacheLock) { if (mDiskLruCache ! null mDiskLruCache.get(key) null) { mDiskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (mDiskCacheLock) { // 磁盘缓存尚未初始化完成时等待 while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); } catch (InterruptedException e) {} } if (mDiskLruCache ! null) { return mDiskLruCache.get(key); } } return null; }获取合适的缓存目录// 在指定的应用缓存目录下创建一个唯一的子目录。 // 优先使用外部存储若未挂载则回退到内部存储。 public static File getDiskCacheDir(Context context, String uniqueName) { // 检查媒体是否已挂载或存储是否为内置存储 // 是则使用外部缓存目录否则使用内部缓存目录。 final String cachePath Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath File.separator uniqueName); }注意getExternalCacheDir()与getCacheDir()返回的都是系统管理的缓存目录应用卸载时自动清理且无需额外存储权限不要与用户可见的公开目录混淆。线程模型小结内存缓存的读取LruCache.get()可在 UI 线程进行磁盘缓存的读取DiskLruCache.get()必须放到后台线程磁盘操作任何时候都不允许发生在 UI 线程。图片处理完成后需同时写入内存缓存与磁盘缓存方便后续直接复用。处理配置改变Handle Configuration Changes运行时配置改变如屏幕方向旋转会导致当前 Activity 被系统销毁并重建。如果不做处理所有图片都会被重新解码用户会明显感知到卡顿。目标是在配置改变时避免重复处理所有图片提供平滑过渡体验。前面建立的内存缓存可以通过一个设置了setRetainInstance(true)的 Fragment 实例被保存下来旋转后新的 Activity 创建时这个被保留的 Fragment 会被重新附着从而拿到缓存对象直接从内存中获取图片快速恢复显示。代码示例如下private LruCacheString, Bitmap mMemoryCache; Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment retainFragment RetainFragment.findOrCreateRetainFragment(getFragmentManager()); mMemoryCache retainFragment.mRetainedCache; if (mMemoryCache null) { mMemoryCache new LruCacheString, Bitmap(cacheSize) { ... // 像往常一样初始化缓存 } retainFragment.mRetainedCache mMemoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG RetainFragment; public LruCacheString, 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); } }findOrCreateRetainFragment()保证同一时间只存在一个持有缓存的 Fragment通过 tag 查找Activity 重建后取回的是同一个缓存实例。验证方式分别在有/无保留 Fragment 的情况下旋转屏幕对比恢复速度。保留缓存时从内存缓存重新绘制几乎没有延迟内存缓存未命中的图片可能存在于磁盘缓存两级缓存都未命中时才走正常解码流程。缓存之外的进阶内存回收与 inBitmap 复用缓存控制存什么而 管理Bitmap的内存使用 一课解决怎么回收与复用二者常配合使用Android 2.3.3API 10及以下Bitmap 像素数据位于 Native 内存GC 无法及时回收。可用引用计数mDisplayRefCount、mCacheRefCountrecycle()在确认不再显示且不在缓存时主动释放注意 recycle 后绘制会抛Canvas: trying to use a recycled bitmap错误Android 3.0API 11及以上像素数据移入 Dalvik 堆GC 可统一管理。推荐在 LruCache 的entryRemoved()回调中把被淘汰的 Bitmap 软引用存入mReusableBitmaps集合随后用BitmapFactory.Options.inBitmap在解码新图时复用旧 Bitmap 的内存减少分配与 GC 压力。Android 4.4API 19之前仅允许复用尺寸完全一致的 Bitmap4.4 起只要新图字节数不超过候选图getAllocationByteCount()即可复用。此外当应用整体内存紧张时可借助onTrimMemory()回调主动清空 LruCache如TRIM_MEMORY_UI_HIDDEN、TRIM_MEMORY_RUNNING_LOW等级别相关内容参见 管理应用的内存。总结在 Android 官方培训课程的这套 Bitmap 缓存方案中三层架构各司其职采样解码层inSampleSize控制单图内存占用load-bitmap.md异步处理层AsyncTask WeakReference AsyncDrawable 控制并发正确性process-bitmap.md缓存层本文LruCache 负责近期热图的内存快速命中DiskLruCache 负责冷图与进程被杀后的持久化兜底RetainFragment 负责配置改变时的不间断缓存。将三者综合应用到 ViewPager、GridView 的完整范例见 在UI上显示Bitmap其核心就是在getView()中调用loadBitmap()先查内存缓存、再查磁盘缓存、最后后台解码并回写两级缓存。遵循这套模式即可在大图、大量图片、频繁滑动与屏幕旋转的严苛场景下获得流畅且不崩溃的图片加载体验。赞分享文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载相关推荐VirtualApp图片加载缓存策略内存与磁盘缓存VirtualApp图片加载缓存策略内存与磁盘缓存 在Android应用开发中图片加载与缓存管理直接影响用户体验和系统性能。VirtualApp作为轻量级沙移动开发虚拟化VirtualAPK资源加载缓存内存缓存与磁盘缓存策略VirtualAPK资源加载缓存内存缓存与磁盘缓存策略 你是否在开发Android插件化应用时遇到过资源加载缓慢、内存占用过高的问题VirtualAPK作为移动开发插件系统5分钟上手Nornir从安装到执行第一个设备管理任务的完整教程5分钟上手Nornir从安装到执行第一个设备管理任务的完整教程 Nornir是一款功能强大的可插拔多线程框架专为设备管理任务设计提供高效的库存管理能力。本上一篇如何一键完整备份你的QQ空间十年青春回忆GetQzonehistory终极解决方案下一篇Windows驱动管理终极指南DriverStore Explorer完全使用手册创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表