`
yhz61010
  • 浏览: 564430 次
  • 来自: -
博客专栏
63c13ecc-ef01-31cf-984e-de461c7dfde8
libgdx 游戏开发
浏览量:12295
社区版块
存档分类
最新评论

[原创] 连载 3 - 深入讨论 Android 关于高效显示图片的问题 - 如何对位图进行缓存

阅读更多
  更加详细的说明,可以参阅如下官网地址:http://developer.android.com/training/building-graphics.html

  快速导航
  1. 如何高效的加载大位图。(如何解码大位图,避免超过每个应用允许使用的最大内存)http://yhz61010.iteye.com/blog/1848337
  2. 如何在非 UI 线程处理位图。(如何使用 AsyncTask 在后台线程处理位图及处理并发问题)http://yhz61010.iteye.com/blog/1848811
  3. 如何对位图进行缓存。(如何通过创建内存缓存和磁盘缓存来流畅的显示多张位图)http://yhz61010.iteye.com/blog/1849645
  4. 如何管理位图内存。(如何针对不同的 Android 版本管理位图内存)http://yhz61010.iteye.com/blog/1850232
  5. 如何在 UI 中显示位图。(如何通过 ViewPager 和 GridView 显示多张图片)http://yhz61010.iteye.com/blog/1852927

  如何对位图进行缓存?
  若只加载一张位图到 UI 中是很简单的事情,不过,如果要一次性加载多张大图片的话,事情就会变得很复杂。例如,使用 ListView, GridView 或 ViewPager 等显示多张图片,由于使用这些组件时,可以滚动屏幕,从而理论上可以加载无数张图片。

  当使用上述组件显示图片时,若显示的内容随机用户滚动屏幕而不在屏幕上显示的时候,组件本身会重复利用子视图,这里内存的使用量便会降低。若没有长时间存在的引用时,垃圾回收器就会释放已经加载过的图片所占的内存。这当然是非常理想的情况。但是,程序为了让用户能快速而流畅的查看图片,并不希望当已经加载过的图片再次显示在屏幕上时,依然对这些图片再次进行处理。所以,对于再次显示已经加载过的图片,进行内存缓存或磁盘缓存就显得很有必要。

  本文将引导你如何通过创建内存缓存和磁盘缓存来流畅的显示多张位图。

  使用内存缓存
  内存缓存提供了一种快速访问位图的方法,不过其代价是需要占用一些宝贵的应用内存。LruCache 类特别适合用于对位图进行缓存,将最近使用过的对象保存在强引用 LinkedHashMap 中,并且在缓存超过指定的大小时,清除掉最近不使用的位图。

  注意:以前比较流行的内存缓存实现方法是使用 SoftReference 或 WeakReference 对位图进行缓存,但是现在已经不建议这么做了。从 Android 2.3(API Level 9)开始,垃圾回收器会更加主动的回收无效的 soft/weak references。此外,在 Android 3.0 (API Level 11)之前,返回的位图数据虽然保存在内存中,但是所占用的内存并不会按照可预知的方式将其释放掉,从而导致的潜在问题就是应用程序很容易就超过内存限制并导致程序崩溃。

  为了给 LruCache 选取一个合适的大小,我们需要考虑如下一些因素:
  ・余下的页面或应用需要占用多少内存?
  ・屏幕上一次最多可以显示几张图片?需要几屏才能显示余下的图片?
  ・手机的屏幕大小及分辨率是多少?与 Nexus S (hdpi)相比,像 Galaxy Nexus 这样的超清屏(xhdpi)需要更大的缓存才能显示下相同数量的图片。
  ・位图的尺寸和配置是什么?并且需要占用多少内存?
  ・是否经常需要访问图片?某些图片的访问频率是否会比其它图片的访问频率高?若是的话,可能需要专门针对这些图片进行内存缓存。或者使用多个 LruCache 对象来缓存不同的位图。
  ・如何在质量和数量上保持平衡?例如,有时需要缓存大量低品质的图片,有时却需要在后台缓存一张高品质的图片。

  并没有特定缓存的大小或公式适用于所有的应用,这完全取决于你对程序内存使用情况的分析。若缓存太小反而会导致额外的内存开销,起不到缓存的作用。若缓存太大可能会导致发生 java.lang.OutOfMemory 异常,让程序可用的内存变得很小。

  下面的例子为你演示如何为位图建立缓存:
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);
}

  注意:在上面这个例子中,我们设置的缓存大小是应用所占内存的 1/8。对于普通分辨率或高分辨率的屏幕来说,这个缓存值大约是 4 MB (32/8)。对于 800x480 分辨率的屏幕来说,一屏幕的图片大约需要占用 1.5 MB (800*480*4 bytes) 内存。也就是说,整个缓存大约能缓存 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 这样的组件,很容易就将内存缓存占满。又如,应用程序可以被其它任务中断,例如,来了一个电话,此时后台任务会被中止,内存缓存自然也就无效了。若之后用户再次回到应用中,程序还得重新处理图片。

  然而磁盘缓存却可以解决这个问题,它可以持久的缓存图片。当内存缓存无效的时候,磁盘缓存可以有效的帮忙你降低图片加载时间。当然,从磁盘获取图片要比从内存获取图片要慢,所以应用在后台线程加载图片,因为磁盘读写时间是不可预知的。

  注意:若频繁的访问图片,那么 ContentProvider 可能是更加合适的地方用于缓存图片。例如,在制作图像画廊这样的应用程序中。

  在下面的例子中,我们使用了 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) {
    ...
    // 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);
}

  注意:即使初始化磁盘缓存需要磁盘操作权限,但是这也不应该在主线程中进行。这就意味着程序有可能在初始化操作之前就对缓存进行访问。为了解决这个问题,我们在上面的实现中,使用了一个同步锁对象来保证直到缓存被初始化之后,才可以访问磁盘缓存。

  虽然检查内存缓存是在 UI 线程中进行的,但是对磁盘缓存的检查却应该放到后台线程去做。要记住,对磁盘的操作永远不要发生在主 UI 线程中。当图片处理完成后,最终的位图应该同时被添加到内存缓存和磁盘缓存中以备后用。

  处理配置变化
  在程序运行的过程中,配置是会发生变化的,例如,屏幕方向的变化会导致系统销毁当前的页面并重新应用新的配置后重启原页面(关于类似行为的详细说明,请参阅如下文章:Handling Runtime Changes http://developer.android.com/guide/topics/resources/runtime-changes.html)。为了让用户有一个更快更平滑的使用体验,我们并不希望当配置发生变化时,就得重新处理所有的图片。

  幸运的是,我们前面已经讲了如何使用内存缓存,使用它就可以完美的解决这个问题。当使用 Fragment 时,我们可以调用 setRetainInstance(true) 方法,将内存缓存传入新的 activity 实例中。当新的 activity 被重新创建时,被保持的 Fragment 会被重新添加到页面中,然后我们就可以重新访问缓存对象了,从而可以快速的在页面上显示图片。

  下面这个例子是关于使用 Fragment,当配置发生变化时,如何保持一个 LruCache 对象:
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。你可能注意到了,当手机方向发生变化时,图片几乎是瞬间就从内存缓存中被重新加载了。任何没有被内存缓存的图片,都可能被磁盘进行缓存,若也没有被磁盘缓存,那么图片就会从非缓存对象中被加载。
1
1
分享到:
评论

相关推荐

    Android图片缓存框架Fresco极其强大的配置

    3. **位图渲染**:Fresco引入了**Software Layer**和**Drawee**的概念,将复杂的图片处理工作放在后台进行,使用**SurfaceView**或者**TextureView**显示,从而降低了主线程的压力。 ### 二、Fresco的配置 Fresco...

    android 位图转单色位图

    这个过程需要对Android图形系统有深入的理解,并可能需要用到原生代码来实现1位深度的支持。通过这样的转换,开发者可以实现诸如二值化、节省内存等特定需求,尤其是在低功耗设备或性能有限的环境中。

    android 照相、压缩图片、图片缓存

    在Android开发中,照相、压缩图片以及图片缓存是三个关键的技术环节,它们对于提升应用性能和用户体验至关重要。下面将分别对这三个方面进行详细阐述。 1. Android 照相: - 活动(Activity)启动:使用Intent启动...

    Android批量下载图片并缓存,非常流畅

    本教程将详细讲解如何在Android应用中实现批量下载图片并进行高效缓存,以实现非常流畅的用户体验。我们将主要关注LruCache技术,这是一种内存管理策略,有助于优化内存使用。 首先,我们需要理解Android中的图片...

    Android应用源码之Android 图片缓存、加载器.zip

    本资源"Android应用源码之Android 图片缓存、加载器.zip"提供了一个实际的安卓实例,旨在帮助开发者理解和实现高效的图片处理机制。以下将详细阐述相关知识点。 1. **图片加载库的选择与原理** - Android开发中...

    Android 大位图压缩方法二

    在Android开发中,大位图(Bitmap)的处理是一个重要的技术点,特别是在处理高分辨率图像或者需要显示大量图片的应用中。由于Android系统对内存管理的限制,直接加载大位图可能导致应用崩溃,这就是著名的"Out of ...

    Android图片裁剪----移动、缩放图片进行裁剪

    1. 创建一个ImageView或自定义View,将其ScaleType设置为Matrix,这样我们可以直接对Matrix进行操作来改变图片的位置。 2. 使用Matrix的setTranslate方法来移动图片。该方法接受两个参数,分别代表X轴和Y轴上的偏移...

    android 网络应用轻量框架-多线程管理-高效缓存-设计模式

    1:队列优先级 (如果想要listview中移动的区域优先被显示,而不是从上到下显示图片,可以把新建的任务提到任务队列前端) 2:实现了:中断任务的功能(比如进入一个Activity会开启大量任务,如果退出这个Activity ...

    Android从网络加载图片并显示在ImageView控件上

    然后,我们需要对图片进行解码和缩放。Android提供了Bitmap类来处理位图,通过 BitmapFactory 类可以将字节流解码为Bitmap对象。为了避免内存溢出,我们可以使用inJustDecodeBounds参数预览图片大小,再通过...

    Android Google官网的图片缓存源码.zip

    "Android Google官网的图片缓存源码"是一个深入理解Android图片加载与缓存机制的重要参考资料,有助于开发者提升应用的性能和用户体验。本文将详细解析这个主题中的关键知识点。 首先,我们要了解Android中图片缓存...

    Android Google官网的图片缓存源码.rar

    "Android Google官网的图片缓存源码"压缩包包含了Google官方关于图片缓存的实现代码,对于理解Android应用中的图片加载和缓存机制具有极高的参考价值。 1. **内存缓存(Memory Cache)** 内存缓存是图片缓存的第一...

    android中的位图操作demo

    在Android开发中,位图(Bitmap)是一种非常重要的图像处理对象,用于在屏幕上显示和操作图像。本示例“android中的位图操作demo”主要涵盖了Android应用开发中关于位图的基本操作,包括位图的加载、绘制以及一些...

    AndroidGoogle官网的图片缓存源码.zip

    你可以通过分析"Android Google官网的图片缓存源码"来深入了解Glide或其他图片库的实现细节,包括如何读写磁盘缓存,如何实现内存管理,以及如何处理各种网络和设备条件下的图片加载问题。这对于提升Android应用的...

    10-android ImageView 图片视图

    在Android开发中,ImageView是用于显示图像资源的重要组件。它能够加载本地图片或者网络图片,是用户界面设计中不可或缺的一部分。本篇文章将深入探讨ImageView的工作原理、如何使用以及相关的优化技巧。 1. **...

    android图片异步加载缓存

    4. **图片库和框架**:在Android中,有许多优秀的第三方库可以帮助开发者处理图片异步加载和缓存问题,如Picasso、Glide、Fresco等。这些库提供了完善的图片处理功能,包括尺寸调整、格式转换、占位符、错误图、内存...

    android显示sdcard上的图片

    在Android平台上,显示SD卡上的图片是一项常见的任务,尤其对于那些需要处理图像的应用来说更是必不可少。这个场景通常涉及到几个关键的技术点,包括读取SD卡上的文件、解析图片数据以及在屏幕上显示。以下是对这些...

    Android高级应用源码-listview获取网络图片缓存优化.rar

    本资源“Android高级应用源码-listview获取网络图片缓存优化.rar”提供了一种解决方案,帮助开发者提高ListView加载网络图片的效率。 1. **内存缓存**: - 内存缓存是一种提高应用性能的有效手段。通过使用HashMap...

    sketch-smooth-corner-android,一个android项目解释了如何在android画布上绘制草图平滑角.zip

    通过研究和学习"sketch-smooth-corner-android"项目,开发者不仅可以掌握如何在Android画布上绘制平滑角,还能对Android图形系统有更深入的理解,这对于提升应用的用户体验和设计质量有着重要的意义。同时,开源的...

    android-image-filter-ndk,使用android ndk在c中处理位图的android示例项目.zip

    总结,"android-image-filter-ndk"项目为我们提供了一个实战范例,展示了如何结合Android NDK和C语言实现高效的图像处理。通过学习该项目,开发者不仅可以掌握Android NDK的使用,还能深入理解图像处理的原理和技巧...

    Android高级应用源码-图片浏览器+缓存+viewpager.zip

    在Android开发中,图片浏览、缓存管理和ViewPager的使用是相当关键的部分,尤其对于构建高效、流畅的用户体验至关重要。这份"Android高级应用源码-图片浏览器+缓存+viewpager.zip"显然提供了一个完整的实现,涵盖了...

Global site tag (gtag.js) - Google Analytics