那些值得你去细细研究的Drawable适配(下)

xiaoxiao2021-02-28  16

本篇文章由 王月半子 投稿,继续承接上篇带你研究Drawable的适配问题。相比于上篇,下篇文章更加面向于实战,内容也更加实用。

王月半子的博客地址:http://blog.csdn.net/wrg_20100512


上篇中我们讲了基本概念,今天我们就进入实战对上篇的问题做出解答,相对于昨天的开胃菜,今天这篇你可能需要花费一番工夫阅读。

首先我准备一张600×960像素的png图片,大小为248k,放在不同分辨率的drawable文件夹下,使用同一手机(1200X1920像素)测试。布局很简单,一个RelativeLayout里面包含一个ImageView,ImageView的宽高均为"wrap_content",Activity中关键的代码如下:

BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();if (drawable != null) {    Bitmap bitmap = drawable.getBitmap();    Log.i(TAG, "bitmap width = " + bitmap.getWidth() + " bitmap height = " + bitmap.getHeight());    Log.i(TAG, "bitmap size = " + bitmap.getByteCount());//获取bitmap的占用内存    Log.i(TAG, "imageView width = " + imageView.getWidth() + " imageView height = " + imageView.getHeight()); }

篇幅有限,这里我直接贴出部分结果以及汇总结果:

放在drawable-mhdpi文件夹下:

放在drawable-xxhdpi文件夹下:

汇总结果(原图600×960像素):

接下来我们来分析两个问题:

同一张图片,放在不同目录下,生成了不同大小的Bitmap

要想回答这个问题,必须要深入理解Android系统加载drawable目录下图片的过程,用的是 decodeResource 方法:

final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); bm = decodeResourceStream(res, value, is, null, opts);

该方法本质上就两步:

1. 读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成之后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;原始资源的 density 其实取决于资源存放的目录(比如 drawable-xxhdpi 对应的是480, drawable-hdpi对应的就是240,而drawable目录对应的是TypedValue.DENSITY_DEFAULT=0)。

2. 调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射,代码如下:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,            InputStream is, Rect pad, Options opts) {      if (opts == null) {          opts = new Options();      }      if (opts.inDensity == 0 && value != null) {          final int density = value.density;          if (density == TypedValue.DENSITY_DEFAULT) {              opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;          } else if (density != TypedValue.DENSITY_NONE) {              opts.inDensity = density;          }      }      if (opts.inTargetDensity == 0 && res != null) {          opts.inTargetDensity = res.getDisplayMetrics().densityDpi;      }      return decodeStream(is, pad, opts); }

该方法主要就是对opts对象中的属性进行赋值,代码不难理解。如果value.density=DisplayMetrics.DENSITY_DEFAULT也就是0的话,将 opts.inDensity赋值为 DisplayMetrics.DENSITY_DEFAULT默认值为160.否则就将 opts.inDensity赋值为第一步获取到的值。此外将 opts.inTargetDensity赋值为屏幕密度Dpi。inDensity 和 inTargetDensity要特别注意,这两个值与下面 cpp 文件里面的 density 和 targetDensity 相对应。

