为什么Bitmap需要高效加载? 现在的高清大图,动辄就要好几M,而Android对单个应用锁施加的内存限制,只有小十几M,如16M,这导致加载Bitmap的时候很容易出现内存溢出。如下异常信息,便是在开发中经常需要的:
java.lang.OutofMemoryError:bitmap size exceeds VM budget
为了解决这个问题,就出现了Bitmap的高效加载策略。假设通过ImageView来显示图片,很多时候ImageView并没有原始图片的尺寸那么大,这个时候把整个图片加载进来后再设置给ImageView,显然是没有必要的,因为ImageView根本没办法显示原始图片。这时候就可以按一定的采样率来将图片缩小后再加载进来,这样图片既能在ImageView显示出来,又能降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。
Bitmap高效加载的具体方式 加载Bitmap的方式 Bitmap在Android中指的是一张图片。通过BitmapFactory类提供的四类方法:decodeFile,decodeResource,decodeStream和decodeByteArray,分别从文件系统,资源,输入流和字节数组中加载出一个Bitmap对象,其中decodeFile,decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。
BitmapFactory.Options的参数 ①inSampleSize参数 上述四类方法都支持BitmapFactory.Options参数,而Bitmap的按一定采样率进行缩放就是通过BitmapFactory.Options参数实现的,主要用到了inSampleSize参数,即采样率。通过对inSampleSize的设置,对图片的像素的高和宽进行缩放。
当inSampleSize=1,即采样后的图片大小为图片的原始大小。小于1,也按照1来计算。 当inSampleSize>1,即采样后的图片将会缩小,缩放比例为1/(inSampleSize的二次方)。
例如:一张1024 ×1024像素的图片,采用ARGB8888格式存储,那么内存大小1024×1024×4=4M。如果inSampleSize=2,那么采样后的图片内存大小:512×512×4=1M。
注意:官方文档支出,inSampleSize的取值应该总是2的指数,如1,2,4,8等。如果外界传入的inSampleSize的值不为2的指数,那么系统会向下取整并选择一个最接近2的指数来代替。比如3,系统会选择2来代替。当时经验证明并非在所有Android版本上都成立。
关于inSampleSize取值的注意事项: 通常是根据图片宽高实际的大小/需要的宽高大小,分别计算出宽和高的缩放比。但应该取其中最小的缩放比,避免缩放图片太小,到达指定控件中不能铺满,需要拉伸从而导致模糊。
例如:ImageView的大小是100×100像素,而图片的原始大小为200×300,那么宽的缩放比是2,高的缩放比是3。如果最终inSampleSize=2,那么缩放后的图片大小100×150,仍然合适ImageView。如果inSampleSize=3,那么缩放后的图片大小小于ImageView所期望的大小,这样图片就会被拉伸而导致模糊。
②inJustDecodeBounds参数 我们需要获取加载的图片的宽高信息,然后交给inSampleSize参数选择缩放比缩放。那么如何能先不加载图片却能获得图片的宽高信息,通过inJustDecodeBounds=true,然后加载图片就可以实现只解析图片的宽高信息,并不会真正的加载图片,所以这个操作是轻量级的。当获取了宽高信息,计算出缩放比后,然后在将inJustDecodeBounds=false,再重新加载图片,就可以加载缩放后的图片。
注意:BitmapFactory获取的图片宽高信息和图片的位置以及程序运行的设备有关,比如同一张图片放在不同的drawable目录下或者程序运行在不同屏幕密度的设备上,都可能导致BitmapFactory获取到不同的结果,和Android的资源加载机制有关。
3.高效加载Bitmap的流程 ①将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
②从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
③根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
④将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
三、Bitmap高效加载的代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static Bitmap decodeSampledBitmapFromResource (Resources res, int resId, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true ; BitmapFactory.decodeResource(res,resId,options); options.inSampleSize = calculateInSampleSize(options,reqHeight,reqWidth); options.inJustDecodeBounds =false ; return BitmapFactory.decodeResource(res,resId,options); } private static int calculateInSampleSize (BitmapFactory.Options options, int reqHeight, int reqWidth) { int height = options.outHeight; int width = options.outWidth; int inSampleSize = 1 ; if (height>reqHeight||width>reqWidth){ int halfHeight = height/2 ; int halfWidth = width/2 ; while ((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){ inSampleSize*=2 ; } } return inSampleSize; }
这个时候就可以通过如下方式高效加载图片:
1 mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(),R.mipmap.ic_launcher,100 ,100 );
除了BitmapFactory的decodeResource方法,其他方法也可以类似实现。
四、三级缓存 大部分情况下我们的图片都是来自于网络,通过网络请求下载到本地再进行加载。而网路请求是耗时操作,不仅需要大量时间,还会消耗大量的流量,非常影响用户体验。如果图片过多,而我们又没有对图片进行有效的缓存,就很容易导致OOM。 因此对图片进行缓存是非常重要的。
原理 Android图片三级缓存的原理如下:
可见,Android中图片的三级缓存主要是强引用、软引用和文件系统。
Android原生为我们提供了一个LruCache,其中维护着一个LinkedHashMap。LruCache可以用来存储各种类型的数据,但最常见的是存储图片(Bitmap)。LruCache创建LruCache时,我们需要设置它的大小,一般是系统最大存储空间的八分之一。LruCache的机制是存储最近、最后使用的图片,如果LruCache中的图片大小超过了其默认大小,则会将最老、最远使用的图片移除出去。
当图片被LruCache移除的时候,我们需要手动将这张图片添加到软引用(SoftReference)中。我们需要在项目中维护一个由SoftReference组成的集合,其中存储被LruCache移除出来的图片。软引用的一个好处是当系统空间紧张的时候,软引用可以随时销毁,因此软引用是不会影响系统运行的,换句话说,如果系统因为某个原因OOM了,那么这个原因肯定不是软引用引起的。
下面叙述一下三级缓存的流程:
当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找,如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去文件系统中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到文件系统中,然后放到LruCache中。
实现 (1)网络访问工具类HttpUtil:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;public class HttpUtil { private static HttpUtil instance; private HttpUtil () { } public static HttpUtil getInstance () { if (instance == null ) { synchronized (HttpUtil.class) { if (instance == null ) { instance = new HttpUtil(); } } } return instance; } public byte [] getByteArrayFromWeb(String path) { byte [] b = null ; InputStream is = null ; ByteArrayOutputStream baos = null ; try { URL url = new URL(path); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET" ); connection.setDoInput(true ); connection.setConnectTimeout(5000 ); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { baos = new ByteArrayOutputStream(); is = connection.getInputStream(); byte [] tmp = new byte [1024 ]; int length = 0 ; while ((length = is.read(tmp)) != -1 ) { baos.write(tmp, 0 , length); } } b = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null ) { is.close(); } if (baos != null ) { baos.close(); } } catch (Exception e) { e.printStackTrace(); } } return b; } }
(2)操作文件系统的工具类FileUtil:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import android.content.Context;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;public class FileUtil { private static FileUtil instance; private Context context; private FileUtil (Context context) { this .context = context; } public static FileUtil getInstance (Context context) { if (instance == null ) { synchronized (FileUtil.class) { if (instance == null ) { instance = new FileUtil(context); } } } return instance; } public void writeFileToStorage (String fileName, byte [] b) { FileOutputStream fos = null ; try { File file = new File(context.getFilesDir(), fileName); fos = new FileOutputStream(file); fos.write(b, 0 , b.length); } catch (Exception e) { e.printStackTrace(); } finally { try { if (fos != null ) { fos.close(); } } catch (Exception e) { e.printStackTrace(); } } } public byte [] readBytesFromStorage(String fileName) { byte [] b = null ; FileInputStream fis = null ; ByteArrayOutputStream baos = null ; try { fis = context.openFileInput(fileName); baos = new ByteArrayOutputStream(); byte [] tmp = new byte [1024 ]; int len = 0 ; while ((len = fis.read(tmp)) != -1 ) { baos.write(tmp, 0 , len); } b = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (fis != null ) { fis.close(); } if (baos != null ) { baos.close(); } } catch (Exception e) { e.printStackTrace(); } } return b; } }
(3)LruCache的子类ImageCache:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import android.graphics.Bitmap;import android.os.Build;import android.support.annotation.RequiresApi;import android.util.LruCache;import java.lang.ref.SoftReference;import java.util.Map;@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1) public class ImageCache extends LruCache <String , Bitmap > { private Map<String, SoftReference<Bitmap>> cacheMap; public ImageCache (Map<String, SoftReference<Bitmap>> cacheMap) { super ((int ) (Runtime.getRuntime().maxMemory() / 8 )); this .cacheMap = cacheMap; } @Override protected int sizeOf (String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override protected void entryRemoved (boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { if (oldValue != null ) { SoftReference<Bitmap> softReference = new SoftReference<Bitmap>(oldValue); cacheMap.put(key, softReference); } } public Map<String, SoftReference<Bitmap>> getCacheMap() { return cacheMap; } }
(4)三级缓存的工具类CacheUtil:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.Build;import android.widget.ImageView;import java.io.File;import java.lang.ref.SoftReference;import java.util.HashMap;import java.util.Map;public class CacheUtil { private static CacheUtil instance; private Context context; private ImageCache imageCache; private CacheUtil (Context context) { this .context = context; Map<String, SoftReference<Bitmap>> cacheMap = new HashMap<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { this .imageCache = new ImageCache(cacheMap); } } public static CacheUtil getInstance (Context context) { if (instance == null ) { synchronized (CacheUtil.class) { if (instance == null ) { instance = new CacheUtil(context); } } } return instance; } private void putBitmapIntoCache (String fileName, byte [] data) { FileUtil.getInstance(context).writeFileToStorage(fileName, data); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { imageCache.put(fileName, BitmapFactory.decodeByteArray(data, 0 , data.length)); } } private Bitmap getBitmapFromCache (String fileName) { Bitmap bm = null ; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { bm = imageCache.get(fileName); if (bm == null ) { Map<String, SoftReference<Bitmap>> cacheMap = imageCache.getCacheMap(); SoftReference<Bitmap> softReference = cacheMap.get(fileName); if (softReference != null ) { bm = softReference.get(); imageCache.put(fileName, bm); } else { byte [] data = FileUtil.getInstance(context).readBytesFromStorage(fileName); if (data != null && data.length > 0 ) { bm = BitmapFactory.decodeByteArray(data, 0 , data.length); imageCache.put(fileName, bm); } } } } return bm; } public void setImageToView (final String path, final ImageView view) { final String fileName = path.substring(path.lastIndexOf(File.separator) + 1 ); Bitmap bm = getBitmapFromCache(fileName); if (bm != null ) { view.setImageBitmap(bm); } else { new Thread(new Runnable() { @Override public void run () { byte [] b = HttpUtil.getInstance().getByteArrayFromWeb(path); if (b != null && b.length > 0 ) { putBitmapIntoCache(fileName, b); final Bitmap bm = BitmapFactory.decodeByteArray(b, 0 , b.length); view.post(new Runnable() { @Override public void run () { view.setImageBitmap(bm); } }); } } }).start(); } } }
调用 (1)MainActivity的布局文件activity_main.xml中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" encoding="utf-8" ?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@+id/id_main_lv_lv" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="#DDDDDD" android:dividerHeight="1.0dip" /> </RelativeLayout>
(2)MainActivity中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.widget.ListView;import com.example.testimagecache.R;import com.example.testimagecache.SharedData;import com.example.testimagecache.adapter.ImageAdapter;import java.util.Arrays;import java.util.List;public class MainActivity extends AppCompatActivity { private ListView lv; private List<String> urlList; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv = (ListView) findViewById(R.id.id_main_lv_lv); initData(); } private void initData () { urlList = Arrays.asList(SharedData.IMAGE_URLS); } @Override protected void onResume () { super .onResume(); initView(); } private void initView () { ImageAdapter adapter = new ImageAdapter(MainActivity.this , urlList); lv.setAdapter(adapter); } }
(3)ListView的适配器类ImageAdapter中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.widget.ListView;import com.example.testimagecache.R;import com.example.testimagecache.SharedData;import com.example.testimagecache.adapter.ImageAdapter;import java.util.Arrays;import java.util.List;public class MainActivity extends AppCompatActivity { private ListView lv; private List<String> urlList; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv = (ListView) findViewById(R.id.id_main_lv_lv); initData(); } private void initData () { urlList = Arrays.asList(SharedData.IMAGE_URLS); } @Override protected void onResume () { super .onResume(); initView(); } private void initView () { ImageAdapter adapter = new ImageAdapter(MainActivity.this , urlList); lv.setAdapter(adapter); } }
(4)ListView的Item的布局文件listitem_image.xml中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="utf-8" ?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="10.0dip" > <ImageView android:id="@+id/id_imageitem_image" android:layout_width="100.0dip" android:layout_height="100.0dip" android:layout_gravity="center_horizontal" android:contentDescription="@string/app_name" android:scaleType="fitXY" /> </LinearLayout>
参考 Android Bitmap压缩策略
【Android - 进阶】之图片三级缓存的原理及实现