适配——Drawable适配,占用内存测试

转自http://blog.csdn.net/wrg_20100512/article/details/51295317

首先我准备了一张600×960像素的png图片大小为248k,文件名为test,分别放置为不同的图片文件目录下

布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/test" />

</RelativeLayout>

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
    ImageView imageView;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.activity_main);
        imageView = (ImageView) findViewById(R.id.imageView);
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        float density = displayMetrics.density; //屏幕密度
        int densityDpi = displayMetrics.densityDpi;//屏幕密度dpi
        int heightPixels = displayMetrics.heightPixels;//屏幕高度的像素
        int widthPixels = displayMetrics.widthPixels;//屏幕宽度的像素
        float scaledDensity = displayMetrics.scaledDensity;//字体的放大系数
        float xdpi = displayMetrics.xdpi;//宽度方向上的dpi
        float ydpi = displayMetrics.ydpi;//高度方向上的dpi
        Log.i(TAG, "density = " + density);
        Log.i(TAG, "densityDpi = " + densityDpi);
        Log.i(TAG, "scaledDensity = " + scaledDensity);
        Log.i(TAG, "Screen resolution = " + widthPixels + "×" + heightPixels);
        Log.i(TAG, "xdpi = " + xdpi);
        Log.i(TAG, "ydpi = " + ydpi);
        imageView.post(new Runnable() {
            @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
            @Override
            public void run() {
                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());
                    Log.i(TAG, "imageView scaleType = " + imageView.getScaleType());
                }
            }
        });
    }
}


测试结果如下:


从上面的测试结果(同一款手机上),我们可以得出如下结论:
1、同一张图片,放在不同目录下,会生成不同大小的Bitmap。Bitmap的长度和宽度越大,占用的内存就越大。
2、同一张图片,放在不同的drawable目录下(从drawable-lpdi到drawable-xxhpdi)在同一手机上占用的内存越来越小。
3、图片在硬盘上占用的大小,与在内存中占用的大小完全不一样。


为什么同一张图片,放在不同目录下,会生成不同大小的Bitmap。

深入理解Android系统加载drawable目录下图片的过程。我们读取的是 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.
其他的类似,这里就不再赘述。至此就解释了结论一。

解释结论二之前,先说说结论三。

图片在硬盘的大小和内存中的大小不一样

解释这个问题前,先说一个我曾经遇到的非技术问题:App为什么不建议使用jpg图片,因为同样的尺寸,png格式的图片要比jpg图片大很多。(png中有透明通道,而jpg中没有,此外png是无损压缩的,而jpg是有损压缩的,所以png中存储的信息会很多,体积自然就大了)。
我们从三方面来谈谈这个问题。
1、从内存角度
不管是png还是jpg文件,他们都说的是文件存储范畴的事情,它们只存在于文件系统,而非内存或者显存。而前面也说过了,图片占有的内存只与图片长度、图片宽度以及单位像素占用的字节数有关。所以jpg 格式的图片与 png 格式的图片在内存当中不应该有什么不同。(这里变相的也解释了结论三:图片在硬盘的大小和内存中的大小不一样)。
Q:那既然内存占有不会有什么不同,那为什么开发者喜欢用png的图片呢?
这个问题只能从另一个角度来说。
2、解码速度
png是无损压缩的,而jpg是有损压缩的。所以png图片的解码速度明显会高于jpg图片,所以png虽然体积比jpg大但是加载速度却要快一些。
Q:那么png图片有这种好处,那么就把App中所有的图片都换成png的吧?
这样其实也不科学。为什么呢,再换一个角度来分析
3、App包的大小以及流量的角度。
前面说过同样的尺寸,png格式的图片要比jpg图片大很多。那打包出来的App自然很大,用户的流量就会耗费很大。同时如果App中所有的图片都换成png,那些下载的图片(也全部变成png格式的)同样又会有流量的问题。

综上所述,android中使用png和jpg图片各有优劣,我曾在网上看过有人为了减小App的大小,将所有的png图都换成了jpg图片。当然这也不是不可以,看你追求的是什么。

