因爲最近公司對圖像這一塊作文章比較多,而本身對於Bitmap的認識確實也比較淺顯,所以花些功夫研究一下Bitmap的使用以及原理,寫下該篇文章記錄一下學習過程。html
關於系統Graphics的研究須要擱置一段時間了,緣由是看了老羅的文章,發現本身的表達能力真是相差甚大,爲了避免誤人子弟,打算熟讀老羅的分析後在進行概括總結。java
文章主要圍繞着以下幾個問題展開分析探討:android
Bitmap是由像素(Pixel)組成的,像素是位圖最小的信息單元,存儲在圖像柵格中。 每一個像素都具備特定的位置和顏色值。按從左到右、從上到下的順序來記錄圖像中每個像素的信息,如:像素在屏幕上的位置、像素的顏色等。位圖圖像質量是由單位長度內像素的多少來決定的。單位長度內像素越多,分辨率越高,圖像的效果越好。位圖也稱爲「位圖圖像」「點陣圖像」「數據圖像」「數碼圖像」。一個像素點能夠由1,4,16,24,32bit來表示,像素點的色彩越豐富,天然圖像的效果就越好了。c++
上面的介紹引用自百度百科,位圖文件(注意是位圖文件)的後綴通常是**.bmp或者.dib**。位圖概念來自於Windows,是Windows的標準圖形文件,咱們在Windows中看到的默認背景圖其實就是一張位圖文件,有興趣的朋友能夠看看自家Windows電腦的背景圖。一個位圖存儲文件的結構以下所示:git
具體的結構解析就不深刻了,畢竟術業有專攻,咱們只要知道概念便可,詳細的能夠查閱該篇文章。程序員
位圖文件不等於位圖(Bitmap)github
接下來介紹兩個概念:位深以及色深web
下面貼個網上的例子理解一下這兩個概念:算法
100像素x100像素的圖片, 使用ARGB_8888,因此色深32位,保存時選擇位深爲24位,則在內存中所佔大小爲:100 x100 x (32 / 8)Byte,而在文件所佔大小爲** 100 x100 x( 24/ 8 ) x 壓縮效率 Byte**。編程
咱們能夠寫個代碼驗證看看是不是這樣的,直接加載一張圖片出來試下看看:
private void testCompress() { try { File file = new File(getInnerSDCardPath() + File.separator + "001LQK0Czy74OrXDiVLdd&690.jpeg"); Log.e("compress", "文件大小=" + file.length()); Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
最後加載後的效果以下:
能夠看到加載到內存後確實變成了32bit的圖像,而加載以前就是24bit。
ok,接下來講說Bitmap和JPG,PNG,WEBP區別。其實Bitmap通俗意義上講就是一張圖片在內存中表現的完整形式,裏面包含的都是像素點,而bmp,jpg,png,webp則是Bitmap在硬盤存儲的格式,能夠理解成一個壓縮包的概念,因此存儲下來的文件相比於內存展示的會小不少。
上面介紹中提到了有損以及無損,這兩個的概念以下:
在Android中解析獲取Bitmap的方式存在於BitmapFactory.java
工廠類當中,該類中提供瞭解析文件,解析流,解析Resource以及解析Asset中圖片文件的方式,具體的使用方法以下:
這裏對Options參數進行一個說明,Options對象可以支持對圖片進行一些預處理的操做,其內部變量以下所示:
public static class Options { public Options() { inDither = false; inScaled = true; //默認容許縮放圖像 inPremultiplied = true; } public Bitmap inBitmap; //涉及重用Bitmap相關知識 //返回的Bitmap是否可變(可操做) public boolean inMutable; //只獲取圖片相關參數(如寬高)不加載圖片 public boolean inJustDecodeBounds; //設置採樣率 public int inSampleSize; //Bitmap.Config的四種枚舉類型,默認使用Bitmap.Config.ARGB_8888 public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888; //若是被設置爲true(默認值),在圖片被顯示出來以前各個顏色通道會被事先乘以它的alpha值,若是圖片是由系統直接繪製或者是由Canvas繪製,這個值不該該被設置爲false,不然會發生RuntimeException public boolean inPremultiplied; //處理圖片抖動,若是設置爲true,則若是圖像存在抖動,就處理抖動,設置爲false則無論抖動問題 public boolean inDither; //原圖像的像素密度,跟縮放inScale有關 public int inDensity; //目標圖片像素密度,跟縮放inScale有關 public int inTargetDensity; //屏幕像素密度 public int inScreenDensity; //是否容許縮放圖像 public boolean inScaled; // 5.0以上的版本標記過期了 public boolean inPurgeable; //// 4.4.4以上版本忽略 public boolean inInputShareable; //是否支持Android自己處理優化圖片,從而加載更高質量的圖片 public boolean inPreferQualityOverSpeed; //圖片寬度 public int outWidth; //圖片高度 public int outHeight; //返回圖片mimetype,可能爲null public String outMimeType; //圖片解碼的臨時存儲空間,默認值爲16K public byte[] inTempStorage; .. }
這裏先須要介紹的是Bitmap.Config,有6個值:
ARGB指的是一種色彩模式,裏面A表明Alpha,R表示red,G表示green,B表示blue
上面能夠看出RGB_565相比於ARGB_8888來講,內存佔用會減小一半,可是其捨棄了透明度,同時三色值也有部分損失,雖然圖片失真度很小。而ALPHA_8使用情景有限,ARGB_4444官方不推薦使用,因此本文研究的着重點就在ARGB_8888以及RGB_565上,當時具體使用策略按需而定,如圖片庫Glide就是使用RGB_565來減小Bitmap的內存佔用。下面咱們從代碼的角度驗證一下正確性:
原圖大小爲:寬x高=690x975
File file = new File(getInnerSDCardPath() + File.separator + "001LQK0Czy74OrXDiVLdd&690.jpeg"); Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; Bitmap bitmap2 = BitmapFactory.decodeFile(file.getAbsolutePath(), options); Log.e("compress", "bitmap1內存佔用大小=" + bitmap1.getByteCount()+" bitmap2內存佔用大小="+bitmap2.getByteCount());
打印Log的結果以下:
bitmap1內存佔用大小=2691000 bitmap2內存佔用大小=1345500
能夠看到,對於同一張圖片而言,RGB_565確實圖片內存佔用減小了一半,所以在對圖片質量要求不是特別高的狀況下,如信息流的小圖,其實使用該模式是很是不錯的。
接下來再看下inBitmap參數,在Android3.0版本後,該參數就在源碼中加上了,該參數的意義在於複用當前Bitmap所申請的內存空間,以優化釋放舊Bitmap內存以及從新申請Bitmap內存致使的性能損耗。這裏討論的版本爲Android4.4.4之後,在該版本之後,使用該參數須要知足以下條件:
知足了上面兩個條件,就能夠從新複用內存,而不須要額外申請了,具體的使用教程移步Andorid官方教程: Managing Bitmap Memory,這裏就不深刻了。
關於decodeFile(...)方式加載出來的Bitmap本質上是調用decodeStream(...)
進行的,上面代碼再貼下:
File file = new File(getInnerSDCardPath() + File.separator + "001LQK0Czy74OrXDiVLdd&690.jpeg"); Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
解碼模式用ARGB_8888,最後佔用的內存大小是2691000,解析後得到的寬x高=690x975,即2691000=690x975x4,發現加載出來的圖片確實是原圖大小,那麼若是加上Options參數的設置呢,上面分析Options對象的構成,咱們能夠發現可能影響內存大小的參數會有inScaled,inScreenDensity,inDensity等等,那麼怎麼去驗證呢?最簡單的方法就是看Native源碼,因此這裏跟蹤一下源碼,而後在用代碼確認一遍。decodeFile(...)
最終會調用到以下方法:
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) { byte [] tempStorage = null; if (opts != null) tempStorage = opts.inTempStorage;//使用解碼臨時緩存區,默認爲16K if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE]; return nativeDecodeStream(is, tempStorage, outPadding, opts); }
Native調用在BitmapFactory.cpp中:
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) { return nativeDecodeStreamScaled(env, clazz, is, storage, padding, options, false, 1.0f); } static jobject nativeDecodeStreamScaled(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options, jboolean applyScale, jfloat scale) { jobject bitmap = NULL; //建立SkStream流 SkStream* stream = CreateJavaInputStreamAdaptor(env, is, storage, 0); if (stream) { // for now we don't allow purgeable with java inputstreams bitmap = doDecode(env, stream, padding, options, false, false, applyScale, scale); stream->unref(); } return bitmap; }
到這能夠看見Skia的影子了,Skia 是 Google 一個底層的圖形、圖像、動畫、 SVG 、文本等多方面的圖形庫,是 Android 中圖形系統的引擎,主要支持Android的2D圖像操做,3D天然就是Opengl es了。關於Skia自己我也瞭解的不是不少,可是這裏並不須要用到相關知識,邏輯仍是可以理清,所以咱們繼續跟蹤doDecode(...)
:
//4.4w版本代碼 static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options, bool allowPurgeable, bool forcePurgeable = false) { int sampleSize = 1; //1.解碼的模式,主要有兩個,一個是kDecodeBounds_Mode,該模式下只返回Bitmap的寬高以及一些Config參數; //另一個是kDecodePixels_Mode,返回完整的圖片以及相關信息 SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode; //Java層Config對應native層的Config,能夠看到默認是使用ARGB_8888來處理圖片 SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config; bool doDither = true; bool isMutable = false; float scale = 1.0f; ////isPurgeable=true bool isPurgeable = forcePurgeable || (allowPurgeable && optionsPurgeable(env, options)); bool preferQualityOverSpeed = false; bool requireUnpremultiplied = false; jobject javaBitmap = NULL; if (options != NULL) { sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID); //能夠看到這步,若是Java層設置了inJustDecodeBounds,那麼使用kDecodeBounds_Mode模式,只獲取寬高以及一些信息,而不是去加載圖片 if (optionsJustBounds(env, options)) { mode = SkImageDecoder::kDecodeBounds_Mode; } // initialize these, in case we fail later on env->SetIntField(options, gOptions_widthFieldID, -1); env->SetIntField(options, gOptions_heightFieldID, -1); env->SetObjectField(options, gOptions_mimeFieldID, 0); jobject jconfig = env->GetObjectField(options, gOptions_configFieldID); prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig); isMutable = env->GetBooleanField(options, gOptions_mutableFieldID); doDither = env->GetBooleanField(options, gOptions_ditherFieldID); preferQualityOverSpeed = env->GetBooleanField(options, gOptions_preferQualityOverSpeedFieldID); requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID); //獲取可重用的Bitmap,即當前Bitmap設置的inBitmap參數不爲空狀況下用到 javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); //能夠設置scale if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } } //這裏情境下爲false const bool willScale = scale != 1.0f; //這裏從新設置了isPurgeable參數,若是不在縮放狀況下,那麼isPurgeable恆等於false,當前狀況下=false isPurgeable &= !willScale; ... SkBitmap* outputBitmap = NULL; unsigned int existingBufferSize = 0; if (javaBitmap != NULL) { outputBitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID); if (outputBitmap->isImmutable()) { ALOGW("Unable to reuse an immutable bitmap as an image decoder target."); javaBitmap = NULL; outputBitmap = NULL; } else { existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap); } } SkAutoTDelete<SkBitmap> adb(outputBitmap == NULL ? new SkBitmap : NULL); if (outputBitmap == NULL) outputBitmap = adb.get(); NinePatchPeeker peeker(decoder); decoder->setPeeker(&peeker); SkImageDecoder::Mode decodeMode = isPurgeable ? SkImageDecoder::kDecodeBounds_Mode : mode; JavaPixelAllocator javaAllocator(env); RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize); ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize); SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ? (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator; if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) { if (!willScale) { // If the java allocator is being used to allocate the pixel memory, the decoder // need not write zeroes, since the memory is initialized to 0. decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator); decoder->setAllocator(outputAllocator); } else if (javaBitmap != NULL) { // check for eventual scaled bounds at allocation time, so we don't decode the bitmap // only to find the scaled result too large to fit in the allocation decoder->setAllocator(&scaleCheckingAllocator); } } ... if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) { return nullObjectReturn("gOptions_mCancelID"); } SkBitmap decodingBitmap; if (!decoder->decode(stream, &decodingBitmap, prefConfig, decodeMode)) { return nullObjectReturn("decoder->decode returned false"); } //獲取寬高 int scaledWidth = decodingBitmap.width(); int scaledHeight = decodingBitmap.height(); if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } // update options (if any) if (options != NULL) { env->SetIntField(options, gOptions_widthFieldID, scaledWidth); env->SetIntField(options, gOptions_heightFieldID, scaledHeight); env->SetObjectField(options, gOptions_mimeFieldID, getMimeTypeString(env, decoder->getFormat())); } //inJustDecodeBounds=true則直接返回null,不對圖片進行解析加載 if (mode == SkImageDecoder::kDecodeBounds_Mode) { return NULL; } ... if (willScale) { //經過畫布的方式縮放Bimap const float sx = scaledWidth / float(decodingBitmap.width()); const float sy = scaledHeight / float(decodingBitmap.height()); SkBitmap::Config config = configForScaledOutput(decodingBitmap.config()); outputBitmap->setConfig(config, scaledWidth, scaledHeight, 0, decodingBitmap.alphaType()); if (!outputBitmap->allocPixels(outputAllocator, NULL)) { return nullObjectReturn("allocation failed for scaled bitmap"); } // If outputBitmap's pixels are newly allocated by Java, there is no need // to erase to 0, since the pixels were initialized to 0. if (outputAllocator != &javaAllocator) { outputBitmap->eraseColor(0); } SkPaint paint; paint.setFilterLevel(SkPaint::kLow_FilterLevel); SkCanvas canvas(*outputBitmap); canvas.scale(sx, sy); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } else { outputBitmap->swap(decodingBitmap); } ... SkPixelRef* pr; if (isPurgeable) { pr = installPixelRef(outputBitmap, stream, sampleSize, doDither); } else { // if we get here, we're in kDecodePixels_Mode and will therefore // already have a pixelref installed. pr = outputBitmap->pixelRef(); } if (pr == NULL) { return nullObjectReturn("Got null SkPixelRef"); } if (!isMutable && javaBitmap == NULL) { // promise we will never change our pixels (great for sharing and pictures) pr->setImmutable(); } // detach bitmap from its autodeleter, since we want to own it now adb.detach(); //若是有重用的Bitmap,則返回 if (javaBitmap != NULL) { bool isPremultiplied = !requireUnpremultiplied; GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap, isPremultiplied); outputBitmap->notifyPixelsChanged(); // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } int bitmapCreateFlags = 0x0; if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable; if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied; // 建立新Bitmap return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(), bitmapCreateFlags, ninePatchChunk, layoutBounds, -1); }
上面代碼中有兩個很是重要的參數:willScale,isPurgeable,這兩個參數直接或間接影響圖片的內存佔用以及管理,willScale表示圖片是否須要縮放操做,而isPurgeable則表明圖片的內存管理方式,不設置對應inDensity,inTargetDensity,inScreenDensity,willScale都是false,不涉及到縮放,因此加載出來的圖片就是原圖片大小,內存天然也是無變化。而isPurgeable的值在當前條件下則爲false,若是爲True的話那麼會走到installPixelRef(...)
方法中:
static SkPixelRef* installPixelRef(SkBitmap* bitmap, SkStream* stream, int sampleSize, bool ditherImage) { SkImageRef* pr; // only use ashmem for large images, since mmaps come at a price if (bitmap->getSize() >= 32 * 1024) { pr = new SkImageRef_ashmem(stream, bitmap->config(), sampleSize); } else { pr = new SkImageRef_GlobalPool(stream, bitmap->config(), sampleSize); } pr->setDitherImage(ditherImage); bitmap->setPixelRef(pr)->unref(); pr->isOpaque(bitmap); return pr; }
經過查閱資料可知若是圖片大小(佔用內存)大於32×1024=32K,那麼就使用Ashmem,不然就就放入一個引用池中。若是圖片不大,直接放到native層內存中,讀取方便且迅速。若是圖片過大,放到native層內存也就不合理了,否則圖片一多,native層內存很難管理。可是若是使用Ashmem匿名共享內存方式,寫入到設備文件中,須要時再讀取就能避免很大的內存消耗了。
不過這是針對於5.0如下的版本使用,在5.0及以上的版本被標記爲Deprecated,即便inPurgeable=true,也不會再使用Ashmem內存存放圖片,而是直接放到了Java Heap中,簡而言之就是inPurgeable屬性被忽略了(下面在分析decodeResource(...)時候使用6.0版原本分析,能夠看到isPurgeable參數已經消失了)。
在查閱相關資料發現Andorid O版本好像針對Bitmap的分配策略又不一樣了,詳細的能夠參考這篇文章,這裏我並無查看源碼驗證,所以僅供參考吧
由於Android系統從5.0開始對Java Heap內存管理作了大幅的優化。和以往不一樣的是,對象再也不統一管理和回收,而是在Java Heap中單獨開闢了一塊區域用來存放大型對象,好比Bitmap這種,同時這塊內存區域的垃圾回收機制也是和其它區域徹底分開的,這樣就使得OOM的機率大幅下降,並且讀取效率更高。因此,用Ashmem來存儲圖片就徹底沒有必要了,況且Ashmem還會致使性能問題。這裏咱們到時候看下再處理decodeResource(...)
時候的邏輯。
對於經過decodeFile(...)
加載Bitmap的流程分析完畢了,總結一下在使用decodeFile(...)
的時候,不設置對應inDensity,inTargetDensity,inScreenDensity,系統是不會對Bitmap進行縮放操做,加載的是原圖。若是設置了inDensity,inTargetDensity,inScreenDensity,而且知足縮放條件,則走的流程跟decodeResource(...)
一致。
這裏記錄一下工做期間遇到的一個問題,經過decodeFile(...)
加載出Bitmap後,再把Bitmap從新保存發現舊圖片和新圖片大小是不同的:
try { File file = new File(Environment.getExternalStorageDirectory() + File.separator + "11.jpeg"); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPurgeable = true; Bitmap bitmap1 = BitmapFactory.decodeFile(file.getAbsolutePath()); File comFile = new File(Environment.getExternalStorageDirectory() + File.separator + "11_new.jpeg"); if (!comFile.exists()) { comFile.createNewFile(); } OutputStream stream = new FileOutputStream(comFile); bitmap1.compress(Bitmap.CompressFormat.JPEG, 100 , stream); stream.flush(); stream.close(); } catch (Exception e) { }
能夠發現新的圖片莫名其妙增長了100多kb的大小,百思不得其解,這裏猜想是不是Android將Bitmap轉換成JPG的算法,因此再嘗試經過11_new.jpg文件再從新生成一張圖片,獲得的結果以下:
發現圖片大小又變大了??這是爲啥,查閱了一下谷歌發現沒有對應的答案,這裏只能猜想是Android生成圖片的算法的緣由吧,暫且作個筆記,往後弄明白了再作回答吧。若是有哪位同行知道的,但願指點一下迷津蛤。
上面介紹了經過文件加載圖片的狀況,在Android中也能夠直接加載drawable或者mipmap文件夾下的圖片,而經過這種方式加載的圖片大小可能不一致,最直觀的就是放在drawable-hdpi和drawable-mdpi文件夾下的相同圖片,加載出來是兩張大小不同的圖片。關於各個文件夾的含義這裏就不解釋了,若是對這些個概念比較模糊,能夠查看一下這篇文章,這裏就盜用一張圖簡單看下對應各個drawable文件夾所表明的屏幕密度:
而各個mipmap文件夾中官方意見是存放應用icon(進行內存優化),其餘的圖片資源仍然存放在drawable文件夾當中,因此在這裏就不探討mipmap文件夾了。
首先咱們能夠經過以下代碼得到手機屏幕的寬高密度:
float densityDpi = getResources().getDisplayMetrics().densityDpi;
獲得的結果是屏幕密度=480,也就是說正常不進行縮放的圖片應該放在xxhdpi文件夾下。下面測試一下同一張圖片(72x72)放在ldpi,mdpi,hdpi,xhdpi,xxhdpi文件夾下面的內存佔用狀況:
ldpi中的圖片 寬x高=288x288 內存大小=324.0kb mdpi中的圖片 寬x高=216x216 內存大小=182.25kb hdpi中的圖片 寬x高=144x144 內存大小=81.0kb xhdpi中的圖片 寬x高=108x108 內存大小=45.5625kb xxhdpi中的圖片 寬x高=72x72 內存大小=20.25kb
能夠看到同一張圖片放在不一樣的drawable在編程Bitmap後寬高跟內存都變了,只有在xxhdpi中才顯示原圖,爲何會這樣呢?Android對於不一樣drawable加載的邏輯是這樣的:
首先先尋找手機密度匹配的drawable文件夾,這裏個人手機匹配的是xxhdpi文件夾,若是沒有則先向高密度的文件夾尋找,即xxxdpi,一直尋找到最高密度文件夾,若是依然沒有則到drawable-nodpi文件夾找這張圖,發現也沒有,那麼就會去更低密度 的文件夾下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi的順序。
若是在比當前屏幕密度高的文件夾中找到了,Android認爲這是一張相對於屏幕密度所屬文件更大的圖,因此要進行縮小,同理若是在相比之下低密度的文件夾中找到了,則須要進行放大操做,縮放因子等於當前屏幕密度所在文件夾的密度除以圖片所在文件夾的密度,拿xhdpi舉例,縮放比例等於480/320=1.5,因此xhdpi中加載出來的寬高等於108x108。
若是以爲講述不清能夠看下郭霖大神這篇博客,講的很好。
上面是結論,那麼天然要在源碼中尋找一下立據才符合程序員的個性,這裏以圖片放在xhdpi文件夾下爲前提,在調用decodeResource(...)
在Java層會最終調用到以下方法中:
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(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); } public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, @Nullable Options opts) { ... bm = nativeDecodeAsset(asset, outPadding, opts); ... return bm; }
這裏會首先肯定圖像位於文件夾的密度,即設置opts.inDensity等於xhdpi的密度值,也就是說等於320,opts.inTargetDensity則爲屏幕密度480。接着走到native方法中:
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jint native_asset, jobject padding, jobject options) { return nativeDecodeAssetScaled(env, clazz, native_asset, padding, options, false, 1.0f); } static jobject nativeDecodeAssetScaled(JNIEnv* env, jobject clazz, jint native_asset, jobject padding, jobject options, jboolean applyScale, jfloat scale) { SkStream* stream; Asset* asset = reinterpret_cast<Asset*>(native_asset); //false bool forcePurgeable = optionsPurgeable(env, options); ... SkAutoUnref aur(stream); //applyScale=false,scale=1.0f,forcePurgeable=false return doDecode(env, stream, padding, options, true, forcePurgeable, applyScale, scale); }
這裏仍是調用到了doDecode(...)
方法中,這裏咱們貼出6.0版本代碼來查看吧,不然跟不上時代了(雖然已經跟不上了):
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) { int sampleSize = 1; //1.解碼的模式,主要有兩個,一個是kDecodeBounds_Mode,該模式下只返回Bitmap的寬高以及一些Config參數; //另一個是kDecodePixels_Mode,返回完整的圖片以及相關信息 SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode; SkColorType prefColorType = kN32_SkColorType; bool doDither = true; bool isMutable = false; float scale = 1.0f; bool preferQualityOverSpeed = false; bool requireUnpremultiplied = false; jobject javaBitmap = NULL; if (options != NULL) { //獲取採樣率 sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID); //能夠看到這步,若是Java層設置了inJustDecodeBounds,那麼使用kDecodeBounds_Mode模式,只獲取寬高以及一些信息,而不是去加載圖片 if (optionsJustBounds(env, options)) { decodeMode = SkImageDecoder::kDecodeBounds_Mode; } // initialize these, in case we fail later on env->SetIntField(options, gOptions_widthFieldID, -1); env->SetIntField(options, gOptions_heightFieldID, -1); env->SetObjectField(options, gOptions_mimeFieldID, 0); jobject jconfig = env->GetObjectField(options, gOptions_configFieldID); prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig); isMutable = env->GetBooleanField(options, gOptions_mutableFieldID); doDither = env->GetBooleanField(options, gOptions_ditherFieldID); preferQualityOverSpeed = env->GetBooleanField(options, gOptions_preferQualityOverSpeedFieldID); requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID); //若是設置了inBitmap,則讀取對應Bitmap javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); //獲取Java層inScaled是否支持縮放 if (env->GetBooleanField(options, gOptions_scaledFieldID)) { //獲取Java層的三個密度 const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { //1. 計算縮放比率 scale = (float) targetDensity / density; } } } //true const bool willScale = scale != 1.0f; SkImageDecoder* decoder = SkImageDecoder::Factory(stream); if (decoder == NULL) { return nullObjectReturn("SkImageDecoder::Factory returned null"); } decoder->setSampleSize(sampleSize); decoder->setDitherImage(doDither); decoder->setPreferQualityOverSpeed(preferQualityOverSpeed); decoder->setRequireUnpremultipliedColors(requireUnpremultiplied); android::Bitmap* reuseBitmap = nullptr; unsigned int existingBufferSize = 0; if (javaBitmap != NULL) { reuseBitmap = GraphicsJNI::getBitmap(env, javaBitmap); if (reuseBitmap->peekAtPixelRef()->isImmutable()) { ALOGW("Unable to reuse an immutable bitmap as an image decoder target."); javaBitmap = NULL; reuseBitmap = nullptr; } else { existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap); } } NinePatchPeeker peeker(decoder); decoder->setPeeker(&peeker); JavaPixelAllocator javaAllocator(env); RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize); ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize); SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ? (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator; if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) { if (!willScale) { // If the java allocator is being used to allocate the pixel memory, the decoder // need not write zeroes, since the memory is initialized to 0. decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator); decoder->setAllocator(outputAllocator); } else if (javaBitmap != NULL) { // check for eventual scaled bounds at allocation time, so we don't decode the bitmap // only to find the scaled result too large to fit in the allocation decoder->setAllocator(&scaleCheckingAllocator); } } // Only setup the decoder to be deleted after its stack-based, refcounted // components (allocators, peekers, etc) are declared. This prevents RefCnt // asserts from firing due to the order objects are deleted from the stack. SkAutoTDelete<SkImageDecoder> add(decoder); AutoDecoderCancel adc(options, decoder); // To fix the race condition in case "requestCancelDecode" // happens earlier than AutoDecoderCancel object is added // to the gAutoDecoderCancelMutex linked list. if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) { return nullObjectReturn("gOptions_mCancelID"); } SkBitmap decodingBitmap; //解析Bitmap if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode) != SkImageDecoder::kSuccess) { return nullObjectReturn("decoder->decode returned false"); } //獲取寬高 int scaledWidth = decodingBitmap.width(); int scaledHeight = decodingBitmap.height(); //這裏加0.5應該是四捨五入的意思 if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } // 設置Options的值 if (options != NULL) { jstring mimeType = getMimeTypeString(env, decoder->getFormat()); if (env->ExceptionCheck()) { return nullObjectReturn("OOM in getMimeTypeString()"); } env->SetIntField(options, gOptions_widthFieldID, scaledWidth); env->SetIntField(options, gOptions_heightFieldID, scaledHeight); env->SetObjectField(options, gOptions_mimeFieldID, mimeType); } //justBounds模式下直接返回空便可 if (decodeMode == SkImageDecoder::kDecodeBounds_Mode) { return NULL; } ... SkBitmap outputBitmap; if (willScale) { // This is weird so let me explain: we could use the scale parameter // directly, but for historical reasons this is how the corresponding // Dalvik code has always behaved. We simply recreate the behavior here. // The result is slightly different from simply using scale because of // the 0.5f rounding bias applied when computing the target image size const float sx = scaledWidth / float(decodingBitmap.width()); const float sy = scaledHeight / float(decodingBitmap.height()); // TODO: avoid copying when scaled size equals decodingBitmap size SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType()); // FIXME: If the alphaType is kUnpremul and the image has alpha, the // colors may not be correct, since Skia does not yet support drawing // to/from unpremultiplied bitmaps. outputBitmap.setInfo(SkImageInfo::Make(scaledWidth, scaledHeight, colorType, decodingBitmap.alphaType())); if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) { return nullObjectReturn("allocation failed for scaled bitmap"); } // If outputBitmap's pixels are newly allocated by Java, there is no need // to erase to 0, since the pixels were initialized to 0. if (outputAllocator != &javaAllocator) { outputBitmap.eraseColor(0); } SkPaint paint; paint.setFilterQuality(kLow_SkFilterQuality); //使用畫布的方式進行縮放 SkCanvas canvas(outputBitmap); canvas.scale(sx, sy); canvas.drawARGB(0x00, 0x00, 0x00, 0x00); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } else { outputBitmap.swap(decodingBitmap); } if (padding) { if (peeker.mPatch != NULL) { GraphicsJNI::set_jrect(env, padding, peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop, peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom); } else { GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1); } } // if we get here, we're in kDecodePixels_Mode and will therefore // already have a pixelref installed. if (outputBitmap.pixelRef() == NULL) { return nullObjectReturn("Got null SkPixelRef"); } if (!isMutable && javaBitmap == NULL) { // promise we will never change our pixels (great for sharing and pictures) outputBitmap.setImmutable(); } //若是進行重用,則更新舊Bitmap if (javaBitmap != NULL) { bool isPremultiplied = !requireUnpremultiplied; GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied); outputBitmap.notifyPixelsChanged(); // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } int bitmapCreateFlags = 0x0; if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable; if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied; //建立Bitmap而且返回 return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); }
SkColorType其實是代替4.4w中SkBitmap::kARGB_8888_Config的一個封裝枚舉類:
enum SkColorType { kUnknown_SkColorType, kAlpha_8_SkColorType, kRGB_565_SkColorType, kARGB_4444_SkColorType, kRGBA_8888_SkColorType, kBGRA_8888_SkColorType, kIndex_8_SkColorType, kGray_8_SkColorType, kLastEnum_SkColorType = kGray_8_SkColorType, #if SK_PMCOLOR_BYTE_ORDER(B,G,R,A) kN32_SkColorType = kBGRA_8888_SkColorType, #elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A) kN32_SkColorType = kRGBA_8888_SkColorType, #else #error "SK_*32_SHFIT values must correspond to BGRA or RGBA byte order" #endif };
上面源碼能夠看見確實少掉了isPurgeable的影子,縮放的核心在於 scale = (float) targetDensity / density;
這句話,經過計算目標密度/原圖密度獲得一個縮放比率,而後分別用原Bitmap的寬高乘以對應比率獲得最終Bitmap的寬高,在xhdpi狀況下就是108x108的寬高啦,這也符合咱們實驗後的結果,而內存在用則是原Bitmap的內存x(縮放比率)x(縮放比率)。
在設置了inDensity以及inTargetDensity的狀況下,同時進行設置sampleSize,源碼中會首先根據sampleSize計算出Bitmap的壓縮寬高,而後在根據inDensity以及inTargetDensity進行縮放.
在Android中處理Bitmap免不了遇到OOM的問題,在上面小節中講述了Bitmap的概念以及在Andorid的表現方式和內存管理,這裏就對OOM作個總結。推薦查看官方文檔:manage-memory
首先介紹一下OOM的概念,也就是Out-Of-Memory,俗稱內存溢出,咱們的app在運行時使用的內存若是超出了單個進程容許最大的值,那麼這個進程就會報OOM。OOM發生的狀況通常由內存泄漏或者一次性加載過大的內存數據致使(最有可能的就是Bitmap的加載)。那麼如何去避免加載過大的Bitmap致使的OOM呢?在谷歌官方文檔中介紹瞭如何有效的去加載一張大的Bitmap,再綜合前輩們的方案獲得了大概以下幾個方式去避免加載Bitmap時候OOM的發生:
在Manifest中設置android:largeHeap="true"
這種方式須要須要謹慎使用,緣由引用胡凱大神的博客解釋:
在一些特殊的情景下,你能夠經過在manifest的application標籤下添加largeHeap=true的屬性來爲應用聲明一個更大的heap空間。而後,你能夠經過getLargeMemoryClass()來獲取到這個更大的heap size閾值。然而,聲明獲得更大Heap閾值的本意是爲了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的由於你須要使用更多的內存而去請求一個大的Heap Size。只有當你清楚的知道哪裏會使用大量的內存而且知道爲何這些內存必須被保留時纔去使用large heap。所以請謹慎使用large heap屬性。使用額外的內存空間會影響系統總體的用戶體驗,而且會使得每次gc的運行時間更長。在任務切換時,系統的性能會大打折扣。另外, large heap並不必定可以獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和一般的heap size是同樣的。所以即便你申請了large heap,你仍是應該經過執行getMemoryClass()來檢查實際獲取到的heap大小。
對圖片壓縮的方式主要有尺寸壓縮,採樣率壓縮以及質量壓縮三種方式,質量壓縮不改變內存佔用,所以這裏說的壓縮主要指使用尺寸壓縮和採樣率壓縮的方式,從代碼上看,採樣率壓縮是尺寸壓縮的的子集,Native中實現的方式都是經過scale參數決定最後生成Bitmap的寬高。這裏介紹一下采樣率壓縮,這種方式在谷歌官方文檔中體現,也是各大圖片庫使用的一種減小內存佔用的方式(Glide,Picasso.etc),當咱們加載一張實際爲1080x1920的圖到一個300x200的ImageView的時候做爲縮略圖展現時候,沒有必要全加載一張那麼大的圖片,咱們能夠經過inSampleSize參數配合inJustDecodeBounds 對圖片進行壓縮,谷歌提供的一個關於採樣率的計算方法:
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { // Raw height and width of image val (height: Int, width: Int) = options.run { outHeight to outWidth } var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { val halfHeight: Int = height / 2 val halfWidth: Int = width / 2 // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize }
更多的內容查看官方文檔吧,這裏不敘述了。
inBitmap參數的使用能夠查看官方Demo。
上述說明的OOM是針對以一張圖片而言,多圖片下的策略基於單圖片,額外添加了緩存的操做,最多見的就是LruCache和DiskLruCache策略,官方文檔獻上,若是想要學習對於多圖片加載使用的,我覺深刻一個圖片庫是一個很是不錯的選擇,如Glide。
上面分析中其實或多或少涉獵了Bitmap的壓縮的相關知識,Android咱們能接觸真正意義上的Bitmap壓縮其實只有兩種(自主編譯libjpg的不算):尺寸壓縮和質量壓縮。
尺寸壓縮的方式能夠經過採樣率或者自主設置inDensity和inTargetDensity以及inScreenDensity的方式進行,這裏就不舉例了;質量壓縮方法使用以下:
public static byte[] compressImageToByteArray(Bitmap src, Bitmap.CompressFormat format, int size) { try { byte[] byteArray; ByteArrayOutputStream baos = new ByteArrayOutputStream(); src.compress(format, size, baos); byteArray = baos.toByteArray(); baos.close(); return byteArray; } catch (IOException e) { e.printStackTrace(); } return null; }
Bitmap的compress(...)
中的CompressFormat參數有三種:
JPEG (0), PNG (1), WEBP (2);
須要注意的是可以進行壓縮的只有JPEG以及WEBP格式的,因爲PNG爲無損壓縮格式,因此進行質量壓縮並不會有多大的效果,測試代碼以下:
private void testCompress() { try { File jpgFile = new File(getInnerSDCardPath() + File.separator + "11.jpeg"); Bitmap bitmap = BitmapFactory.decodeFile(jpgFile.getAbsolutePath()); Log.e("test", "初始jpg大小=" + bitmap.getByteCount()); byte[] array = BitmapUtils.compressImageToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 50); Log.e("test", "質量壓縮到50%後的jpg大小=" + array.length); byte[] pngArray = BitmapUtils.compressImageToByteArray(bitmap, Bitmap.CompressFormat.PNG, 50); Log.e("test", "質量壓縮到50%後的png大小=" + pngArray.length); byte[] pngArray1 = BitmapUtils.compressImageToByteArray(bitmap, Bitmap.CompressFormat.PNG, 80); Log.e("test", "質量壓縮到80%後的png大小=" + pngArray1.length); byte[] pngArray2 = BitmapUtils.compressImageToByteArray(bitmap, Bitmap.CompressFormat.PNG, 80); Log.e("test", "質量壓縮到100%後的png大小=" + pngArray2.length); byte[] webArray = BitmapUtils.compressImageToByteArray(bitmap, Bitmap.CompressFormat.WEBP, 50); Log.e("test", "質量壓縮到50%後的webp大小=" + webArray.length); } catch (Exception e) { e.printStackTrace(); } } --- test: 初始jpg大小=2691000 test: 質量壓縮到50%後的jpg大小=142720 test: 質量壓縮到50%後的png大小=660135 test: 質量壓縮到80%後的png大小=660135 test: 質量壓縮到100%後的png大小=660135 test: 質量壓縮到50%後的webp大小=112188
能夠看到JPG和WEBP都進行了壓縮,而對應PNG則沒有變化。