drawable和mipmap目录

1. 前言

11月分劳务派遣进了家大公司orz,开始一个新的项目。当我将一张切图分别放在drawable的各个dpi文件夹后,同事和我说不需要弄那么多份切图放在drawable中,没必要,而且会增大apk包的大小,放一份切图到mipmap-xhdpi中就够了。

我很好奇为什么,但是他并没有回答我,去群里问了下,他们都说我同事说的是对的,我感觉有点不对劲,我一直以为mipmap是专门用来放置launcher图标的,所以就自己探究一下。

2. 相关的文章

在android官网和StackOverflow上找了下,主要是一下几篇文章

2.1 android开发者官网

原文:支持不同密度

由于运行 Android 的设备具有多种屏幕密度,您应始终提供能够根据各种通用密度级别(低密度、中密度、高密度和超高密度)进行定制的位图资源。这有助于您在所有屏幕密度上获得良好的图形质量和性能。

如需生成这些图像,您应以矢量格式的原始资源为基础,按以下尺寸缩放比例生成每种屏幕密度对应的图像:

  • xhdpi:2.0
  • hdpi:1.5
  • mdpi:1.0(基准)
  • ldpi:0.75

这意味着,如果您为 xhdpi 设备生成了一幅 200x200 的图像,则应分别按 150x150、100x100 和 75x75 图像密度为 hdpi 设备、mdpi 设备和 ldpi 设备生成同一资源。

然后,将生成的图片文件置于 res/ 下的相应子目录中,系统将自动根据运行您的应用的设备的屏幕密度选取正确的文件:

MyProject/
  res/
    drawable-xhdpi/
        awesomeimage.png
    drawable-hdpi/
        awesomeimage.png
    drawable-mdpi/
        awesomeimage.png
    drawable-ldpi/
        awesomeimage.png

之后,每当您引用 @drawable/awesomeimage 时,系统便会根据屏幕 dpi 选择相应的位图。

将您的启动器图标置于 mipmap/ 文件夹中。

res/...
    mipmap-ldpi/...
        finished_launcher_asset.png
    mipmap-mdpi/...
        finished_launcher_asset.png
    mipmap-hdpi/...
        finished_launcher_asset.png
    mipmap-xhdpi/...
        finished_launcher_asset.png
    mipmap-xxhdpi/...
        finished_launcher_asset.png
    mipmap-xxxhdpi/...
        finished_launcher_asset.png

注:您应该将所有启动器图标都置于 res/mipmap-[density]/ 文件夹而非 drawable/ 文件夹内,以确保启动器应用使用最佳分辨率图标。 如需了解有关使用 mipmap 文件夹的详细信息,请参阅管理项目概览。

PS: 参阅管理项目那块并没有关于mipmap的说明。

2.2 android开发者官网官方博客

原文:Getting Your Apps Ready for Nexus 6 and Nexus 9

Provide at least an xxxhdpi app icon because devices can display large app icons on the launcher. It’s best practice to place your app icons in mipmap- folders (not the drawable- folders) because they are used at resolutions different from the device’s current density. For example, an xxxhdpi app icon can be used on the launcher for an xxhdpi device.

res/
   mipmap-mdpi/
      ic_launcher.png
   mipmap-hdpi/
      ic_launcher.png
   mipmap-xhdpi/
      ic_launcher.png  
   mipmap-xxhdpi/
      ic_launcher.png
   mipmap-xxxhdpi/   
      ic_launcher.png  # App icon used on Nexus 6 device launcher

Choosing to add xxxhdpi versions for the rest of your assets will provide a sharper visual experience on the Nexus 6, but does increase apk size, so you should make an appropriate decision for your app.

res/
   drawable-mdpi/
      ic_sunny.png
   drawable-hdpi/
      ic_sunny.png
   drawable-xhdpi/   
      ic_sunny.png
   drawable-xxhdpi/  # Fall back to these if xxxhdpi versions aren’t available
      ic_sunny.png
   drawable-xxxhdpi/ # Higher resolution assets for Nexus 6
      ic_sunny.png

2.3 StackOverflow

mipmap drawables for icons:https://stackoverflow.com/questions/23935810/mipmap-drawables-for-icons

mipmap vs drawable folders [duplicate]:https://stackoverflow.com/questions/28065267/mipmap-vs-drawable-folders

