drawable和mipmap目錄

原文鏈接:https://www.jianshu.com/p/f7dc272b3469

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分,現在睡覺上班不會遲到吧……



作者:AItsuki
鏈接:https://www.jianshu.com/p/f7dc272b3469
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章