drawable和mipmap目錄

1. 前言

11月分勞務(wù)派遣進了家大公司orz,開始一個新的項目。當(dāng)我將一張切圖分別放在drawable的各個dpi文件夾后,同事和我說不需要弄那么多份切圖放在drawable中,沒必要,而且會增大apk包的大小,放一份切圖到mipmap-xhdpi中就夠了。

我很好奇為什么,但是他并沒有回答我,去群里問了下,他們都說我同事說的是對的,我感覺有點不對勁,我一直以為mipmap是專門用來放置launcher圖標的,所以就自己探究一下。

2. 相關(guān)的文章

在android官網(wǎng)和StackOverflow上找了下,主要是一下幾篇文章

2.1 android開發(fā)者官網(wǎng)

原文:支持不同密度

由于運行 Android 的設(shè)備具有多種屏幕密度,您應(yīng)始終提供能夠根據(jù)各種通用密度級別(低密度、中密度、高密度和超高密度)進行定制的位圖資源。這有助于您在所有屏幕密度上獲得良好的圖形質(zhì)量和性能。

如需生成這些圖像,您應(yīng)以矢量格式的原始資源為基礎(chǔ),按以下尺寸縮放比例生成每種屏幕密度對應(yīng)的圖像:

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

這意味著,如果您為 xhdpi 設(shè)備生成了一幅 200x200 的圖像,則應(yīng)分別按 150x150、100x100 和 75x75 圖像密度為 hdpi 設(shè)備、mdpi 設(shè)備和 ldpi 設(shè)備生成同一資源。

然后,將生成的圖片文件置于 res/ 下的相應(yīng)子目錄中,系統(tǒng)將自動根據(jù)運行您的應(yīng)用的設(shè)備的屏幕密度選取正確的文件:

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

之后,每當(dāng)您引用 @drawable/awesomeimage 時,系統(tǒng)便會根據(jù)屏幕 dpi 選擇相應(yīng)的位圖。

將您的啟動器圖標置于 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

注:您應(yīng)該將所有啟動器圖標都置于 res/mipmap-[density]/ 文件夾而非 drawable/ 文件夾內(nèi),以確保啟動器應(yīng)用使用最佳分辨率圖標。 如需了解有關(guān)使用 mipmap 文件夾的詳細信息,請參閱管理項目概覽。

PS: 參閱管理項目那塊并沒有關(guān)于mipmap的說明。

2.2 android開發(fā)者官網(wǎng)官方博客

原文: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. 測試文章中總結(jié)出mipmap的特性

3.1 針對density構(gòu)建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.

有些開發(fā)者為不同density的設(shè)備構(gòu)建單獨的APK,以保持APK的大小。(例如目標設(shè)備是xhdpi的像素密度,那么打包時會剝離掉除了drawable-xhdpi的其他文件夾,從而保證apk的大小。PS:原來還有這種操作嗎,我不知道怎么弄。)

但是一些launcher(某些設(shè)備,或者在Goolge Play中提供)使用會使用比標準48dp更大的尺寸。
launcher會使用getDrawableForDensity去獲取更大的icon(PS:也可能并不會,取決于定制的Launcher),并且在需要的時候縮小他們,而不是放大,所以這時候icon時高質(zhì)量的。比如在一個hdpi平板的啟動器可能加載一個xhdpi的icon。通過將你的啟動器圖標放置在mipmap-xhdpi目錄中,它不會像構(gòu)建用于hdpi設(shè)備的APK一樣在drawable-xhdpi目錄下被剝離。
如果您正在為所有設(shè)備構(gòu)建一個APK,那么這并不重要,因為啟動器可以訪問所需密度的可繪制資源。

注:上面的表達有點模糊,這里重新捋一下。比如某個平板是hdpi的,launcher一般使用的48dp的大小顯示icon,但是這個平板不使用48dp,而是56dp(例如錘子的九宮格桌面就會顯示很大的icon),那么啟動器一般會通過getDrawableForDensity去獲取xhdpi的icon,獲取到的這個icon其實是適用在64dp的。那么設(shè)置到56dp大小的Imageview時,就會被縮小,所以保證了icon的質(zhì)量。但是因為xhdpi的drawable已經(jīng)被剝離,所以只能獲取到hdpi的,這樣icon會顯示就模糊了。而放置在mipmap目錄中的圖標,則不會被剝離。

--------------以上是原文加上個人翻譯(全靠google...)-------------


經(jīng)測試這個說法基本可以說是正確的,但是有點問題。問題在于最后那句話“如果您正在為所有設(shè)備構(gòu)建一個APK,那么這并不重要,因為啟動器可以訪問所需密度的可繪制資源?!?/code>