3. 测试文章中总结出mipmap的特性

3.1 针对density构建apk时不会被剥离。

来自stackoverflow mipmap drawables for icon的回答(该回答中引用了goole工程师的博客,可靠度max)。

For launcher icons when building density specific APKs. Some developers build separate APKs for every density, to keep the APK size down. However some launchers (shipped with some devices, or available on the Play Store) use larger icon sizes than the standard 48dp. Launchers use getDrawableForDensity and scale down if needed, rather than up, so the icons are high quality. For example on an hdpi tablet the launcher might load the xhdpi icon. By placing your launcher icon in the mipmap-xhdpi directory, it will not be stripped the way a drawable-xhdpi directory is when building an APK for hdpi devices. If you're building a single APK for all devices, then this doesn't really matter as the launcher can access the drawable resources for the desired density.

有些开发者为不同density的设备构建单独的APK,以保持APK的大小。(例如目标设备是xhdpi的像素密度,那么打包时会剥离掉除了drawable-xhdpi的其他文件夹,从而保证apk的大小。PS:原来还有这种操作吗,我不知道怎么弄。)

但是一些launcher(某些设备,或者在Goolge Play中提供)使用会使用比标准48dp更大的尺寸。
launcher会使用getDrawableForDensity去获取更大的icon(PS:也可能并不会,取决于定制的Launcher),并且在需要的时候缩小他们,而不是放大,所以这时候icon时高质量的。比如在一个hdpi平板的启动器可能加载一个xhdpi的icon。通过将你的启动器图标放置在mipmap-xhdpi目录中,它不会像构建用于hdpi设备的APK一样在drawable-xhdpi目录下被剥离。
如果您正在为所有设备构建一个APK,那么这并不重要,因为启动器可以访问所需密度的可绘制资源。

注:上面的表达有点模糊,这里重新捋一下。比如某个平板是hdpi的,launcher一般使用的48dp的大小显示icon,但是这个平板不使用48dp,而是56dp(例如锤子的九宫格桌面就会显示很大的icon),那么启动器一般会通过getDrawableForDensity去获取xhdpi的icon,获取到的这个icon其实是适用在64dp的。那么设置到56dp大小的Imageview时,就会被缩小,所以保证了icon的质量。但是因为xhdpi的drawable已经被剥离,所以只能获取到hdpi的,这样icon会显示就模糊了。而放置在mipmap目录中的图标,则不会被剥离。

--------------以上是原文加上个人翻译(全靠google...)-------------


经测试这个说法基本可以说是正确的,但是有点问题。问题在于最后那句话“如果您正在为所有设备构建一个APK,那么这并不重要,因为启动器可以访问所需密度的可绘制资源。”

这个其实是有区别的,在标准的launcher中,默认是访问当前设备density对应的drawable目录获取资源,并不会判断是否需要获取更高密度的drawable。
而在定制的launcher中可能才有这种判断,因为定制的lanuncher也知道自己的图标是比标准launcher更大的,所以开发者可能会尝试获取更高密度的资源来使用。

而如果放在mipmap中,那么标准的launcher会自动的去获取更加合适的icon。

下面是一些在三星S8 android7.0上的测试,此设备是的density是xxhdpi。

我在drawable-xxhdpi和mipmap-xxhdpi放置了一张44x44的icon。在drawable-xxxhdpi和mipmap-xxxhdpi放置了一张144x144的icon。

分别测试他们在Activity上和launcher上显示的区别。(直接手机屏幕截图,所以图片会很大,如果有缩放请查看原图)

drawable目录 mipmap目录
Activity中
Launcher中

如图,可以看到,在Activity中,有一个全屏的ImageView。分别在其中加载Drawable和mipmap的资源。
发现他们都是直接加载对应的xxhdpi中的资源,也就是48x48的那张icon。

然后在launcher中,如果引用drawable目录中的资源,那么应用图标看起来有点模糊,加载的应该是xxhdpi中的icon。
而如果引用mipmap目录,那么应用图标就清晰了很多,加载的应该是xxxhdpi中的icon。

结论:在App中,无论你将图片放在drawable还是mipmap目录,系统只会加载对应density中的图片,例如xxhdpi的设备,只会加载drawable-xxhdpi或者mipmap-xxhdpi中的资源。
而在Launcher中,如果使用mipmap,那么Launcher会自动加载更加合适的密度的资源。

