Android加载图片你必须知道的技巧
来源:互联网 发布:箪食壶浆以迎将军者乎 编辑:程序博客网 时间:2024/06/05 15:47
学习如何处理和加载Bitmap,显示在UI上非常的重要。如果你不重视这块,Bitmap讲很快耗尽你的内存资源,最终导致oom内存溢出。
- 移动设备的内存资源很稀缺,很多时候每个应用只能分配到16MB的内存空间。部分机型可能分配的会更多,但是我们必须保证不超过最大内存的限制。
- Bitmaps本身就非常占用资源。比如一个Galaxy Nexus拍一张照片2592x1936分辨率。如果使用ARGB_8888(2.3版本以后默认值)加载bitmap的话,加载这张图将耗费将近19MB(2592*1936*4 bytes)的内存,直接就超过了很多机器的最大内存。
- 有时候针对ListView, GridView 和 ViewPager 这种控件,我们会有显示很多图片的需求,也要为即将可能显示在屏幕上的图片做处理,让图片为显示做好准备。
加载大图片
图片什么大小和形状都有。经常图片比你的UI控件大得多。例如摄像头拍出来的照片比你手机屏幕的分辨率高得多。
获取Bitmap大小和类型
BitmapFactory提供了很多通过各种各样渠道解码的方法(decodeByteArray(), decodeFile(), decodeResource(), 等等.) 。这些方法都很容易引起oom内存溢出。每个方法都可以通过BitmapFactory.Options这个类去指定解码选项。把inJustDecodeBounds设定成true,然后进行解码,这个时候就不会真的去分配内存,而是返回空的bitmap同时也会返回outWidth, outHeight 和 outMimeType.有了这三个值,我们就可以按照需要对图片进行压缩。
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;
除非你对图片的来源有绝对的信心,不然建议每次解码都需要检查图片的大小和类型。
按比例压缩后加载
你需要注意这些问题
- 预估加载整个图片需要多少内存
- 愿意从整个应用中,分配多少内存给这张图
- 目标UI控件比如ImageView 的大小
- 当前设备的屏幕分辨率和尺寸
例如把一个1024x768的图片全部加载显示在一个128x96像素的ImageView.上是没有价值的。
下面是根据目标控件传入的宽和高,计算出合适的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;}
使用这个方法,第一次解码设置inJustDecodeBounds为true,得到合适的inSampleSize后,设置inJustDecodeBounds为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);}
这个方法可以把很大的图,很方便的展示在很小的ImageView上。
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
避免在UI线程上处理图片
如果图片来源在磁盘或者网络上(或者其他内存之外的媒介),上面讨论的BitmapFactory.decode*方法不应该在主UI线程上执行。加载图片需要的时间是不可预知的,依赖于很多因素(磁盘读取速度,网速,图片大小,CPU速度等)。任何一个工作都可以阻塞UI线程,造成无响应。下面说说用子线程加载bitmaps的问题。
AsyncTask
AsyncTask应该是大家都很熟悉的子线程通知UI线程修改的方法。
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); } } }}
这段代码很简单,但是可以看到好的代码习惯,比如使用软引用避免AsyncTask的引用导致内存泄漏。判断是否为空,避免空指针异常。使用代码如下:
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId);}
并发的使用
如果我们使用ListView 和 GridView这种重复利用子控件的UI控件时,如果我们使用上面的AsyncTask,当任务完成的时候 无法保证该子控件是否已经被重用了。此外,任务开始的顺序和完成的顺序也无法保证。
下面是解决方案,创建一个专门的 Drawable 子类来储存载入图片的任务引用。这样使用BitmapDrawable,当任务完成的时候placeholder 中的图片就能在ImageView显示了。
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(); }}
在执行 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方法用来检查是否已经有任务绑定到,如果已经有一个任务了就尝试取消这个任务(调用 cancel()方法)。
在少数情况下,如果新的任务和已经存在任务的数据一样,则不需要特殊额外的处理。下面是 cancelPotentialWork 方法的一种实现:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; // If bitmapData is not yet set or it differs from the new 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(),获取和 ImageView关联的任务:
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() 。
检查任务是否取消了,和当前的任务和 ImageView引用的任务是否为同一个任务。
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); } } }}
这套实现适用于ListView 和 GridView ,也适用于其他有子控件回收机制的控件。简单的在设置ImageView图片的地方调用loadBitmap 方法就可以。比如在GridView的getView方法中调用loadBitmap。
缓存Bitmaps
加载一张图片很简单,加载一大堆图片就麻烦了,比如 ListView, GridView 或者 ViewPager。LruCache 是官方推荐的缓存图片的类,低版本可以使用v4支持包。
- 你的设备可以为每个应用程序分配多大的内存?
- 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
- 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
- 图片的尺寸和大小,还有每张图片会占据多少内存空间。
- 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
- 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
下面是一个例子:
private LruCache<String, Bitmap> mMemoryCache;@Overrideprotected 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. 使用最大可以内存的八分之一作为LruCache的缓存大小 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 这个是必须实现的,返回的是每个bitmap的大小,每次添加bitmap时都会调用 // 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);}
当加载bitmap到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 LruCache很容易超出内存限制, DiskLruCache是官方推荐做持久化的缓存类,把图片缓存到磁盘内避免反复网络下载图片也能提高加载速度。
private DiskLruCache mDiskLruCache;private final Object mDiskCacheLock = new Object();private boolean mDiskCacheStarting = true;private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MBprivate static final String DISK_CACHE_SUBDIR = "thumbnails";@Overrideprotected 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);}
当图片加载完毕,图片应该同时缓存到memory和磁盘中,以便以后加载。
处理配置变化
当配置发生变化,例如横竖屏切换的时候,安卓会销毁使用新的配置重建activity。为了避免图片重新处理,我们需要对代码做一些修改。
这是一个Fragment中保留LruCache不因为配置变化而重新加载的例子。
private LruCache<String, Bitmap> mMemoryCache;@Overrideprotected 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); }}
使用第三方图片加载库
处理图片是每个开发者都无法避免的大问题,幸运的是已经有很多非常成熟的第三方图片加载库。笔者用过市面上主流的四种加载库。ImageLoader,Picasso,Glide,Fresco,甚至Volley的图片加载也尝试过。每个加载库各有千秋,可以根据需求做选择,大大提升开发速度,减少oom异常的概率。笔者更推荐来自facebook的Fresco,在使用过程中,渐进式显示,三级内存在低端机上体验更好。送上github上的链接: https://github.com/facebook/fresco 。中文文档非常详尽,地址: http://fresco-cn.org/ 。开发者可以根据自己的需求,选择适合自己项目的库。
- Android加载图片你必须知道的技巧
- android 工程师 你必须知道的
- 你必须知道的Android命名规范
- Android应用开发之(你必须知道的“避免内存溢出图片处理方案”)
- Android应用开发之(你必须知道的“避免内存溢出图片处理方案”)
- CakePHP你必须知道的21条技巧
- CakePHP你必须知道的21条技巧
- CakePHP你必须知道的21条技巧
- (转)CakePHP你必须知道的21条技巧
- (转)CakePHP你必须知道的21条技巧
- 你必须知道的10个提高Canvas性能技巧
- 你必须知道的10个提高Canvas性能技巧
- 你必须知道的10个提高Canvas性能技巧
- 你必须知道的10个提高Canvas性能技巧
- 10个你必须知道的实时图片搜索引擎
- 你必须知道的网页设计图片常识
- seo必须知道的技巧
- 《你必须知道的.NET》
- 同一服务器部署多个tomcat时的端口号修改详情
- Unix/Linux系统下的高级C编程的主要内容1、20160531
- SharedPreferences保持对象数据
- 一个简单的MemoryCache的实现
- hdu 2433 spfa
- Android加载图片你必须知道的技巧
- 理解Docker跨多主机容器网络
- 当使用apm遇到问题时常用的apm命令
- hiho第七周 完全背包
- C++ 中的关键字explicit
- java面向对象编程学习日志【1】第一章
- New package not yet registered with the system 解决方法
- iOS学习之——GCD(Grand Central Dispatch)
- 遇到的问题(一)——editText软键盘不是自动弹出的吗,为什么宝宝的不弹?