BitmapFactory.cpp:

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) { ......    if (env->GetBooleanField(options, gOptions_scaledFieldID)) {        const int density = env->GetIntField(options, gOptions_densityFieldID);//通过JNI获取opts.inDensity的值        const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//通过JNI获取opts.inTargetDensity的值        const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);        if (density != 0 && targetDensity != 0 && density != screenDensity) {            scale = (float) targetDensity / density;//求出缩放的倍数。        }    } }        const bool willScale = scale != 1.0f; ...... SkBitmap decodingBitmap;if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {   return nullObjectReturn("decoder->decode returned false"); }//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小int scaledWidth = decodingBitmap.width();int scaledHeight = decodingBitmap.height();if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {    scaledWidth = int(scaledWidth * scale + 0.5f);//缩放后的宽    scaledHeight = int(scaledHeight * scale + 0.5f);//缩放后的高}if (willScale) {    const float sx = scaledWidth / float(decodingBitmap.width());//宽的缩放倍数    const float sy = scaledHeight / float(decodingBitmap.height());//高的缩放倍数    ......    SkPaint paint;    SkCanvas canvas(*outputBitmap);    canvas.scale(sx, sy);//缩放画布    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);//画出图像}

代码中的density 和 targetDensity均是通过JNI获取的值,前者是 opts.inDensity,targetDensity 实际上是opts.inTargetDensity也就是 DisplayMetrics 的 densityDpi,我的手机的densityDpi在上面已经打印过了320。最终我们看到 Canvas 放大了 scale 倍,然后又把读到内存的这张 bitmap 画上去,相当于把这张 bitmap 放大了 scale 倍。

Android中一张图片(BitMap)占用的内存主要和以下几个因数有关:图片长度,图片宽度,单位像素占用的字节数。这里我们需要知道bitmap中单位像素占据的内存大小,而单位像素占据的内存大小是与.Options的inPreferredConfig有关的:

public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

inPreferredConfig的类型为Bitmap.Config默认值为Bitmap.Config.ARGB_8888。ARGB指的是一种色彩模式,里面A代表Alpha,R表示red,G表示green,B表示blue。ARGB_8888代表的就是这四个通道各占8位也就是一个字节,合起来就是4个字节。同理Bitmap.Config中还有ARGB_4444、 ALPHA_8、 RGB_565 ,他们占用内存的大小和ARGB_8888一样。

我们再来分析一下上面的那汇总表:

已知图片的大小为600×960,格式为png,测试手机的densityDpi为320( opts.inTargetDensity=320)。

当图片放在drawable-mdpi目录时,此时得到的opts.inDensity=160,那么放大的倍数就是320/160=2,放大后图片的大小就是1200×1920,占用的内存就是:1200×1920×4=9216000B,9216000÷1024÷1024≈8.79M。同样的当图片放在drawable-xxhdpi目录下时,此时得到的opts.inDensity=480,那么放大的倍数就是320/480=2/3,放大后图片的大小就是400×640,占用的内存就是:400×640×4=1024000B,1024000÷1024÷1024≈0.98M。其他的类似,这里就不再赘述。至此问题分析完毕。

如果只做一套配图,为什么要放在drawable-xxhdpi文件夹中?

由汇总图可看出,同一张图片,放在不同的drawable目录下(从drawable-lpdi到drawable-xxhpdi)在同一手机上占用的内存越来越小。

因为同一部手机,它的densityDpi是固定的,而不同的drawable目录对应的原始密度不同,并且从drawable-lpdi到drawable-xxhpdi原始密度越来越大,而图片的放大倍数=densityDpi÷原始密度,所以放大倍数变小,自然占用的内存小了。

图片占用的内存主要和以下几个因数有关:

   1. 图片在内存中的像素。

   2. 单位像素占用的字节数。

而对于同一应用来说,单位像素占用的字节数一定是相同的,那么图片占用的内存只与图片在内存中的像素有关了,而图片在内存中的像素又由图片的原始像素和图片在内存中放大的倍数(scale = densityDpi÷原始密度)。 

综上所述,图片占用的内存和以下几个因素有关: 

图片的原始像素

设备的densityDp

图片在哪个drawable目录下

设想一下,做一套适配xhdpi设备的配图,放在drawable-xhdpi目录下,和做一套适配mdpi设备的配图,放在drawable-mdpi目录下,这时候我用一个hdpi的设备来测试这两种方案,哪种方案更省内存呢?

咱们具体分析一下,就拿我的设备来说是xhdpi的,分辨率为1200×1920,仍旧用原来的测试代码,要让ImageView显示为全屏,并且图片放在drawable-xhdpi目录下,那么图片的原始像素就应该是1200×1920。对于相同尺寸的mdpi来说,他的分辨率是600×960,要让ImageView显示为全屏,并且图片放在drawable-mhdpi目录下,那么图片的原始像素就应该是600×960。

这时候使用hdpi的设备来测试方案一,可以得到:图片原始的像素为1200×1920,设备的densityDpi为240,原始的dpi为320。所以放大倍数为2/3,最终图片在内存中的大小为800×1280。

使用hdpi的设备来测试方案二,可以得到:图片原始的像素为600×960,设备的densityDpi为240,原始的dpi为160。所以放大倍数为3/2,最终图片在内存中的大小也为800×1280。

当然在其他不同dpi的设备上这两种方案占用的内存也是一样的,这里就不再赘述。

所以如果只做一套图的话,不考虑app包的大小以及app内部配图的清晰度来说,只要图片所处的drawable目录适配该目录对应着dpi设备(例如做一套适配mdpi设备的图,将这些配图放在drawable-mdpi目录下),再通过android系统加载图片的缩放机制后,不论哪种方案,对于同种dpi的设备,图片所占的内存是相同的。

但是这些都是不考虑app包的大小以及app内部配图的清晰度来说的,事实上不可能不考虑这些因素。在考虑这些因素之前,先说一下图片缩放的问题。

图片的像素、分辨率以及尺寸满足如下关系:

分辨率 = 像素 ÷ 尺寸

图片在进行缩放的时候,分辨率是不变的,变化的是像素和尺寸。比如将图片放大两倍,这时就会有白色像素插值补充原有的像素中,所以就会看起来模糊,清晰度不高。而缩小图片,会通过对图片像素采样生成缩略图,将会增强它的平滑度和清晰度。

如果制作适配xxhdpi设备的图片,同时放在drawable-xxhdpi目录中,其他除了xxxhdpi的设备在显示配图时都是缩小原图,不会出现模糊的情况。而这样的话App打包由于配图的质量高,自然App会相对大些。

相比较App的大小和用户体验来说,毫无疑问,用户至上。所以如果只有一套配图的话,制作高清大图适配xxhdpi的设备,将配图放置在drawable-xxhdpi目录下就可以了。

另外对于xxxhdpi来说,市场上的设备不多,没必要为了那么一点点特殊群体来加大app的容量(4÷3≈1.3倍,容量放大的倍数不小呀!!!)

至此,所有的问题都分析完毕,感谢您的阅读。


如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

转载请注明原文地址: https://www.6miu.com/read-1250125.html

最新回复(0)