Android性能优化之Bitmap高效加载

为什么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;
//计算缩放比,是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;

/**
* 访问Http的工具类
*/
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;
}

/**
* 通过path(URL)访问网络获取返回的字节数组
*/
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 // 当有图片从LruCache中移除时,将其放进软引用集合中
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) { // SDK版本判断
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);
// 将图片存入强引用(LruCache)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
imageCache.put(fileName, BitmapFactory.decodeByteArray(data, 0, data.length));
}
}

/**
* 从缓存中取出图片
*/
private Bitmap getBitmapFromCache(String fileName) {
// 从强引用(LruCache)中取出图片
Bitmap bm = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { // SDK版本判断
bm = imageCache.get(fileName);
if (bm == null) {
// 如果图片不存在强引用中,则去软引用(SoftReference)中查找
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;
}

/**
* 使用三级缓存为ImageView设置图片
*/
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);
// 将从网络获取到的图片设置给ImageView
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() {
// 初始化图片URL列表
urlList = Arrays.asList(SharedData.IMAGE_URLS);
}

@Override
protected void onResume() {
super.onResume();
initView();
}

// 初始化视图
private void initView() {
// 为ListView适配数据
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() {
// 初始化图片URL列表
urlList = Arrays.asList(SharedData.IMAGE_URLS);
}

@Override
protected void onResume() {
super.onResume();
initView();
}

// 初始化视图
private void initView() {
// 为ListView适配数据
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 - 进阶】之图片三级缓存的原理及实现


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!