這個其實是有區(qū)別的,在標準的launcher中,默認是訪問當(dāng)前設(shè)備density對應(yīng)的drawable目錄獲取資源,并不會判斷是否需要獲取更高密度的drawable。
而在定制的launcher中可能才有這種判斷,因為定制的lanuncher也知道自己的圖標是比標準launcher更大的,所以開發(fā)者可能會嘗試獲取更高密度的資源來使用。

而如果放在mipmap中,那么標準的launcher會自動的去獲取更加合適的icon。

下面是一些在三星S8 android7.0上的測試,此設(shè)備是的density是xxhdpi。

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

分別測試他們在Activity上和launcher上顯示的區(qū)別。(直接手機屏幕截圖,所以圖片會很大,如果有縮放請查看原圖)

drawable目錄 mipmap目錄
Activity中
Launcher中

如圖,可以看到,在Activity中,有一個全屏的ImageView。分別在其中加載Drawable和mipmap的資源。
發(fā)現(xiàn)他們都是直接加載對應(yīng)的xxhdpi中的資源,也就是48x48的那張icon。

然后在launcher中,如果引用drawable目錄中的資源,那么應(yīng)用圖標看起來有點模糊,加載的應(yīng)該是xxhdpi中的icon。
而如果引用mipmap目錄,那么應(yīng)用圖標就清晰了很多,加載的應(yīng)該是xxxhdpi中的icon。

結(jié)論:在App中,無論你將圖片放在drawable還是mipmap目錄,系統(tǒng)只會加載對應(yīng)density中的圖片,例如xxhdpi的設(shè)備,只會加載drawable-xxhdpi或者mipmap-xxhdpi中的資源。
而在Launcher中,如果使用mipmap,那么Launcher會自動加載更加合適的密度的資源。

或者說,mipmap會自動選擇更加合適的圖片僅在launcher中有效。

3.2 在圖片縮放時保證更高的質(zhì)量(錯誤的,沒有這個特性)

在問題中有這個文章的引用。
https://programmium.wordpress.com/2014/03/20/mipmapping-for-drawables-in-android-4-3/

意思是,使用mipmap時,放大縮小的操作會使圖片具有更高的質(zhì)量。并且在圖像渲染時間,提高質(zhì)量和減輕GPU壓力方面具有優(yōu)勢。

經(jīng)測試,該文章基本瞎扯,是放大還是縮小,圖片顯示質(zhì)量一模一樣。也可能是在高版本安卓中,無論是drawable目錄還是mipmap目錄都已經(jīng)使用了mipmap技術(shù)。

以下是在三星s8上的測試,android7.0。分別使用48x48像素的icon放大到984x984測試放大時的質(zhì)量,192x192像素的icon縮小到20x20的質(zhì)量,看到的效果如下表格(圖片太大,已經(jīng)進行了縮放適合排版)。

drawable 目錄 mipmap 目錄
放大
縮小

4. drawable和mipmap目錄的結(jié)論

  1. 在App中,無論你將圖片放在drawable還是mipmap目錄,系統(tǒng)只會加載對應(yīng)density中的圖片。
    而在Launcher中,如果使用mipmap,那么Launcher會自動加載更加合適的密度的資源。

  2. 應(yīng)用內(nèi)使用到的圖片資源,并不會因為你放在mipmap或者drawable目錄而產(chǎn)生差異。單純只是資源路徑的差異R.drawable.xxx或者R.mipmap.xxx。(也可能在低版本系統(tǒng)中有差異)

  3. 一句話來說就是,自動跨設(shè)備密度展示的能力是launcher的,而不是mipmap的。

總的來說,app圖標(launcher icon) 必須放在mipmap目錄中,并且最好準備不同密度的圖片,否則縮放后可能導(dǎo)致失真。

而應(yīng)用內(nèi)使用到的圖片資源,放在drawable目錄亦或是mipmap目錄中是沒有區(qū)別的,該準備多個密度的還是要準備多個密度,如果只想使用一份切圖,那盡量將切圖放在高密度的文件夾中。

5. 是否需要多份不同密度的切圖的問題

關(guān)于這個問題,也可以看看郭霖大神的一篇文章。
Android drawable微技巧,你所不知道的drawable的那些細節(jié)

文章中沒有獲取bitmap原始大小進行對比,只是獲取ImageView寬高。
雖然ImageView寬高就是根據(jù)Bitmap大小來的。

這里就再做一遍測試。

測試代碼:

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

    <string name="device_dpi" formatted="true">當(dāng)前設(shè)備的dpi:%s</string>
    <string name="screen_size" formatted="true">當(dāng)前屏幕分辨率:%d x %d</string>
    <string name="image_size" formatted="true">當(dāng)前ImageView大?。?d x %d</string>
    <string name="bitmap_size" formatted="true">當(dāng)前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,對應(yīng)資源文件夾是drawable-xxhdpi。
所以,先將圖片放置到xxhdpi中,然后再放到其他目錄中進行對比。
下面直接上測試的結(jié)果。

