Android Bitmap 研究与思考(上篇)
轉載請標明出處:http://blog.csdn.net/zhaoyanjun6/article/details/107951273
本文出自【趙彥軍的博客】
做Android 6年來,一直都沒有對 Bitmap 做過深入研究。最近的工作需要,我認真的研究了一下Bitmap , 了卻了多年的心愿。
本次研究的東西比較多,建議先收藏,再看。
補充:關于 bitmap 壓縮問題,可以看看 Android Bitmap 研究與思考(中篇)
文章目錄
- 一:Bitmap 是什么?
- 二:bitmap 到底占用多少內存?
- 內存計算
- 三:Bitmap 尺寸怎么算?
- 實驗1:
- 實驗2
- 實驗3
- 實驗4
- 四:什么是 DPI ?
- 五:什么是 PPI ?
- 六:Android 系統 DPI 分級
- 七:不同密度下的圖片內存占用怎么計算
- 實驗1:把圖片放在 xxhdpi
- 實驗2:把圖片放在 xhdpi
- 實驗3:把圖片放在 hdpi
- 實驗4:把圖片放在 xxxhdpi
一:Bitmap 是什么?
從字面的意思上可以理解為 位圖 。
在Android中是一種存儲像素的數據結構,通過這個對象可以得到一系列的圖像屬性。還可以對圖像進行旋轉,切割,放大,縮小等操作。
我畫了一張像素圖,大家理解一下。
我們一般說說的手機分辨率 1080 * 1920 , 就代表手機屏幕橫向是 1080 個像素點,豎向是 1920 個像素點,整個手機的像素點是兩者相乘為 2073600 個像素點。
從代碼上來講 ,Bitmap 是一個 final 類,實現了 Parcelable 接口,因此不能被繼承。Bitmap只有一個構造方法,且該構造方法是私有的,所以無法通過 new 的方式來建一個 Bitmap。
在 Android 中,bitmap 只是一個存儲像素信息的對象,是內存虛構出來的。
二:bitmap 到底占用多少內存?
首先我們先來看看怎么創建一個 bitmap ,上面我們已經提到過,無法通過 new 的方式創建Bitmap 對象 。 幸運的是 Bitmap 提供了多個靜態方法。
可以看到除了createBitmap(Bitmap src) 和 createBitmap( Picture source) 這兩個方法外,其他的方法都是需要一個 Config 參數。
Bitmap.Config google官方描述為:
Possible bitmap configurations. A bitmap configuration describes how pixels are stored. This affects the quality (color depth) as well as the ability to display transparent/translucent colors.
Bitmap.Config 描述了像素的存儲方式,這會影響質量(顏色深度)以及顯示透明/半透明顏色的能力。在Bitmap.Config文檔中我們看到四個枚舉變量。
public enum Config {ALPHA_8 (1),/*** Each pixel is stored on 2 bytes and only the RGB channels are* encoded: red is stored with 5 bits of precision (32 possible* values), green is stored with 6 bits of precision (64 possible* values) and blue is stored with 5 bits of precision.** This configuration can produce slight visual artifacts depending* on the configuration of the source. For instance, without* dithering, the result might show a greenish tint. To get better* results dithering should be applied.** This configuration may be useful when using opaque bitmaps* that do not require high color fidelity.** <p>Use this formula to pack into 16 bits:</p>* <pre class="prettyprint">* short color = (R & 0x1f) << 11 | (G & 0x3f) << 5 | (B & 0x1f);* </pre>*/RGB_565 (3),/*** Each pixel is stored on 2 bytes. The three RGB color channels* and the alpha channel (translucency) are stored with a 4 bits* precision (16 possible values.)** This configuration is mostly useful if the application needs* to store translucency information but also needs to save* memory.** It is recommended to use {@link #ARGB_8888} instead of this* configuration.** Note: as of {@link android.os.Build.VERSION_CODES#KITKAT},* any bitmap created with this configuration will be created* using {@link #ARGB_8888} instead.** @deprecated Because of the poor quality of this configuration,* it is advised to use {@link #ARGB_8888} instead.*/@DeprecatedARGB_4444 (4),/*** Each pixel is stored on 4 bytes. Each channel (RGB and alpha* for translucency) is stored with 8 bits of precision (256* possible values.)** This configuration is very flexible and offers the best* quality. It should be used whenever possible.** <p>Use this formula to pack into 32 bits:</p>* <pre class="prettyprint">* int color = (A & 0xff) << 24 | (B & 0xff) << 16 | (G & 0xff) << 8 | (R & 0xff);* </pre>*/ARGB_8888 (5),/*** Each pixels is stored on 8 bytes. Each channel (RGB and alpha* for translucency) is stored as a* {@link android.util.Half half-precision floating point value}.** This configuration is particularly suited for wide-gamut and* HDR content.** <p>Use this formula to pack into 64 bits:</p>* <pre class="prettyprint">* long color = (A & 0xffff) << 48 | (B & 0xffff) << 32 | (G & 0xffff) << 16 | (R & 0xffff);* </pre>*/RGBA_F16 (6),/*** Special configuration, when bitmap is stored only in graphic memory.* Bitmaps in this configuration are always immutable.** It is optimal for cases, when the only operation with the bitmap is to draw it on a* screen.*/HARDWARE (7);final int nativeInt;private static Config sConfigs[] = {null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE};Config(int ni) {this.nativeInt = ni;}static Config nativeToConfig(int ni) {return sConfigs[ni];}}- ALPHA_8 :
說白了就是:這種模式每個像素只存儲一個透明度值,不會存儲其他顏色信息。所以我們在創建蒙版的時候,推薦使用這種模式。每個像素占 8 位,也就是 1 個字節。
- ARGB_4444 :
即A=4,R=4,G=4,B=4,那么一個像素點占4+4+4+4=16位 , 也就是每個像素占 2 個字節。
- ARGB_8888 :
即A=8,R=8,G=8,B=8,那么一個像素點占8+8+8+8=32位, 也就是一個像素占 4 個字節。
- RGB_565 :
即R=5,G=6,B=5,沒有透明度,那么一個像素點占5+6+5=16位, 也就是一個像素占 2 個字節。
內存計算
我們知道了,每種模式下,每個像素所占用的內存,就能很清楚的算清楚,每個bitmap 所占用的內存。比如:
Bitmap bitmap = Bitmap.createBitmap(1024,1024, Bitmap.Config.ARGB_8888);我們創建了一個 bitmap ,寬 1024 個像素,高 1024 個像素,模式 ARGB_8888 。所以我們很容易算出:
- 這個bitmap 總共像素總量為:1024 x 1024
- 每個像素占用的內存為 4 個 byte
- 整個bitmap 占用內存為 :1024 x 1024 x 4 , 單位字節 byte 。 換算為 (1024 x 4 ) kb = 4 M
最后算出這個bigmap 占用內存為 4 M 。
總結:
-
在 bitmap 存儲模式相同的情況下,尺寸越大,占用內存越大。因為尺寸越大,bigmap 總像素點越多。
-
在 bitmap 尺寸相同的情況下,ARGB_8888 占用內存 > ARGB_4444 占用內存 = RGB_565 占用內存 > ARGB_4444 占用內存。
三:Bitmap 尺寸怎么算?
在第二部分,我們知道了 bitmap 占用的內存 = bitmap 總像素個數 x 單個像素占用的內存。
其中,單個像素占用的內存由存儲模式決定,比如 Config.ARGB_8888 。
而 bitmap 的總像素 = bitmap 寬度 x bitmap 高度 。
現在我們要弄清楚 bitmap 尺寸怎么計算?
首先上一段代碼,計算ImageView中加載圖片的具體尺寸和內存占用大小。
import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.Log; import android.widget.ImageView;/*** @author yanjun.zhao* @time 2020/8/13 7:51 PM* @desc*/ class Util {/**** 計算ImageView中加載圖片的具體尺寸和內存占用大小* @param imageView*/public static void calculateBitmapInfo(ImageView imageView) {Drawable drawable = imageView.getDrawable();if (drawable != null) {BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;Bitmap bitmap = bitmapDrawable.getBitmap();Log.d("bitmap--", " bitmap width = " + bitmap.getWidth() + " bitmap height = " + bitmap.getHeight());Log.d("bitmap--", " memory usage = " + bitmap.getAllocationByteCount());/**bitmap.getByteCount()方法不再使用*/} else {Log.d("bitmap--", "drawable is null!");}} }實驗1:
我們做個試驗,現在布局中定義個 ImageView , 尺寸是 100 dp x 100 dp
<ImageViewandroid:id="@+id/image"android:layout_width="100dp"android:layout_height="100dp" />給 imageView 設置一個圖片,名字是 image.png , 尺寸是 300 x 300 。
<ImageViewandroid:id="@+id/image"android:src="@drawable/image"android:layout_width="100dp"android:layout_height="100dp" />然后輸出 bitmap 寬高和占用內存。
import androidx.appcompat.app.AppCompatActivity import com.example.myview.util.Utils import kotlinx.android.synthetic.main.activity_main.*class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)//計算bitmap 寬高和占用內存Util.calculateBitmapInfo(image)} }在 紅米4 手機上輸出:
bitmap--: bitmap width = 1200 bitmap height = 1200 bitmap--: memory usage = 5760000在我的雜牌手機上輸出:
bitmap--: bitmap width = 600 bitmap height = 600 bitmap--: memory usage = 1440000至此,我們可以得出結論:
- 使用相同尺寸的圖片放在 drawable 目錄下,用 ImageView 加載出來。在不同的手機上,獲取的 bitmap 寬高是不一樣的,bitmap 所占用的內存也是不一樣的。
查閱 ImageView 源碼,發現 ImageView中圖片是以 Bitmap 形式保存在內存中;查閱 Bitmap 源碼,發現圖像在內存中的實際 bitmap 尺寸和圖像的原始尺寸(withPixel * heightPixel),資源文件像素密度(sourcedensity)以及目標手機的像素密度(targetDensity)有密不可分的關系。
其中 sourceDensity>>1 部分是為了對 targetDensity/sourceDensity 進行四舍五入
綜上,
- 結論1: ImageView中顯示圖像對內存的占用與原始圖像尺寸,資源文件的dpi,以及實際設備的dpi有密切的關系,與圖像在UI上實際顯示的尺寸無關。只放置單 dpi 資源文件,對不同設備加載過程中的內存占用無影響。
實驗2
測試方案:將尺寸為圖片A(尺寸60*60 大小2.02K)放入drawable和drawable-xxhdpi文件夾,圖片顯示尺寸采用wrap_content,分別用mate 9手機進行測試;
測試結果:內存占用分別為129600Byte和14400Byte,圖片在ImageView中的bitmap尺寸為180 * 180和 60 * 60;
結果分析:相同的圖片放在不同的 drawable 目錄下,bitmap 寬高不一樣,bitmap 占用的內存也不一樣。內存占用與圖片的原始尺寸沒有關系,與 bitmap尺寸有密切的關系。
實驗3
測試方案:將尺寸為圖片A(尺寸60 x 60 ,大小2.02K)放入drawable-xxhdpi文件夾,圖片顯示尺寸設置為30dp * 30dp和 60dp * 60dp,分別用mate 9手機進行測試;
測試結果:內存占用均為14400Byte,bitmap尺寸均為60*60;
結果分析:說明內存占用與圖片的實際顯示尺寸沒有關系。
實驗4
測試方案:將尺寸為圖片A(尺寸 60 x 60 大小 2.02K ),圖片B(尺寸 60 x 60 ,大小1.63K),將圖片均放入drawable-xxhdpi文件夾,圖片顯示尺寸采用wrap_content,用華為mate 9(xxhdpi)手機進行測試;
測試結果:二者內存占用均為14400Byte,bitmap尺寸為 60 x 60;
結果分析:說明內存占用單獨與圖片原始大小沒有關系。
四:什么是 DPI ?
先放一個維基百科 鏈接 DPI
DPI(英語:Dots Per Inch,每英寸點數)是一個量度單位,用于點陣數字圖像,意思是指每一英寸長度中,取樣或可顯示或輸出點的數目。如:打印機輸出可達600DPI的分辨率,表示打印機可以在每一平方英寸的面積中可以輸出600X600=360000個輸出點。
打印機所設置之分辨率的DPI值越高,印出的圖像會越精細。打印機通常可以調校分辨率。例如撞針打印機,分辨率通常是60至90 DPI。噴墨打印機則可達1200 DPI,甚至9600 DPI。激光打印機則有600至1200 DPI。
一般顯示器為96 DPI,印刷所需位圖的DPI數則視印刷網線數而定。一般150線印刷質量需要350 DPI的位圖。而這里的D(dot)就是像素(pixel)。
五:什么是 PPI ?
每英寸像素(英語:Pixels Per Inch,縮寫:PPI),又被稱為像素密度,是一個表示打印圖像或顯示器單位面積上像素數量的指數。一般用來計量電腦顯示器,電視機和手持電子設備屏幕的精細程度。通常情況下,每英寸像素值越高,屏幕能顯示的圖像也越精細。
電腦與手機屏幕的每英寸像素值取決于尺寸和分辨率,通常指的就是每英寸上的像素點數。同樣的一臺顯示器,如果分辨率設置的不同,像素點數也不同。分辨率越高,每英寸像素值也越高。
六:Android 系統 DPI 分級
根據屏幕每英寸像素值的不同,Android系統的開發者將平板電腦和手機的屏幕分成五類:
隨著屏幕技術的發展,出現了比 xxhdpi 更高密度的屏幕 xxxhdpi
| XXXHDPI | 超超高像素密度 | 每英寸大約640像素 (192 x 192 px ) |
七:不同密度下的圖片內存占用怎么計算
往下閱讀前,可以首先閱讀 郭霖的博客:
Android drawable微技巧,你所不知道的drawable的那些細節
在郭神的文章中,討論了圖片放在不同的 dpi 目錄下,ImageView 會被縮放,ImageView 的寬高會改變。但是文章中沒有談論的是 ImageView 寬高改變了,ImageView 中的 bitmap 寬高會不會改變,因為 bitmap 的寬高會直接影響到所占用內存的大小。
我們先來做個實驗,首先計算出當前手機屏幕密度,計算方法是:
var phoneDpi = resources.displayMetrics.densityDpi計算的結果是 480 ,可以看出來,我的手機屏幕密是 超高像素密度 , 對應的是 xxhdpi
我現在有一張圖片 image.png,尺寸是300 x 300 。
條件:ImageView 的寬高是 wrap_content
<ImageViewandroid:id="@+id/image"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/image" />實驗1:把圖片放在 xxhdpi
獲取 bitmap 的寬高和占用內存
bitmap_width = 300 bitmap_height = 300 memory_usage = 360000圖片放在 xxhdpi , 屏幕上顯示的是原圖的大小,原來的尺寸是多少,在屏幕上就會顯示多少。獲取的 bitmap 寬高也是原圖的寬高,尺寸沒有發生變化,所占內存也沒有變化
實驗2:把圖片放在 xhdpi
獲取 bitmap 的寬高和占用內存
bitmap_width = 300 bitmap_height = 300 memory_usage = 360000圖片放在 xhdpi 獲取的 bitmap 寬高 和 xxhdpi 一樣 ,內存占用也一樣。
實驗3:把圖片放在 hdpi
獲取 bitmap 的寬高和占用內存
bitmap_width = 300 bitmap_height = 300 memory_usage = 360000圖片放在 hdpi 獲取的 bitmap 寬高 和 xxhdpi 、 xhdpi 一樣 ,內存占用也一樣。
實驗4:把圖片放在 xxxhdpi
獲取 bitmap 的寬高和占用內存
bitmap_width = 225 bitmap_height = 225 memory_usage = 202500圖片放在 xxxhdpi 獲取的 bitmap 寬高都比原圖要小,占用內存也小了。小了多少是怎么計算的。
bitmap寬度 = (手機屏幕密度 / 圖片所在目錄像素密度) * 原圖寬度
結合到本例就是:
手機屏幕密度 = 480 圖片放在 xxxhdpi , 像素密度 = 640 原圖寬度 = 300 帶入公式就是: bitmap 寬度 = (480 / 640 ) * 300 = 225 bitmap的模式是:Config.ARGB_888,每個像素占用 4 個bit bitmap占用內存:225 x 225 x 4 = 202500同理,bitmap 的高度也是這樣計算的。
那么問題來了,為什么放在 xxxhdpi 目錄下,bitmap 的尺寸發生了變化,而放在 hdpi 、xhdpi bitmap 的尺寸卻沒有發生變化 ???
總結
以上是生活随笔為你收集整理的Android Bitmap 研究与思考(上篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android RxJava 3.x 使
- 下一篇: Android RecyclerView