004_有效的显示位图(LruCache、DiskCache)

Android汇总icon-default.png?t=N6B9https://blog.csdn.net/baopengjian/article/details/104049142​​​​​​​

谷歌文档翻译,配有Demo;

一 技术点:

1. 加载大图片,图片的压缩,缓存;

2. 软引用

3. LruCahce

4.DiskCahe

5. 处理并发

6.ListView加载图片错位,闪烁,上下滑动重复加载

7.横屏时图片不清除

二 Demo地址:

关于DiskCahe的使用代码中要详细些

 http://download.csdn.net/detail/baopengjian/9799317

在加载图片的过程中要注意内存的使用情况,如果不小心,会导致应用崩溃的内存溢出异常:java.lang.OutofMemoryError: bitmap size exceeds VM budget.
加载图片有内存限制的原因:
        1) 移动设备的内存有限,有些手机对单个程序仅分配16M的内存;
        2) 图片本身占用内存大
        3) Android应用程序UI的经常需要几z张位图加载

三 有效加载大图片

图片通常有不同的尺寸和分辨率;如相机的分辨率一般会比手机屏幕的分辨率大,一个图像的高分辨率不提供任何可见的好处,但仍然占用宝贵的记忆,并且由于额外的动态扩展有额外的性能开销,,为了在有限的内存空间限制下加载相应图片,可以加载对应图片的低分辨率版本。

1 读取图片的尺寸(Dimensions)和类型(Type

  BitmapFactory 提供了根据不同数据源创建位图对象的方法如: decodeByteArray()decodeFile(), decodeResource()等等;这些方法试图分配内存的构造图,因此可以很容易地导致OutOfMemory异常。每种类型的解码方法有额外的签名,让你通过BitmapFactory.Optionsclass指定解码选项。将inJustDecodeBounds属性设置为true,而解码避免内存分配,返回零位图对象。这种技术允许您读取图像的尺寸和类型数据之前建设(和内存分配)的位图。
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;

除非你信任的数据源,否则可以通过上述API来检查位图的尺寸

2 缩小图片加载到内存中

是否加载原图片的因素:
  • 估计加载整个图片需要的内存
  • 应用程序提供给加载图片的内存
  • 加载图片的UI控件尺寸
  • 当前设备的屏幕大小和密度
例如,将1024 x768像素图像加载到内存中,如果它最终将被显示在128x96像素ImageView缩略图上,这样做事不值得的。这时需要通过 BitmapFactory.Options 对象的设置  inSampleSize来告诉解码器进行缩放。那么缩放的比例是多少?这里有一种方法:
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;
}

方法中使用2进行计算是跟因为解码器使用的最终值有关)

3 当获取到缩放比例后,因为前面inJustDecodeBounds设置为true返回空的位图对象,必须重新设置为false

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);
}

使用:任意大大小的位图加载到一个100 x100的ImageView中,如以下示例所示代码:

mImageView .setImageBitmap (
    decodeSampledBitmapFromResource (getResources (), R .id .myimage , 100 , 100 ));

四、 异步加载图片

当从网络、本地或内存以外的其他来源加载大图片时,不应放在主线程。
注意:网络加载图片在缩放时,先将输入流用字节数组缓存起来

1 使用AsyncTask

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(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);
            }
        }
    }
}

WeakReference的使用时确保imageView不被回收,使用:
public void loadBitmap ( int resId , ImageView imageView ) {
    BitmapWorkerTask task = new BitmapWorkerTask (imageView );
    task .execute (resId );
}

2 处理并发(Concurrency Handling

列表和表格会复用View,如果每个View都去使用 AsyncTask加载图片, 不能保证异步任务的顺序开始的顺序完成。

a . 创建一个AsyncDrawable

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

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

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

b. 在执行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);
    }
}

c. cancelPotentialWork方法中引用上面的代码示例检查另一个运行的任务已经与ImageView相关联.如果是这样,它试图取消先前的任务callingcancel()。

在少数情况下,新任务数据匹配现有的任务并没有进一步需要发生。这是cancelPotentialWork的实现:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (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;
}

d. getBitmapWorkerTask获取与imageView相关的BitmapWorkerTask

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 ;
}

