Android 官网Train阅读记录——4

本文介绍如何在Android应用中高效地加载和缓存Bitmap,包括按比例缩小Bitmap、使用非UI线程处理Bitmap、处理并发问题、内存及磁盘缓存Bitmap以及处理配置改变。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LZ阅读的是中文翻译版本:http://hukai.me/android-training-course-in-chinese/basics/activity-lifecycle/recreating.html,试验机系统版本7.1。

4、Android图像与动画

4.1 高效显示Bitmap

在Android应用中加载Bitmap的操作是需要特别小心处理的,有以下几个方面的原因:

i、移动设备的系统资源有限。Android设备对于单个程序至少需要16MB的内存。

ii、Bitmap会消耗很多内存,特别是对于类似照片等内容更加丰富的图片。例如,一个手机的照相机能够拍摄2592x1936像素(5MB)的图片。如果Bitmap的图像配置是使用ARGB_8888(从Android2.3开始的默认配置),那么加载这张照片到内存大约需要19MB(2592*1936*4bytes)的空间,从而迅速消耗掉该应用的剩余内存空间。

iii、Android应用的UI通常会在一次操作中立即加载许多张Bitmap。


4.1.1 高效加载大图

读取Bitmap的尺寸与类型

BitmapFactory提供了一些解码的方法,用来从不同的资源中创建一个Bitmap。这些方法在解码位图的时候会尝试分配内存,因此容易导致outOfMemory的异常。每一种解码方法都可以通过BitmapFactory.Options设置一些附加的标记,以此来指定解码选项。设置inJustDecodeBounds属性为true可以在解码的时候避免内存的分配,它会返回一个null的Bitmap,但是可以获取奥outWidth、outHeight与outMimeTyype。该技术可以允许你在解码Bitmap之前优先获取图片的尺寸与类型。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;


加载一个按比例缩小的Bitmap到内存中

如果一个ImageView加载一张很大的图片如1024x768,而ImageView需要的像素为128x96,就没有必要把图片整个加载到内存。为了告诉解码器去加载一个缩小版本的Bitmap到内存中,需要在BitmapFactory.Options中设置inSampleSize的值。如果要把1024x768的图片缩小到128x96,则要把inSampleSize的值设置为8,具体的计算SampleSize的方法

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}
Note:设置inSampleSize为2的幂试音为解码器最终还是会对非2的幂的数进行向下处理,获取到最靠近2的幂的数。

接着就可以用该方法获取来解码位图了

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}
在调用计算SampleSize的方法钱,首先需设置Options的inJustDecodeBounds的值为true,然后调用该方法计算,接着把inJustDecodeBounds的值设为false,最后调用解码方法,将Options的值传递进去,加载缩小版本的Bitmap。

非UI线程处理Bitmap

当图片来源是网络或者是存储卡时,解码Bitmap的方法都不应该在UI线程中执行。因为当从网络或存储卡加载Bitmap时,其执行时间时不可估计的,如果加载时间过长,会出现ANR错误。所以接下来用AsyncTask在后台decode Bitmap。


使用AsyncTask

class BitmapWorkerTask extends AsyncTask {
    private final WeakReference imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}
上面代码中为ImageView使用WeakReference确保了AsyncTask所引用的资源可以被垃圾回收器回收。由于当任务结束时不能确保ImageView仍然存在,因此必须在onPostExecute方法里面对引用进行检查。该ImageView在有些情况下可能已经不存在了,如,在任务结束之前用户使用了回退操作,或者是屏幕旋转了。上面代码中的doInBackground方法的内容就是在子线程中执行解码Bitmap的操作。而onPostExecute方法中的内容是在主线程中执行的,而其参数就是doInBackground方法的返回值。

接下来加载Bitmap就简单了

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}


处理并发问题

通常类似ListView等控件在使用上面的loadBitmap方法时,会带来并发的问题。为了提高效率,ListView的Item视图会在用户滑动屏幕时被循环使用。如果每一个Item视图都出发一个AsyncTask,那么就无法确保关联的视图在结束任务时,分配给另外一个Item进行重用。而且,无法确保所有的异步任务的完成顺序和他们本身的启动顺序一致。

所以需要创建一个专用的Drawable的子类来储存任务的引用。

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}
在执行BitmapWorkerTask之前,需要创建一个AsyncDrawable并且将它绑定到目标控件ImageView上

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}
其中cancelPotentialWork方法检查是否有另一个正在执行的任务与该ImageView关联,如果有,它通过执行Task.cancel(true)方法来取消另一个任务。下面是cancelPotentialWork方法

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData == 0 || bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}
getBitmapWorkerTask方法

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

最后在BitmapWorkerTask的onPostExecute方法里修改

class BitmapWorkerTask extends AsyncTask {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}


缓存Bitmap

为了保证一个流畅的user experience,避免在每次屏幕来回滑动时,都要重复加载相同的图片,可以采用内存与磁盘缓存。


使用内存缓存

内存缓存以话费宝贵的程序内存为前提来快速加载Bitmap。LruCache类(在API Level 4的Support Library中可以找到)特别适合用来缓存Bitmap,它使用一个强引用的LinkedHashMap保存最近引用的对象,并且在缓存超过设置大小的时候剔除最近最少使用到的对象。

下面实例用LruCache缓存Bitmap,首先创建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);
}

Note:在上例中,有1/8的内存被用作缓存,这意味着在常见的设备上,最少大概有4MB的缓存控件。如果一个填满图片的控件放置在800x480像素的手机屏幕上,大概会花费1.5MB的缓存空间(800x400x4bytes),因此缓存的容量大概可以缓存2.5页的图片内容。


当加载Bitmap时,会先从LruCache中检查是否缓存了这个Bitmap,若是,则直接从LruCache中取出显示到ImageView,否则,启动一个子线程去架子啊该Bitmap。

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);
    }
}
当LruCache中没有缓存某个Bitmap,启动子线程去加载该Bitmap时,应该在加载完该Bitmap时将之存入LruCache中

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,它还可以减少那些不在内存缓存中的Bitmap的加载次数。当然从磁盘读取图片要比从内存慢,而且由于磁盘读取操作时间是不可预期的,所以读取磁盘操作需要在子线程中使用。

下面演示磁盘缓存Bitmap,这里使用的是Android源码中的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);
}

Note:因为初始化磁盘缓存涉及到IO操作,所以不应该在主线程中进行。但是这也意味着在初始化完成之前缓存可以被访问,为了解决这个问题,在上面的代码中,有一个锁对象来确保在磁盘缓存完成初始化之前,应用无法对它进行读取。

内存缓存的检查是可以在UI线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在UI线程中发生。当图片处理完成后,Bitmap需要添加到内存与磁盘缓存中,方便以后的使用。


处理配置改变

这里特举屏幕旋转的例子。屏幕旋转会导致当前显示的Activity被销毁然后重建。如果我们想在屏幕旋转的时候避免重新加载所有的图片,达到一个良好的user experience,我们就要处理这个配置改变。具体的做法就是,我们可以把内存缓存通过一个Fragment的setretianInstance(true)方法保留下来,当Activity重建时,获取该Fragment内的LruCache实例即可。具体

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.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();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

为了测试上面代码的效果,可以解除屏幕锁定,旋转屏幕观察app是否有延迟的现象,如果没有,则说明LruCache确实在屏幕旋转的时候得到了保留。目的也就达到了。LruCache中没有的图片可能存储在磁盘缓存中。如果两个缓存中都没有,则图片会被正常加载流程加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值