文章目录
前言
本篇记录笔者在学习Bitmap过程中的思路和心得
一、起因
在之前为Android界面设置图片的过程中,笔者对于设置图片大致有两种做法
- setImageResource,从而将图片从本地路径直接加载到目标控件上
- Glide框架,从网络url加载图片到视图上
但是笔者在一次本地加载清晰大图到视图上的时候,因为图片过于庞大,且数目较多(用于轮播图),熟悉Android运行期间给应用分配主存存储空间的都知道,这样的做法最终只会导致项目OOM,然后崩溃
那么如何在不对原图使用其他修图软件的基础上,加载清晰图片到自己的应用视图上同时避免OOM呢,那么便需要引入我们本篇介绍的主人公——Bitmap
二、学习Bitmap的前置知识
Android屏幕显示
px
px,即像素,是屏幕上显示数据的最基本的点,在PS里面也是其最根本的单位,所有的图形都是在此基础上生成的,平时我们经常讲的手机屏幕分辨率就是以像素作为单位的,比如在android中我们经常说的手机像素是1080X1920,其实它所表达的意思是在该手机上面在横向上面有1080个像素点,在纵向上有1920个像素点。
sp
在android中用来形式字体大小的单位,正常情况下会按照手机系统设置的文本大小来显示文字,但是同时也会与系统设置的文本保持一致,比如在有些老年机上面为了更好的操作手机有些人会将字体设置为较大字体,这个时候使用sp作为单位的字体也会随之变大,但是如果将字体大小的单位设置为dp,则不会随着系统字体的变化而变化。
ppi
在每次的手机厂商新品发布会上,我们都会听到关于手机的介绍,比如手机的屏幕分辨率,多大尺寸等等。而当我们知晓一个手机的屏幕分辩率和手机尺寸的时候,就可以计算出手机的物理像素密度,其计算公式为:
dpi
屏幕密度与dpi密切相关,dpi是每英寸的点数。也就是说,密度越大,每英寸内容纳的点数就越多。
dpi十分重要,dpi 决定了应用在显示 drawable 时是选择哪一个文件夹内的切图。每个 drawable 文件夹都对应不同的 dpi 大小,Android 系统会自动根据当前手机的实际 dpi 大小从合适的 drawable 文件夹内选取图片,不同的后缀名对应的 dpi 大小就如以下表格所示。如果 drawable 文件夹名不带后缀,那么该文件夹就对应 160dpi
具体关系如下:
dp
dp和dip是一样的,设备独立像素,这个和设备硬件有关,不同设备有不同的显示效果。而通常在做android项目的时候,为了适配市场上面众多的手机屏幕分辩率,我们一般都会采用dp。dp是Android基于物理设备的PPI抽象出来的一个单位。它是以160dpi的屏幕为基准定义的,在160dpi的屏幕上1dp=1px,那么由此我们就可以得出其计算公式:
换算公式:1dp = (屏幕ppi/160)px或者是px = (屏幕ppi/160)*1dp。举个例子:假设ppi = 320,那么1dp = 2px。
要想知道屏幕具体的dp,我们可以通过DisplayMetrics类获得,其中density属性表示模拟器上,每dp对应的px
三、Bitmap和内存之间的关系
我们这里尝试将一张1920*1080px的图片保存到 drawable-xxhdpi 文件夹内,然后将其显示在一个宽高均为 180dp 的 ImageView 上,该 Bitmap 所占用的内存就通过 bitmap.byteCount来获取;我们来看看它最终占用了多少内存空间
下面我们通过一段代码来查看Bitmap最终用了多少空间
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.iv_bitmap_practice);
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.bitmappicthird, options);
imageView.setImageBitmap(bitmap);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.d("imageView width: " , imageView.getWidth()+"");
Log.d("imageView height: " , imageView.getHeight()+"");
Log.d("bitmap width: " , bitmap.getWidth()+"");
Log.d("bitmap height: " , bitmap.getHeight()+"");
Log.d("bitmap config: " , bitmap.getConfig()+"");
Log.d("inDensity: " , options.inDensity+"");
Log.d("inTargetDensity: " , options.inTargetDensity+"");
Log.d("bitmap byteCount: ", bitmap.getByteCount()+""); }
},1000); //10这个数字是毫秒单位
}
将19201080的图片置于mipmap-xxhdpi文件夹下,查看执行结果
将19201080的图片置于mipmap-hdpi文件夹下,查看执行结果
inDensity代表的是系统最终选择的 mipmap 文件夹类型对应的dpi,这里可以看到hdpi对应的240,xxhdpi对应的是640;
inTargetDensity代表的是目标设备对应的dpi,这里我们始终看到虚拟机对应的dpi为320
这里我们可以总结出如下事实
-
imageView的宽度为inTargetDensity/160*(我们设定的dp宽度和长度)
-
当inDensity!=inTargetDensity时,bitmap会对原始长度进行缩放,缩放的比例为inTargetDensity/inDensity*(Bitmap原始的图片像素大小)
-
Bitmap图片大小为,实际的widthheight(编码对应的字节长度) e.g.如大家都是知道ARGB_8888对应的是4位字节的长度,分别表示不透明度、红色、绿色、蓝色,所以以第二张图为例,bitmap的大小为256014404=14745600(字节)
-
ImageView和Bitmap的大小没有任何关系,两张图可以看出ImageView的大小只和我们设定的dp大小和inTargetDensity/160有关系
四、BitmapFactory
BitmapFactory提供如下方法进行加载Bitmap对象
- decodeFile
- decodeResourceStream
- decodeResource
- decodeByteArray
- decodeStream
DecodeResource方法也会调用到decodeResourceStream方法,decodeResourceStream方法如果判断到inDensity 和 inTargetDensity 两个属性外部没有主动赋值的话,就会根据实际情况进行赋值
相关代码如下:
@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//如果 density 没有赋值的话(等于0),那么就使用基准值 160 dpi
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
//在这里进行赋值,density 就等于 drawable 对应的 dpi
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//如果没有主动设置 inTargetDensity 的话,inTargetDensity 就等于设备的 dpi
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
五、BitmapConfig
Bitmap.Config 定义了四种常见的编码格式,分别是:
- ALPHA_8。每个像素点需要一个字节的内存,只存储位图的透明度,没有颜色信息
- ARGB_4444。A(Alpha)、R(Red)、G(Green)、B(Blue)各占四位精度,共计十六位的精度,折合两个字节,也就是说一个像素点占两个字节的内存,会存储位图的透明度和颜色信息
- ARGB_8888。ARGB 各占八个位的精度,折合四个字节,会存储位图的透明度和颜色信息
- RGB_565。R占五位精度,G占六位精度,B占五位精度,一共是十六位精度,折合两个字节,只存储颜色信息,没有透明度信息
六、优化Bitmap相关
在之前的前言中提到,笔者加载大图未经过任何处理直接加载到imageView上,导致了OOM的出现,那么这种情况下,势必要减小Bitmap图片的体积,保证应用的成功运行
尽量减少 Bitmap 占用的内存大小的话就要从
- 降低图片分辨率
- 降低单位像素需要的字节数
进行考虑
来看将1920*1080的图片位于hdpi文件夹下的结果
此时需要大概14M的空间,毫无疑问这是一个相当大的空间
进行第一种做法,我们观察到图片的宽为2560px,高为1440px;
但是实际上我们imageView的大小为360px 360px,毫无疑问用不了那么多像素
inSampleSize
于是我们通过设置inSampleSize,对图片的宽和高进行压缩
//Bitmap最终大小=
BeforeSampleWidth/inSampleSize*BeforeSampleHeight/inSampleSize
我们设置inSampleSize=2,添加如下代码
options.inSampleSize=2;
结果如下
可以观察到,大小缩小为四分之一
inTargetDensity
如果我们不主动设置 inTargetDensity 的话,decodeResource 方法会自动根据当前设备的 dpi 来对 Bitmap 进行缩放处理,我们可以通过主动设置 inTargetDensity 来控制缩放比例,从而控制 Bitmap 的最终宽高。
Bitmap.Config
BitmapFactory 默认使用的编码图片格式是 ARGB_8888,每个像素点占用四个字节,我们可以按需改变要采用的图片格式。例如,如果要加载的 Bitmap 不包含透明通道的,我们可以使用 RGB_565,该格式每个像素点占用两个字节
这里就不进行展示了
七、Bitmap内存存放位置
App开发不可避免的要和图片打交道,由于其占用内存非常大,管理不当很容易导致内存不足,最后OOM,图片的背后其实是Bitmap,它是Android中最能吃内存的对象之一,也是很多OOM的元凶,不过,在不同的Android版本中,Bitmap或多或少都存在差异,尤其是在其内存分配上
2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用),而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,8.0之后图像资源的管理更加优秀,极大降低了OOM。
相关的Bitmap内存分配情况监控案例可以参考这篇博客,本篇文章仅是对其进行总结
Android Bitmap变迁与原理解析
- Android 8.0前,Bitmap的内存数据以数组的形式存储在Java虚拟机中,决定内存OOM的上限为Java虚拟机为Android单个应用开辟的堆内存,大概上限为了192M
- Android 8.0后,Bitmap的内存数据从Java堆转换到了Native堆栈,决定内存OOM的上限为系统内存,即APP无法从系统分配到内存的时候,发生崩溃,需要注意的是,这一层的崩溃一般为Native层的崩溃,如果只是针对虚拟机崩溃的日志收集,是无法收集到Native这一层的系统崩溃的;在Android 8.0后,Bitmap.cpp负责btye[]数组的分配,并以指针的形式返回到Bitmap.java, Bitmap.java只是持有了对相关地址的引用,换而言之Bitmap.java只是一个空壳子
关于内存回收,NativeAllocationRegistry类为Android 8.0引入的一种辅助自动回收native内存的一种机制,当Java对象被GC机制回收的时候,NativeAllocationRegistry可以辅助回收Java对象所申请的native内存,所以Native层的对象回收并不需要我们手动支持
总结
关于Bitmap类的学习笔者用以下的图进行整理