e. 在BitmapWorkerTask的onPostExecute方法中检查任务是否被取消,

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @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);
            }
        }
    }
}

五、 缓存图片

内存和磁盘缓存常常可以允许组件快速加载图像处理,提高响应能力和流动性的UI加载多个位图

1 内存缓存

通过占用一定的宝贵内存,内存缓存提供了快速访问位图的方法。LruCache尤其适合缓存位图,其保持最近引用的对象在一个强引用LinkedHashMap,当缓存超过其指定大小时,移除最近最少使用的对象。
注意:过去内存缓存通过软引用或弱引用实现,但这种方法不推荐,因为从API9(2.3)以后,垃圾回收器开始更积极的回收软引用或弱引用,使得这种实现变得相当无效。此外,Android 3.0(API级别11)之前,支持位图的数据是存储在本机内存中不是以一种可预见的方式释放,可能导致应用程序暂时超过其内存限制和崩溃
为了给LruCache设置一个合理的大小,应该考虑许多因素,例如:
        1)除了Activity和Application之外的剩余内存;
        2)有多少图片在屏幕上呢?有多少需要屏幕可用准备?
        3)设备的屏幕大小和密度是多少?额外的高密度屏幕(xhdpi)
        4)图片的尺寸和配置,占用多少内存
        5)图片的访问频率。一些图片是否比另一些图片更高频率的被访问,如果是这样,可以维护多个LruCache来缓存不同的对象。
        6)能否有效平衡质量和数量。有时存储大量低质量的位图,而在后台去加载另一版本的高质量位图是非常有用的。
没有特定的大小或配方,适合所有应用程序,由你来分析您的使用和想出一个合适的解决方案。缓存太小会导致额外的开销而且达不到想要的效果,缓存太大可以再次引起OutOfMemory异常,让其他应用程序内存变小。
LruCache设置的Demo:

(1)设置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);
}

注意: 在这个例子中,八分之一的缓存的应用程序内存分配。正常/ hdpi设备这是一个最低约为4 mb(32/8)。全屏显示数据表格填满800 x480分辨率的图像设备上使用大约1.5 mb(800 * 480 * 4个字节),所以这将缓存至少约2.5页的图片在内存中

(2)从LruCache获取图片

当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);
    }
}

(3)请求成功之后,将图片缓存进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;
    }
    ...
}

磁盘缓存

内存缓存虽然有用,但不能完全依赖,当显示数据表格或更大的结果集的时候,很容易就填满它。你的应用程序如果被切到后台,有可能缓存被回收,当重新返回是,你必须的处理每个图像。
但图片不在可用的内存缓存中,磁盘缓存可以解决这种情况。当然,从磁盘获取比从内存加载速度慢,时间不确定,需要使用异步线程。
注意:如果图片被频繁的访问,可以使用内容提供者来存储缓存,例如一个图片库应用程序。
下载好了源码之后,只需要在项目中新建一个libcore.io包,然后将DiskLruCache.java文件复制到这个包中即可。
Demo:DiskLruCache添加了一个磁盘缓存
具体见003. DiskLruCache(demo中有代码)
注意:缓存的初始化要在异步线程中,这意味着,有可能缓存尚未创建就被访问,为了避免这样的问题,需要加线程锁确保在缓存在创建之后才被访问。
在UI线程检查内存缓存,在后台线程检查磁盘缓存。磁盘操作不应该发生在UI线程上。当完成图像处理,最后添加位图内存和磁盘缓存以备将来使用

处理配置更改

运行时配置更改,比如一个屏幕方向改变,导致安卓销毁并重新启动运行活动的新配置(关于这种行为的更多信息,请参阅处理运行时更改)。你想避免再次处理所有你的照片所以用户平稳和快速体验当配置发生变化。
但是你有一个通过内存缓存构建的缓存容器。通过Fragment调用setRetainInstance(true)) 保存缓存从而在新的Activity实例使用。活动被重新创建后,获取被保存的缓存对象,快速获取图像到ImageView对象。】
Demo:Fragment通过配置来保存缓存
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);
    }
}

试着旋转的设备(设置或不设置保留片段)来测试这一点。设置保存内存缓存的图片会被立即加载出来,没有保存的则像往常一样处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值