或者说,mipmap会自动选择更加合适的图片仅在launcher中有效。

3.2 在图片缩放时保证更高的质量(错误的,没有这个特性)

在问题中有这个文章的引用。
https://programmium.wordpress.com/2014/03/20/mipmapping-for-drawables-in-android-4-3/

意思是,使用mipmap时,放大缩小的操作会使图片具有更高的质量。并且在图像渲染时间,提高质量和减轻GPU压力方面具有优势。

经测试,该文章基本瞎扯,是放大还是缩小,图片显示质量一模一样。也可能是在高版本安卓中,无论是drawable目录还是mipmap目录都已经使用了mipmap技术。

以下是在三星s8上的测试,android7.0。分别使用48x48像素的icon放大到984x984测试放大时的质量,192x192像素的icon缩小到20x20的质量,看到的效果如下表格(图片太大,已经进行了缩放适合排版)。

drawable 目录 mipmap 目录
放大
缩小

4. drawable和mipmap目录的结论

  1. 在App中,无论你将图片放在drawable还是mipmap目录,系统只会加载对应density中的图片。
    而在Launcher中,如果使用mipmap,那么Launcher会自动加载更加合适的密度的资源。

  2. 应用内使用到的图片资源,并不会因为你放在mipmap或者drawable目录而产生差异。单纯只是资源路径的差异R.drawable.xxx或者R.mipmap.xxx。(也可能在低版本系统中有差异)

  3. 一句话来说就是,自动跨设备密度展示的能力是launcher的,而不是mipmap的。

总的来说,app图标(launcher icon) 必须放在mipmap目录中,并且最好准备不同密度的图片,否则缩放后可能导致失真。

而应用内使用到的图片资源,放在drawable目录亦或是mipmap目录中是没有区别的,该准备多个密度的还是要准备多个密度,如果只想使用一份切图,那尽量将切图放在高密度的文件夹中。

5. 是否需要多份不同密度的切图的问题

关于这个问题,也可以看看郭霖大神的一篇文章。
Android drawable微技巧,你所不知道的drawable的那些细节

文章中没有获取bitmap原始大小进行对比,只是获取ImageView宽高。
虽然ImageView宽高就是根据Bitmap大小来的。

这里就再做一遍测试。

测试代码:

<resources>
    <string name="app_name">Drawable对比</string>

    <string name="device_dpi" formatted="true">当前设备的dpi:%s</string>
    <string name="screen_size" formatted="true">当前屏幕分辨率:%d x %d</string>
    <string name="image_size" formatted="true">当前ImageView大小:%d x %d</string>
    <string name="bitmap_size" formatted="true">当前bitmap大小:%d x %d</string>
</resources>
<LinearLayout 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"
    android:layout_marginBottom="8dp"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:layout_marginTop="8dp"
    android:orientation="vertical"
    tools:context="com.aitsuki.drawable.MainActivity">

    <TextView
        android:id="@+id/tv_dpi"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/device_dpi" />

    <TextView
        android:id="@+id/tv_screen_size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_dpi"
        android:text="@string/screen_size" />

    <TextView
        android:id="@+id/tv_image_size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_screen_size"
        android:text="@string/image_size" />

    <TextView
        android:id="@+id/tv_bitmap_size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_image_size"
        android:text="@string/bitmap_size"/>

    <ImageView
        android:id="@+id/iv_asuna"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_image_size"
        android:layout_marginTop="16dp"
        android:src="@drawable/asuna" />

