创建带有动画的APP 之 高效的显示位图:缓存位图

载入一张位图到UI界面是很简单直接的事情,但是如果要一次性载入多张图片,那么就会变得很复杂了。在很多情况下,如在ListView, GridView or ViewPager里面,页面上展示的图片,是由快速滚动的很多个图片组成的,数量是几乎没有限制的。

像上面说的几种视图控件,在他们的离开屏幕使子视图被回收利用的情况下,内存的使用一直是保持比较低的。假定你不在使用载入的位图,内存垃圾回收机制同样会释放你载入的位图。这样做理论上来说很好,但是,为了保持你的UI的流畅性和快速载入,你要避免当UI视图重新显示到屏幕的时候,重新去进行图片的载入,处理工作。那么内存或者磁盘的缓存区在这里就是解决这个问题的,可以让UI可以快速的再次载入之前的位图。

这节课里面就引导你如何利用内存,磁盘缓存位图,来提高响应速度,当载入多张位图的时候,让你的UI如流水一样顺滑。

使用内存缓存

内存缓存在应用程序所使用的珍贵的内存情况下,提供了快速访问位图的途径。LruCache类非常适合缓存位图的任务,把最近的引用保存在一个强的引用对象LinkedHashMap里面,当缓存到达它被设计的最大值,会把最近几乎不怎么使用的资源释放掉。

注意:在上一节里面,使用到的一个缓存实现是:SoftReference or WeakReference位图缓存,然而这样做并不推荐。从android 2.3开始,内存回收机制对soft/weak的垃圾回收更加严格,这种情况下,几乎会导致他们无效。另外,前面的版本到android3.0,备份的位图数据时存在本地内存里面的,并无法按可预测的方式自动释放掉,可能会潜在的导致APP暂时内存溢出而导致崩溃。

为了选择一个合适大小的LruCache,以下几个因素要考虑:

  • 你的activity或者app要使用的内存密度
  • 一次性显示到屏幕的位图数量,要准备多少位图来显示到屏幕
  • 设备的分辨率。一个高分辨率的设备,如Nexus,就需要更大的缓存来存储位图。
  • 位图的分辨率和配置,以及要使用多大的内存
  • 对位图的使用的频繁程度。如果频繁使用,就需要让这些位图长期存在于内存,甚至可以有多个LruCache来针对多组位图。
  • 你可以平衡质量和数量吗?有时候这一点很重要,如要存放许多低质量的位图,但是同时要在另外一个任务中载入高质量的位图。

没有标准的大小或者公式来适合每一个应用。主要取决于你来分析你的APP要使用的内存大小来采取合适的解决方案。缓存的数据太小,会导致额外的浪费,缓存的数据太大,可能会导致内存溢出的异常,使得你的APP可用的内存会很有限。

下面的例子就是针对位图而设置的一个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);
}


注意:在这个例子中,程序内存的八分之一被用来作为缓存。一个正常的高分辨率设备使用的缓存大小大约在4MB左右。一个填充位图,占据整个屏幕的GridView,在分辨率为800*480的设备上,大小约为1.5MB。所以我们创建的这个缓存大概可以存放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也许是更加适合存放缓存图像的地方,如果是需要频繁的访问这些图像,例如画廊应用。

下面的的代码示例,使用在Android source里面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);
}


注意:对磁盘缓存操作需要有对磁盘操作的权限,而且也不应该在主线程里面进行。而且,也并不意味着在初始化之前就有机会去访问缓存。要处理这些,在上面的实现中,一个上锁的对象可以保证APP在缓存未初始化之前,无法对该缓存进行读取操作。

在UI线程里面对内存缓存进行检查,对磁盘缓存是在后台线程里面检查的。磁盘的操作永远不应该放在UI线程里面。当图像处理完成,最终产生的位图会被同时存在内存和磁盘缓存,以便将来使用。

处理配置发送改变的情况

运行时的配置发送改变,例如屏幕方向发生变化,会导致andorid系统会销毁并重新启动当前运行的activity.你要避免重复去初始化全部的位图,这样当屏幕发送改变是,用户有一个平滑,快速的用户体验。

幸运的是,你有很好位图内存缓存,如我们在上面讲述到的,这个缓存可以被传递到一个带有Fragment的activity里面,通过调研setRetainInstance(true)方法。当这个anctivity重新被创建的时候,保存的Fragment重新被添加到activity,获得缓存对象,允许位图快速的取出填充到ImageView里面。

下面就一个例子,在配变化的时候,一个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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值