是时候解释结论二啦!

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

结论二:同一张图片,放在不同的drawable目录下(从drawable-lpdi到drawable-xxhpdi)在同一手机上占用的内存越来越小。
其实它的解释表观上从测试结果的表中很明显的体现出来了,并且从结论一的解释中也能分析出为什么从drawable-lpdi到drawable-xxhpdi占用内存越来越小。
解释一下:同一部手机,它的densityDpi是固定的,而不同的drawable目录对应的原始密度不同,并且从drawable-lpdi到drawable-xxhpdi原始密度越来越大,而图片的放大倍数=densityDpi÷原始密度,所以放大倍数变小,自然占用的内存小了。

其实这里想通过结论二解释一下:只做一套配图要放在drawable-xxdpi文件夹中是真的省内存吗,进而回答如果只做一套配图为什么要放在drawable-xxdpi文件夹中。
Q:只做一套配图要放在drawable-xxdpi文件夹中是真的省内存吗?
A:这个问题就从内存分析,图片占用的内存主要和以下几个因数有关:
1、图片在内存中的像素。
2、单位像素占用的字节数。
而对于同一应用来说,单位像素占用的字节数一定是相同的,那么图片占用的内存只与图片在内存中的像素有关了,而图片在内存中的像素又由图片的原始像素和图片在内存中放大的倍数(scale = densityDpi÷原始密度)。 综上所述,图片占用的内存和以下几个因素有关:

  1. 图片的原始像素。
  2. 设备的densityDpi
  3. 图片在哪个drawable目录下

设想一下,做一套适配xhdpi设备的配图,放在drawable-xhdpi目录下,和做一套适配mdpi设备的配图,放在drawable-mdpi目录下,这时候我用一个hdpi的设备来测试这两种方案,哪种方案更省内存呢?
咱们具体分析一下,就拿我的设备来说是xhdpi的,分辨率为1200×1920,仍旧用原来的测试代码,要让imagevie显示为全屏,并且图片放在drawable-xhdpi目录下,那么图片的原始像素就应该是1200×1920。对于相同尺寸的mdpi来说,他的分辨率是600×960,要让imagevie显示为全屏,并且图片放在drawable-mhdpi目录下,那么图片的原始像素就应该是600×960。
这时候使用hdpi的设备来测试方案一,可以得到:
图片原始的像素为1200×1920,设备的densityDpi为240,原始的dpi为320。所以放大倍数为3/4,最终图片在内存中的大小为900×1440。
使用hdpi的设备来测试方案二,可以得到:
图片原始的像素为600×960,设备的densityDpi为240,原始的dpi为160。所以放大倍数为3/2,最终图片在内存中的大小也为900×1440。
当然在其他不同dpi的设备上这两种方案占用的内存也是一样的,这里就不再赘述。

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

但是这些都是不考虑app包的大小以及app内部配图的清晰度来说的,事实上不可能不考虑这些因素。在考虑这些因素之前,先说一下图片缩放的问题。
图片的像素、分辨率以及尺寸满足如下关系:

分辨率 = 像素 ÷ 尺寸

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

如果制作适配xxhdpi设备的图片,同时放在drawable-xxhdpi目录中,其他除了xxxdpi的设备在显示配图时都是缩小原图,不会出现模糊的情况。而这样的话App打包由于配图的质量高,自然App会相对大些。
如果制作适配mdpi设备的图片,同时放在drawable-mdpi目录中,其他除了ldpi的设备在显示配图时都是放大原图,dpi越大的设备显示的配图也就越模糊。
相比较App的大小和用户体验来说,毫无疑问,用户至上。所以如果只有一套配图的话,制作高清大图适配xxdpi的设备,将配图放置在drawable-xxhdpi目录下就可以了。
Q:为什么不制作适配xxxdpi设备的配图,放在drawable-xxxhdpi目录下?
A:市场上xxxdpi的设备不多,没必要为了那么一点点特殊群体来加大app的容量(4÷3≈1.3倍,容量放大的倍数不小呀!!!)



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值