</LinearLayout>
package com.aitsuki.drawable;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        float density = getResources().getDisplayMetrics().density;
        final int widthPixels = getResources().getDisplayMetrics().widthPixels;
        final int heightPixels = getResources().getDisplayMetrics().heightPixels;

        TextView tv_dpi = findViewById(R.id.tv_dpi);
        final TextView tv_screen_size = findViewById(R.id.tv_screen_size);
        final TextView tv_image_size = findViewById(R.id.tv_image_size);
        final TextView tv_bitmap_size = findViewById(R.id.tv_bitmap_size);
        final ImageView iv_asuna = findViewById(R.id.iv_asuna);

        tv_bitmap_size.setText(
                getResources().getString(R.string.bitmap_size,
                        iv_asuna.getDrawable().getIntrinsicWidth(),
                        iv_asuna.getDrawable().getIntrinsicHeight()));

        tv_dpi.setText(getString(R.string.device_dpi, getDpiString(density)));
        tv_screen_size.setText(
                getResources().getString(R.string.screen_size, widthPixels, heightPixels ));

        tv_image_size.postDelayed(new Runnable() {
            @Override
            public void run() {
                int height = iv_asuna.getHeight();
                int width = iv_asuna.getWidth();
                tv_image_size.setText(getString(R.string.image_size, width, height));
            }
        }, 1000);
    }

    private String getDpiString(float density) {
        if (density == 0.75f) {
            return "lhdpi";
        } else if (density == 1f) {
            return "mhdpi";
        } else if( density == 1.5f) {
            return "hdpi";
        } else if (density == 2f) {
            return "xhdpi";
        } else if (density == 3f) {
            return "xxhpdi";
        } else if (density == 4f) {
            return "xxxhdpi";
        } else {
            return density +"";
        }
    }
}

测试用的图片:亚斯娜,300x300


asuna.jpg

因为我使用的是测试机是三星s8, 密度3.0,对应资源文件夹是drawable-xxhdpi。
所以,先将图片放置到xxhdpi中,然后再放到其他目录中进行对比。
下面直接上测试的结果。

使用目录 屏幕截图(包含Bitmap大小和Imageview大小) 占用内存
xxhdpi
hdpi
xxxhdpi

可以看到,当图片放到xxhdpi时,显示图片原始大小300x300。而放到hdpi时则是600x600,放到xxxhdpi时则是225x225。

引用郭霖大神博文中的一段话就是:

当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用android_logo这张图时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果drawable-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,我们当前的场景就是drawable-xxxhdpi文件夹,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

当缩放时,会根据系统和使用的文件夹的density进行缩放。

dpi density
xxhdpi 3
hdpi 1.5
xxxhdpi 4
  • 当使用和设备相同的density的xxhdpi文件夹时,图片显示原始大小300x300.
  • 当使用hdpi的图片时,因为该图片在低密度下,系统会认它不够大,会自动帮我们放大, 300 * 3 / 1.5 ==> 600x600
  • 当使用xxxhdpi时,300 * 3 / 4 ==> 225x225

关于内存的使用,android加载图片到ImageView时,具体使用到多少内存我不太清楚(从上面的内存使用截图中很难看的出来)。

但是关于bitmap使用到多少内存倒是非常容易计算的。
默认情况下,android使用argb的方式加载图片资源,也就是一个像素点占用4个字节。而300x300分辨率的图片就占用了360000个字节,也就是350多K的内存。

同样一张图片,我们放到不同目录下导致出现不同的内存使用结果。如果放到hdpi中,那么bitmap使用的内存多了4倍,整整1.3M的内存!

那么,同事跟我说只需要一份切图放到mipmap-xhdpi中会出现什么问题呢?

关于这点,其实郭霖大神说的不对“图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支”
事实上,内存的使用基本是一样的,因为不同drawable会放置不同分辨率的图片。
例如你在drawable-xhdpi放置30x30的icon,在drawable-xxhdpi放置45x45的icon。当一个xxhdpi的设备去加载这张图片时,会自动选择45x45的图片,占用8k左右的内存。
如果你只在drawable-xhdpi放置30x30的icon,而不在drawable-xxhdpi中放置任何icon,那么系统会自动将30x30的图片放大到45x45,也是占用8k左右的内存。

所以,同事跟我说只需要一份切图放到mipmap-xhdpi在内存的使用上是没有什么大问题的,确实能有效的控制apk包的大小。

小问题则是,在高分辨率或者低分辨率的设备中,图片可能因为经过缩放导致模糊或者失真,特别是分辨率比较大的图片,比如引导页和启动页的大图。但是因为选择的是xhdpi是市面上最普及的分辨率,所以也不会存在什么大问题,不过我更加推荐在xxhdpi中多放一份切图,因为大多数旗舰机已经是xxhdpi的分辨率了。

最后,切图放在mipmap-xhdpi和drawable-xhdpi中是没有区别的!
那些跟我说放在mipmap中比较好,只需要一份,会缩放,会更省内存的网友我敲你lailai。

现在时间是:2018年1月12日凌晨05点57分,现在睡觉上班不会迟到吧……