在日常开发的APP,大部分时候需要想用户展示图片信息,图片最终对应Android中的Bitmap对象。而对于APP端来说Bitmap又是一个比较麻烦的问题,主要表现在Bitmap是非常占用内存的对象,处理不当将导致APP运行卡顿甚至出现OOM。Google在其官方有针对Bitmap的使用专门写了一个专题Displaying Bitmaps Efficiently
一、主动释放Bitmap资源
当你确定这个Bitmap资源不会再被使用的时候(当然这个Bitmap不释放可能会让程序下一次启动或者resume快一些,但是其占用的内存资源太大,可能导致程序在后台的时候被杀掉,反而得不偿失),我们建议手动调用recycle()方法,释放其Native内存:
if(bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
bitmap = null;
}
调用bitmap.recycle之后,这个Bitmap如果没有被引用到,那么就会被垃圾回收器回收。如果不主动调用这个方法,垃圾回收器也会进行回收工作,只不过垃圾回收器的不确定性太大,依赖其自动回收不靠谱(比如垃圾回收器一次性要回收好多Bitmap,那么需要的时间就会很多,导致回收的时候会卡顿)。所以我们需要主动调用recycle。
二、主动释放ImageView的图片资源
由于我们在实际开发中,很多情况是在xml布局文件中设置ImageView的src或者在代码中调用ImageView.setImageResource/setImageURI/setImageDrawable等方法设置图像,下面代码可以回收这个ImageView所对应的资源:
private static void recycleImageViewBitMap(ImageView imageView) {
if (imageView != null) {
BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
rceycleBitmapDrawable(bd);
}
}
private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
rceycleBitmap(bitmap);
}
bitmapDrawable = null;
}
private static void rceycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}
三、主动释放ImageView的背景资源
如果你的ImageView是有Background,那么下面的代码可以释放他:
public static void recycleBackgroundBitMap(ImageView view) {
if (view != null) {
BitmapDrawable bd = (BitmapDrawable) view.getBackground();
rceycleBitmapDrawable(bd);
}
}
public static void recycleImageViewBitMap(ImageView imageView) {
if (imageView != null) {
BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable();
rceycleBitmapDrawable(bd);
}
}
private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) {
if (bitmapDrawable != null) {
Bitmap bitmap = bitmapDrawable.getBitmap();
rceycleBitmap(bitmap);
}
bitmapDrawable = null;
}
四、尽量少用Png图,多用NinePatch的图
现在手机的分辨率越来越高,图片资源在被加载后所占用的内存也越来越大,所以要尽量避免使用大的PNG图,在产品设计的时候就要尽量避免用一张大图来进行展示,尽量多用NinePatch资源。
Android中的NinePatch指的是一种拉伸后不会变形的特殊png图,NinePatch的拉伸区域可以自己定义。这种图的优点是体积小,拉伸不变形,可以适配多机型。Android SDK中有自带NinePatch资源制作工具,Android-Studio中在普通png图片点击右键可以将其转换为NinePatch资源,使用起来非常方便。
五、使用大图之前,尽量先对其进行压缩
图片有不同的形状与大小。在大多数情况下它们的实际大小都比需要呈现出来的要大很多。例如,系统的Gallery程序会显示那些你使用设备camera拍摄的图片,但是那些图片的分辨率通常都比你的设备屏幕分辨率要高很多。
考虑到程序是在有限的内存下工作,理想情况是你只需要在内存中加载一个低分辨率的版本即可。这个低分辨率的版本应该是与你的UI大小所匹配的,这样才便于显示。一个高分辨率的图片不会提供任何可见的好处,却会占用宝贵的(precious)的内存资源,并且会在快速滑动图片时导致(incurs)附加的效率问题。
5.1 图片大小压缩:
直接使用ImageView显示bitmap会占用较多资源,特别是图片较大的时候,可能导致崩溃。
使用BitmapFactory.Options设置inSampleSize, 这样做可以减少对系统资源的要求。
属性值inSampleSize表示缩略图大小为原始图片大小的几分之一,即如果这个值为2,则取出的缩略图的宽和高都是原始图片的1/2,图片大小就为原始大小的1/4。
BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options();
bitmapFactoryOptions.inJustDecodeBounds = true;
bitmapFactoryOptions.inSampleSize = 2;
// 这里一定要将其设置回false,因为之前我们将其设置成了true
// 设置inJustDecodeBounds为true后,decodeFile并不分配空间,即,BitmapFactory解码出来的Bitmap为Null,但可计算出原始图片的长度和宽度
options.inJustDecodeBounds = false;
Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);
5.2 图片像素压缩:
Android中图片有四种属性,分别是:
ALPHA_8:每个像素占用1byte内存
ARGB_4444:每个像素占用2byte内存
ARGB_8888:每个像素占用4byte内存 (默认)
RGB_565:每个像素占用2byte内存
Android默认的颜色模式为ARGB_8888,这个颜色模式色彩最细腻,显示质量最高。但同样的,占用的内存也最大。 所以在对图片效果不是特别高的情况下使用RGB_565(565没有透明度属性),如下:
public static Bitmap readBitMap(Context context, intresId) {
BitmapFactory.Options opt = newBitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable = true;
opt.inInputShareable = true;
//获取资源图片
InputStreamis = context.getResources().openRawResource(resId);
returnBitmapFactory.decodeStream(is, null, opt);
}
其他知识点:Android 关于dp dip sp px dpi density解析
1.px
px即像素(Pixel),1px代表了手机屏幕上一个物理的像素点。由于以px为单位的控件在不同手机上显示大小不一定相同,故Android不推荐使用px来设置控件大小。2.分辨率
分辨率通常表示为横轴像素长度和纵轴像素长度的乘积,如320*480等。3.dpi/densityDpi
dpi的全称是Dots Per Inch,即点每英寸,一般被称为像素密度,它代表了一英寸里面有多少个像素点。计算方法为屏幕总像素点(即分辨率的乘积除以屏幕大小),常见的取值有120,160,240。4.density
density直译为密度,它的计算公式为屏幕dpi除以160点每英寸,由于单位除掉了,故density只是一个比值,常见取值为1.0,1.5等。在Android中我们可以通过下面代码获取当前屏幕的density:getResources().getDisplayMetrics().density;
简单来说,可以理解为 density 的数值是 1dp=density px;densityDpi 是屏幕每英寸对应多少个点(不是像素点),在 DisplayMetrics 当中,这两个的关系是线性的:
density 1 1.5 2 3 3.5 4 densityDpi 160 240 320 480 560 640 5.dp(dip)
dp,也叫做dip,全称为Density independent pixels,叫做设备独立像素。他是Android为了解决众多手机dpi不同所定义的单位,dp是一种虚拟抽象的像素单位,他的计算公式为:px = dp * (dpi / 160) = dp * density。因此在dpi大小为160的手机上,1dp = 1px,而在dpi大小为320的手机上,1dp = 2px,即在屏幕越大的手机上,1dp代表的像素也越大。因此我们定义控件大小的时候应该使用dp代替使用px。6.sp
sp是Android中定义字体大小的一种单位,全称为Scaled Pixels,叫做放大像素。sp会根据用户手机上设定的字体大小而改变,在用户手机字体大小设置为正常的情况下,1sp = 1dp。sp与px之间的密度比例可以通过如下代码获取:getResources().getDisplayMetrics().scaledDensity;
7.资源文件分辨率
一般而言,我们存放资源文件的目录(res)会有多个子目录,这些子目录代表了不同系统屏幕分辨率:
密度 | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi |
---|---|---|---|---|---|---|
中文 | 低分辨率 | 中分辨率 | 高分辨率 | 超高分辨率 | 超超高分辨率 | 超超超高分辨率 |
dpi | 120以下 | 120~160 | 160~240 | 240~320 | 320~480 | 480~640 |
分辨率 | 240*320 | 320*480 | 480*800 | 720*1280 | 1080*1920 | 3840*2160 |
8.找不到对应分辨率资源文件情况
对于drawable资源,当应用在设备对应dpi目录下没有找到某个资源时,遵循“先高再低”原则,会从附近的分辨率获取图片,然后按比例进行缩放:比如,当前为xhdpi设备,并且只有以下几个目录,则drawable的寻找顺序为:
xhdpi->xxhdpi->xxxhdpi(如果没有更高的了)->nodpi(如果有的话)->hdpi->mdpi,如果在xxhdpi中找到目标图片,则压缩2/3来使用,如果在mdpi中找到图片,则放大2倍来使用。因此,以现在主流设备来说一般可能在drawable-xxhdpi放置一份即可,这样可以尽量避免Android为我们放大图片所导致的OOM
对于values资源,当应用设备在当前dpi对应目录的demins.xml中没有找到目标条目时,采用“就近匹配”原则:
比如,当前为hdpi设备,并且只有以下几个目录,则values的寻找顺序为:
hdpi->xhdpi->mdpi->values,即先向上级dpi目录查找,再向下级dpi目录查找,最后一路向下查找到values目录,如果values下都找不到,就只有找values-ldpi,当然,现在有这个目录的应用不多了。
单位换算
计算dpi
例:一个手机屏幕4英寸(对角线长度),分辨率480×800,dpi如何计算?
dpi(像素密度) = 对角线像素数量 ÷ 对角线长度
对角线像素数量:利用勾股定理,通过屏幕分辨率480×800计算
dpi = 233像素/英寸
density = (233 px/inch)/(160 px/inch)=1.46计算dp与px
dp = (dpi / (160像素/英寸)) px = density px
如1中的dp = 1.46px,意为在dip为233的屏幕上,1dp = 1.46px(像素)
此时屏幕的相对分辨率为:
宽度 = (480 / 1.46)dp = 329dp
高度 = (800 / 1.46)dp = 548dp