使用目錄 屏幕截圖(包含Bitmap大小和Imageview大?。?/th> 占用內(nèi)存
xxhdpi
hdpi
xxxhdpi

可以看到,當(dāng)圖片放到xxhdpi時,顯示圖片原始大小300x300。而放到hdpi時則是600x600,放到xxxhdpi時則是225x225。

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

當(dāng)我們使用資源id來去引用一張圖片時,Android會使用一些規(guī)則來去幫我們匹配最適合的圖片。什么叫最適合的圖片?比如我的手機屏幕密度是xxhdpi,那么drawable-xxhdpi文件夾下的圖片就是最適合的圖片。因此,當(dāng)我引用android_logo這張圖時,如果drawable-xxhdpi文件夾下有這張圖就會優(yōu)先被使用,在這種情況下,圖片是不會被縮放的。但是,如果drawable-xxhdpi文件夾下沒有這張圖時, 系統(tǒng)就會自動去其它文件夾下找這張圖了,優(yōu)先會去更高密度的文件夾下找這張圖片,我們當(dāng)前的場景就是drawable-xxxhdpi文件夾,然后發(fā)現(xiàn)這里也沒有android_logo這張圖,接下來會嘗試再找更高密度的文件夾,發(fā)現(xiàn)沒有更高密度的了,這個時候會去drawable-nodpi文件夾找這張圖,發(fā)現(xiàn)也沒有,那么就會去更低密度的文件夾下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

當(dāng)縮放時,會根據(jù)系統(tǒng)和使用的文件夾的density進行縮放。

dpi density
xxhdpi 3
hdpi 1.5
xxxhdpi 4
  • 當(dāng)使用和設(shè)備相同的density的xxhdpi文件夾時,圖片顯示原始大小300x300.
  • 當(dāng)使用hdpi的圖片時,因為該圖片在低密度下,系統(tǒng)會認它不夠大,會自動幫我們放大, 300 * 3 / 1.5 ==> 600x600
  • 當(dāng)使用xxxhdpi時,300 * 3 / 4 ==> 225x225

關(guān)于內(nèi)存的使用,android加載圖片到ImageView時,具體使用到多少內(nèi)存我不太清楚(從上面的內(nèi)存使用截圖中很難看的出來)。

但是關(guān)于bitmap使用到多少內(nèi)存倒是非常容易計算的。
默認情況下,android使用argb的方式加載圖片資源,也就是一個像素點占用4個字節(jié)。而300x300分辨率的圖片就占用了360000個字節(jié),也就是350多K的內(nèi)存。

同樣一張圖片,我們放到不同目錄下導(dǎo)致出現(xiàn)不同的內(nèi)存使用結(jié)果。如果放到hdpi中,那么bitmap使用的內(nèi)存多了4倍,整整1.3M的內(nèi)存!

那么,同事跟我說只需要一份切圖放到mipmap-xhdpi中會出現(xiàn)什么問題呢?

關(guān)于這點,其實郭霖大神說的不對“圖片資源應(yīng)該盡量放在高密度文件夾下,這樣可以節(jié)省圖片的內(nèi)存開支”
事實上,內(nèi)存的使用基本是一樣的,因為不同drawable會放置不同分辨率的圖片。
例如你在drawable-xhdpi放置30x30的icon,在drawable-xxhdpi放置45x45的icon。當(dāng)一個xxhdpi的設(shè)備去加載這張圖片時,會自動選擇45x45的圖片,占用8k左右的內(nèi)存。
如果你只在drawable-xhdpi放置30x30的icon,而不在drawable-xxhdpi中放置任何icon,那么系統(tǒng)會自動將30x30的圖片放大到45x45,也是占用8k左右的內(nèi)存。

所以,同事跟我說只需要一份切圖放到mipmap-xhdpi在內(nèi)存的使用上是沒有什么大問題的,確實能有效的控制apk包的大小。

小問題則是,在高分辨率或者低分辨率的設(shè)備中,圖片可能因為經(jīng)過縮放導(dǎo)致模糊或者失真,特別是分辨率比較大的圖片,比如引導(dǎo)頁和啟動頁的大圖。但是因為選擇的是xhdpi是市面上最普及的分辨率,所以也不會存在什么大問題,不過我更加推薦在xxhdpi中多放一份切圖,因為大多數(shù)旗艦機已經(jīng)是xxhdpi的分辨率了。

最后,切圖放在mipmap-xhdpi和drawable-xhdpi中是沒有區(qū)別的!
那些跟我說放在mipmap中比較好,只需要一份,會縮放,會更省內(nèi)存的網(wǎng)友我敲你lailai。

現(xiàn)在時間是:2018年1月12日凌晨05點57分,現(xiàn)在睡覺上班不會遲到吧……

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容