安卓OOM和Bitmap图片二级缓存机制
本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。
OOM(Out Of Memory)
什么是OOM
手机系统内存份存储内存(ROM)和运行内存(RAM),我们谈论OOM讨论的是运行内存,这点如果是新人需要明确。。现在一般来说手机运行内存是2G,3G基本就算很顶配了,4G运行内存的话只有个别手机配置了。
简而言之,OOM就是我们申请的内存太大了,超出了系统分配给我们(app或者说进程)的可用内存。
android系统的app的每个进程或者每个虚拟机有个最大内存限制,如果申请的内存资源超过这个限制,系统就会抛出OOM错误。跟整个设备的剩余内存没太大关系。比如比较早的android系统的一个虚拟机最多16M内存,当一个app启动后,虚拟机不停的申请内存资源来装载图片,当超过内存上限时就出现OOM。
举个栗子,一条金鱼,每次只能吃24颗饲料,你偏偏要喂它30颗,结果,金鱼受不鸟,就挂掉了。
安卓手机有多少内存
早期的手机是每个进程(每个app)分配16M。
后来随着慢慢发展,开始有了24M的,32M的,再变态就是64了。
具体每个手机的给app分配的运行内存根据厂商和机型的不同而定,但是基本的几个数值是一样的。
安卓手机基于Linux系统,Linux是一个多用户的操作系统,一个app在安卓手机里面就是一个用户,一个用户分配到了16m(假如是16m,那么统一每一个app就是16m),当我当前这个app挂了,不会影响我其他程序的运行。
比如我的手机里面有10个app,其中3个在运行,那么这个手机就有3个进程在运行,这3个进程每一个都分配到了(16m)的运行内存。
每个App的内存怎么分配
我是一个app,我被启动了,我分配到了16m的空间,而且,这16m还不是完完整整给你当前程序自己玩个够的,有一部分还必须分给native内存。
- 那么每一个程序的分配到的运行内存到底是怎么分配的呢?
16M = dalvik内存(Java) + native内存(C/C++)
APP内存由 dalvik内存 和 native内存 2部分组成,dalvik也就是java堆,创建的对象就是就是在这里分配的,而native是通过c/c++方式申请的内存,Bitmap就是以这种方式分配的。(android3.0以后,系统都默认通过dalvik分配的,native作为堆来管理)。这2部分加起来不能超过android对单个进程,虚拟机的内存限制。
至于这Dvlyik和Native两部分的分配,有个特点值得说一下。那就是Dalvik(Java)申请的内存即使释放了,native也别想去申请,只能Dalvik自己用,Dalivk申请过的内存Native就不能用了。
以下为引用部分
基于Android开发多媒体和游戏应用时,可能会挺经常出现Out Of Memory 异常 ,顾名思义这个异常是说你的内存不够用或者耗尽了。
在Android中,一个Process 只能使用16M内存,如果超过了这个限制就会跳出这个异常。这样就要求我们要时刻想着释放资源。Java的回收工作是交给GC的,如何让GC能及时的回收已经不是用的对象,这个里面有很多技巧,大家可以google一下。
因为总内存的使用超过16M而导致OOM的情况,非常简单,我就不继续展开说。值得注意的是Bitmap在不用时,一定要recycle,不然OOM是非常容易出现的。
本文想跟大家一起讨论的是另一种情况:明明还有很多内存,但是发生OOM了。
这种情况经常出现在生成Bitmap的时候。有兴趣的可以试一下,在一个函数里生成一个13m 的int数组。
再该函数结束后,按理说这个int数组应该已经被释放了,或者说可以释放,这个13M的空间应该可以空出来,
这个时候如果你继续生成一个10M的int数组是没有问题的,反而生成一个4M的Bitmap就会跳出OOM。这个就奇怪了,为什么10M的int够空间,反而4M的Bitmap不够呢?
这个问题困扰很久,在网上,国外各大论坛搜索了很久,一般关于OOM的解释和解决方法都是,如何让GC尽快回收的代码风格之类,并没有实际的支出上述情况的根源。
直到昨天在一个老外的blog上终于看到了这方面的解释,我理解后归纳如下:
在Android中:
1.一个进程的内存可以由2个部分组成:java 使用内存 ,C 使用内存 ,这两个内存的和必须小于16M,不然就会出现大家熟悉的OOM,这个就是第一种OOM的情况。
2.更加奇怪的是这个:一旦内存分配给Java后,以后这块内存即使释放后,也只能给Java的使用,这个估计跟java虚拟机里把内存分成好几块进行缓存的原因有关,反正C就别想用到这块的内存了,所以如果Java突然占用了一个大块内存,即使很快释放了:
C能使用的内存 = 16M - Java某一瞬间占用的最大内存。
而Bitmap的生成是通过malloc进行内存分配的,占用的是C的内存,这个也就说明了,上述的4MBitmap无法生成的原因,因为在13M被Java用过后,剩下C能用的只有3M了。
引用至此结束
点此查看原文地址
引用这一部分的描述,就是为了进一步证明,每个app所占用的16m(比如说16m)运行内存不是自己可以玩个够的,还得和另外一个小伙伴分享
另外清楚一点,1、我们在Bitmap的时候申请的内存是输入C/C++的,也就是Native这一块的
OOM一般在什么时候发生?
造成OOM的可以概括为两种情况:
1、Bitmap的使用上 (利用Lru的LruCache和DiskLruCache两个类来解决)
2、线程的管理上(利用线程池管理解决。不纳入本次探讨)
Bitmap导致的OOM是比较常见的,而针对Bitmap,常见的有两种情况:
- 单个ImageView加载高清大图的时候
- ListView或者GridView等批量快速加载图片的时候
简而言之,几乎都是操作Bitmap的时候发生的。
制造一个OOM的例子
当前环境:
- Android Studio1.4
- win7 64bit
- 模拟器: Genymotion Nexus One 2.3.7 API10 480*800
如何获得当前手机把为每个app(进程)分配的运行内存
// 测试每个app可用的最大内存(安卓每一个app都运行在自己独立的沙箱里面)
ActivityManager activityManager=(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本机给每个app分配的运行内存
当我们当前测模拟器返回的 32M 的运行内存
在此附上相关代码:
public class MainActivity extends Activity implements View.OnClickListener {
private TextView mTvBtn; // 按钮
private TextView mTvNum; // 显示最大内存
private TextView mTvLoadBigPic; // 加载图片按钮
private ImageView mIvPic;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mTvBtn= (TextView) findViewById(R.id.mTvBtn);
mTvNum= (TextView) findViewById(R.id.mTvNum);
mTvLoadBigPic= (TextView) findViewById(R.id.mTvLoadBigPic);
mIvPic= (ImageView) findViewById(R.id.mIvPic);
mTvBtn.setOnClickListener(this);
mTvLoadBigPic.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.mTvBtn:
// 测试每个app可用的最大内存(安卓每一个app都运行在自己独立的沙箱里面)
ActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本机给每个app分配的运行内存
mTvNum.setText("最大内存: "+memoryClass);
break;
case R.id.mTvLoadBigPic:
Bitmap bigPicBitMap=BitmapFactory.decodeResource(getResources(),R.mipmap.test_pic);
mIvPic.setImageBitmap(bigPicBitMap);
break;
}
}
}
看完代码,我们这里应该停下来看看一下Bitmap类,补充一些知识
通过这样的代码就可以首先从资源文件里面加载图片
Bitmap bigPicBitMap=BitmapFactory.decodeResource(getResources(),R.mipmap.test_pic);
mIvPic.setImageBitmap(bigPicBitMap);
关于Bitmap和BitmapFactory的知识可以百度补充
提一下,BitmapFactory提供了4类方法用于加载Bitmap对象
- 1、decodeFile
- 2、decodeResource
- 3、decodeStream
- 4、decodeByteArray
分别从文件系统、资源、输入流和字节数组读取Bitmap对象
其中,decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法都是在安卓底层实现的,对应BitmapFactory类的几个Native类。
decodeResource这个方法内应说到底还是需要创建一个位图(Bitmap),
对于创建位图,我们来补充一个知识,先看一下的下面这个方法:
public static Bitmap createBitmap (int[] colors, int width, int height, Bitmap.Config config)
具体安卓内部如何调用这个方法本人不得而知,但是我们要明白的是 config 这个参数,每一个位图都有一个默认confit参数,默认值是 ARGB8888
对于config,有几个值,我们借用一个文章说明一下:
A:透明度
R:红色
G:绿
B:蓝
Bitmap.Config ARGB_4444:由4个4位组成,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位
Bitmap.Config ARGB_8888:由4个8位组成,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位
Bitmap.Config RGB_565:即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位
Bitmap.Config ALPHA_8:只有透明度,没有颜色,那么一个像素点占8位。
一般情况下我们都是使用的ARGB_8888,由此可知它是最占内存的,因为一个像素占32位,8位=1字节(byte),所以一个像素占4字节的内存。假设有一张480x800的图片,如果格式为ARGB_8888,那么将会占用(480x800x32)/(8x1024) = 1500KB的内存。
简单来说,我们可以知道,Bitmap默认的ARGB8888是一个质量较好参数,毕竟一个像素点有32个比特位(bit),相当于4个字节(Byte)了。
梦回唐朝,接着说OOM的例子
有了Bitmap和config的知识之后,我们的OOM的成功与否就看我们图片的分辨率了
加入说,图片分辨率是2500+1000,那么加载这张图片所需要的运行内存我们可以大概这么算:
Bitmap.Config ARGB_8888情况下,一个像素点占32位,也就是4个字节。
2500 * 1000 * 4 得出多少个byte
(2500 * 1000) / 1024 得出kb
(2500 * 1000) / 1024 / 1024 得出m
经过运算,得出加载所需的运行内存大致为9.5m
是不是说我们当前手机的这个app就一定可以加载这张图片呢?
不一定,如果这个时候app还有其他代码也占用着的内存,可能就加载不了了。而且我们说过,分配到的16m内存不是自己玩个够,还得两个哥们分着玩。
如果想一针见血,彻底减小,可以整个5000*2000的图片,肯定马上挂掉,爆出OOM。
5000 * 2000 * 4 / 1024 / 1024 得出 38m多。一针见血
高效加载大图和二级缓存,避免OOM
知道了OOM是什么,怎么发生的,接下来我们就应该知道怎么解决问题了。
提出的问题的人很多,拿出解决办法才是关键。
如何高效加载大图?
造成OOM的核心原因:图片分辨率过大
核心解决办法:图片,我们只加载适合的、需要的尺寸!!利用BitmapFactory.Options可完成这一项任务。
注意:我们要处理的分辨率的问题,而不是图片本身大小的问题,一个100*100的10m的图片和一张2000*2000的2m的图片,对我们来说,2m的那张对我们来说反而是大图片,我们针对的是分辨率
通过BitmapFactory.Options通过指定的采样率来缩小图片的分辨率,把缩小到合适分辨率的图片的放到ImageView上面来显示,大大降低了内存压力,有效避免OOM,至于缩小的怎样的分辨率才算合适,谷歌有为我们提供了一段代码,就可以得出这个合适的度!这段代码后面会贴出。
inSimpleSize的比例计算
计算采样率,主要是通过 BitmaoFactory.Options 的inSimpleSize参数进行。
这里我们以120*800的分辨率的图片举例子
当inSimpleSize为1时,图片的分辨率就是原来的分辨率,也就是1200*800
当inSimpleSize为2时,表示图片的宽和高都是为原来的1/2,所整张图变成了原来的1/4
当inSimpleSize位4时,表示图片的宽和高都是为原来的1/4,所以整张图也就变成原来的1/16
依次类推
inSimpleSize数值的说明
- inSimpleSize的值必须是整数
- inSimpleSize的值不能是负数,负数无效
- inSimpleSize的值谷歌建议是2的整数倍,当然你可以写个3,但是最好不要这么干
inSimpleSize的数值怎么确定
这里我们以为400*400图片为例子
比如我们ImageView的大小位100*100,那么我们的,那么这时我们写一个 inSimpleSize 为2的值,那么久刚好变成原图的四分之一,那么很好,刚刚好,那么如果ImageView的大小是320*120之类的呢?问题就来了,怎么去的一个合适的值呢,还有就是,一个页面有多个ImageView,难道我们为每一个ImageView都去挨个计算取样值吗?明显不可能。
inSimpleSize怎么用啊?
谷歌为我们提供了一个规则,很好用,看代码之前,我们还是文字说一下吧,主要逻辑如下,分三步走:
- (1) 将 BitmapFactory的 inJustDecodeBounds 参数设置为true,当设置为true,代表此时不真正加载图片,而是将图片的原始宽和高数值读取出来
- (2) 利用options取出原始图片的宽高和请求的宽高进行比较,计算出一个合适的inSimpleSize的值
- (3) 将 BitmapFactory的 inJustDecodeBounds 参数设置为false,真正开始加载图片(这时候加载就是经过计算后的分辨率)
谷歌提供的方法:
import java.io.FileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
// 从资源加载
public Bitmap decodeSampledBitmapFromResource(Resources res,int resId, int reqWidth, int reqHeight) {
// 设置inJustDecodeBounds = true ,表示先不加载图片
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 调用方法计算合适的 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// inJustDecodeBounds 置为 false 真正开始加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
// 设置inJustDecodeBounds = true ,表示先不加载图片
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// 调用方法计算合适的 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// inJustDecodeBounds 置为 false 真正开始加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
// 计算 BitmapFactpry 的 inSimpleSize的值的方法
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// 获取图片原生的宽和高
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
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;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
来一个调用的代码示例:
mIvPic.setImageBitmap(new ImageResizer().decodeSampledBitmapFromResource(getResources(),R.mipmap.test_pic,300,200));
注意看下面控制台的打印信息
加载一张宽高为 5120*3200的图片,依然没问题,sampleSize为16
16*16=256,代表现在加载的这样图是原图的256分之1.
差别好大
10-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: origin, w= 5120 h=3200
10-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: sampleSize:16
高效加载图片不报OOM就先说到这里啦,下一篇再说图片的二级缓存,也叫图片的存取机制
二级,即为内存缓存,本地缓存,网络,,三者一起构成了图片的存取机制。
内存缓存拿不到就去本地拿。本地拿不到就去网络拿。当我们第一次获取A图片,肯定是是从网络获取的,网络获取后,图片A就存储到本地缓存,就这还会缓存到内存缓存。
缓存主要利用的一个机制是Lru,(Least Recently Used)最近最少使用的。
而Lru和只要是利用两个类,LruCache 和 DiskLruCache。
LruCache主要针对的是 内存缓存 (缓存)
DiskLruCache 主要针对的是 存储缓存 (本地)