Bitmap源码解析与优化

Bitmap 创建示例

Bitmap的创建方式有很多种,例如在XML中定义bitmap、通过bitmapDrawable生成drawable,又或者通过BitmapFactory来创建Bitmap,可以根据实际使用情况来选择,示例如下:

private void bitmapSample() {
        /** 1. xml形式
          * <bitmap xmlns:android="http://schemas.android.com/apk/res/android"
          *   android:src="@mipmap/ic_avatar_default"></bitmap>
 			*/

        //2.Drawable
        Resources resources = getResources();
        Drawable bitmapDrawable = resources.getDrawable(R.mipmap.ic_avatar_default,null);
        
        //3.Bitmap直接创建
        Bitmap bitmap = Bitmap.createBitmap(200,200,Bitmap.Config.ARGB_8888);
        

        //4.通过BitmapFactory创建Bitmap
        Bitmap bm = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_avatar_default);
     
  }

Bitmap源码解析

Bitmap是一种位图,可以被序列化,且不能被继承,Bitmap的创建和像素数据都是在native层,那么先来看看Bitmap这个类都有些成员变量和属性:

public final class Bitmap implements Parcelable {
   
    public static final int DENSITY_NONE = 0;
    
    private static final long NATIVE_ALLOCATION_SIZE = 32;

    private final long mNativePtr;  //native指针,这个指针很重要,指向的是Bitmap对应的native层对象

    private final boolean mIsMutable;  //标记这个bitmap是否可变,如果不可变,那么需要创建一个同样大小、分辨率的bitmap时,可以返回原始bitmap对象
    
    private boolean mRequestPremultiplied;

    private byte[] mNinePatchChunk;  //.9图数组
    private NinePatch.InsetStruct mNinePatchInsets; 
    
    private int mWidth; //bitmap宽高
    private int mHeight;   
    
    private boolean mRecycled; //是否被回收空间。

    private ColorSpace mColorSpace; //记录Bitmap颜色
    
    public int mDensity = getDefaultDensity();

    private static volatile int sDefaultDensity = -1;
 
    public static volatile int sPreloadTracingNumInstantiatedBitmaps;
   
    public static volatile long sPreloadTracingTotalBitmapsSize;
   
    //Bitmap私有构造函数,必须传入一个nativeBitmap指针,指向一个native层的bitmap,根据该bitmap创建一个Java层的Bitmap
    Bitmap(long nativeBitmap, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk,  NinePatch.InsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        //...成员变量赋值操作
    }
    
 
    
   //内部枚举,用于计算Bitmap大小,不同类型的bitmap占用内存大小不一样。
   public enum Config { 
   
        ALPHA_8     (1),
        
        RGB_565     (3),
        
        @Deprecated
        ARGB_4444   (4),
        
        ARGB_8888   (5),
        
        RGBA_F16    (6),
        
        HARDWARE    (7);
        
   }
   
      //Bitmap压缩类型,有JPEG\PNG\WEBP三种,不同的压缩格式,压缩后大小也不同。
    public enum CompressFormat {
        JPEG    (0),
        PNG     (1),
        WEBP    (2);

        CompressFormat(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;
    }
   
   //... 省略很多createBitmap()方法,创建bitmap的最终实现都是在native层,Java层只是做一些条件判断以及参数设置等等操作。
   
    

Bitmap native层创建源码

由于Bitmap是final的,且构造函数私有,那么就无法直接通过new Bitmap()去创建Bitmap,但是可以通过BitmapFactory去创建Bitmap,BitmapFactory提供了很多方法去创建Bitmap,例如:


BitmapFactory.decodeResource(Resources res, int id);

BitmapFactory.decodeFile(String fileName);

BitmapFactory.decodeStream(InputStream is);

...

//这些方法最终都是调用native的方法去创建Bitmap

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,Rect padding, Options opts);
   
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,Rect padding, Options opts);

private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);

private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,int length, Options opts);


BitmapFactory.Options

创建Bitmap时可以传入一个BitmapFactory.Options对象来设置Bitmap的属性,这个Options合理使用,可以极大的提高Bitmap的性能,来看下这个类的具体代码:

public class BitmapFactory {
   //Options是BitmapFactory的静态内部类
   public static class Options {
       
		public Options() {
            inScaled = true;
            inPremultiplied = true;
       }
       
       /**
         * 如果设置了inBitmap,那么解码bitmap输入流时,会尝试去复用这个inBitmap,如果无法复用这个inBitmap,那么就会抛出一个IllegalArgumentException;
         * 因此这个复用的inBitmap必须是可变的,同时,得到的Bitmap也会是可变的(一般从resource中解码得到的bitmap是一个不可变的bitmap)
         */ 
       public Bitmap inBitmap;
       
       /**
         * 设置获取到的bitmap是否可变,可以用来在通过BitmapFactory加载bitmap时设置想要的属性。
         */ 
       public boolean inMutable;
        
       /**
 	      * 设置为true,那么解码器会返回null,但是会将宽高写入Options的outWidth和outHeight供调用者查询,而无需将Bitmap加载入内存,可以显著的提高性能
 	      */ 
       public boolean inJustDecodeBounds; 
       
       /**
		  *	 缩放比例,通过设置inSampleSize可以请求解码器去返回一个缩小后的image去节省内存,例如设置为2,那么返回的bitmap的宽高是原始bitmap的1/2,像素点是原始bitmap的1/4.
		  *	 如果设置inSampleSize < 1,解码器会重设为1.另外,inSampleSize应设置为2的倍数,否则解码器会四舍五入,取离2的幂最近的数。
         */
       public int inSampleSize;
       
       //图片默认配置是ARGB_8888
       public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
       
       //图片的密度,跟图片放在哪个目录下有关,mipmap-hdpi,mipmap-mdpi,mipmap-xhdpi...
       public int inDensity;
       
       //图片的目标密度,如果没有设置,默认就是屏幕密度
       public int inTargetDensity;
        
       //屏幕的密度
       public int inScreenDensity;
       
       //如果设置了inScaled,那么返回的bitmap会缩放至适配inTargetDensity,而不是依赖于系统的分辨率。
       public boolean inScaled;
       
       //bitmap的最终宽高,如果inJustDecodeBounds设置为false,当bitmap发生缩放时,这个值就是bitmap的宽高。如果设置为true,那么无需计算缩放,这两个值就是返回bitmap的宽高。如果解码发生
       // 错误,那么就设置为-1。
       public int outWidth;
       public int outHeight;
    }
 }   
      

在创建bitmap时设置的Options属性会一路传入到native层,现在再来看看具体的创建过程;选择其中的decodeStream()来看看具体的创建过程:

public static Bitmap decodeStream(InputStream is) {
    //decodeStream(InputStream is)内部调用了decodeStream(InputStream is, Rect outPadding,Options opts)
    return decodeStream(is, null, null);
}

再看看decodeStream(InputStream is, Rect outPadding,Options opts)的源码:

/** 
  * outPadding:如果设置了outPadding,相当于对原始Bitmap进行一个裁剪,返回Rect包裹的Bitmap
  * opts : bitmap属性设置,例如设置inJustDecodeBounds = true,允许计算bitmap大小,但是不将bitmap加载进内存。
  *
  */
public static Bitmap decodeStream(InputStream is, Rect outPadding,Options opts){
    if (is == null) {
            return null;
        }
        //验证opt是否合法
        validate(opts);

        Bitmap bm = null;

        Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
        
        try {
           //根据输入流类型调用下面的不同方法去创建Bitmap。
            if (is instanceof AssetManager.AssetInputStream) {
                final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
                bm = nativeDecodeAsset(asset, outPadding, opts);
            } else {
                //这里最终就会调用nativeDecodeStream()
                bm = decodeStreamInternal(is, outPadding, opts);
            }

            if (bm == null && opts != null && opts.inBitmap != null) {
                throw new IllegalArgumentException("Problem decoding into existing bitmap");
            }

            setDensityFromOptions(bm, opts);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
        }

        return bm;
}

decodeStreamInternal():

 private static Bitmap decodeStreamInternal(InputStream is,Rect outPadding, Options opts) {
    
        byte [] tempStorage = null;
        if (opts != null) tempStorage = opts.inTempStorage;
        if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE]; //创建一个buffer用于decode,默认是16K。
        return nativeDecodeStream(is, tempStorage, outPadding, opts); //最后就调用BitmapFactory的native方法去创建bitmap
    }
    

BitmapFactory.cpp的源码在:

/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

现在来看看BitmapFactory.cpp的源码:

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {

   //定义一个bitmap对象
   jobject bitmap = NULL;
   
   //创建一个输入流适配器
   std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

   if (stream.get()) {
        std::unique_ptr<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
       SkASSERT(bufferedStream.get() != NULL);
       
       // 创建bitmap的代码在doDecode()里面
        bitmap = doDecode(env, bufferedStream.release(), padding, options);
    }
   return bitmap;
}

再看看doDecode()的代码:


static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {

   std::unique_ptr<SkStreamRewindable> streamDeleter(stream);
   
   //从Java代码的Bitmap.java中我们知道,创建Bitmap需要如下的一些成员变量,现在首先定义默认的一些配置,例如缩放比例、是否只是decode,是否是不可变的等等:

   int sampleSize = 1;
   bool onlyDecodeSize = false;
   SkColorType prefColorType = kN32_SkColorType;
   bool isHardware = false;
   bool isMutable = false;
   float scale = 1.0f;
   bool requireUnpremultiplied = false;
   
   //这个就是需要返回给Java层的bitmap对象。
   jobject javaBitmap = NULL;
   

   //当options不为空时,从option里面取出各种配置设置给上面定义的各个变量
   if (options != NULL) {
       sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);

		//若sampleSize <= 0,会默认重设为1
       if (sampleSize <= 0) {
           sampleSize = 1;
      }
		//获取并设置onlyDecodeSize,对应Options中的inJustDecodeBounds。
       if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
           onlyDecodeSize = true;
       }
       
       ...
       //这里很重要,先从options中获取scale的值,如果为true,那么就分别再获取inDensity和inTargetDensity的值以及inScreenDensity的值,用于计算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 等于 要加载图片的density / 图片实际存放目录的density的比值
               scale = (float) targetDensity / density;
           }
       }

		...
       isHardware = GraphicsJNI::isHardwareConfig(env, jconfig);
       isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
       requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
       
       //这个gOptions_bitmapFieldID指的是BitmapOptions的inBitmap,如果这个值被设置了,那么decode方法就会去重复使用已存在的bitmap,这样能够提高性能,减少内存的分配和删除。
       javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);

       ....
   }

   //Bitmap不能既是可变又是Hardware的。
   if (isMutable && isHardware) {
       doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
       return nullObjectReturn("Cannot create mutable hardware bitmap");
    }

   // 创建解码器
   NinePatchPeeker peeker;
   std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(streamDeleter.release(), &peeker));
    if (!codec.get()) {
        return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
    }

   
       //.9图不能被decode成RGB565格式,因为会抖动
294    if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
295        prefColorType = kN32_SkColorType;
296    }
297
298    //根据采样率创建SkISize
299    SkISize size = codec->getSampledDimensions(sampleSize);
300
		 
301    int scaledWidth = size.width();
302    int scaledHeight = size.height();
303    bool willScale = false;
304
305  
306    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
307        willScale = true;
			 //从流中解码,并获得具体的宽高,再根据sampleSize得到缩放后的宽高。
308        scaledWidth = codec->getInfo().width() / sampleSize;
309        scaledHeight = codec->getInfo().height() / sampleSize;
310    }
311
312    // 设置解码color类型
313    SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);
314    sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(decodeColorType, prefColorSpace);
316
317    // 如果设置了inJustDecodeBounds,那么这里就返回Bitmap的宽高,而不会创建具体的Bitmap。
318    if (options != NULL) {
319        jstring mimeType = encodedFormatToString(env, (SkEncodedImageFormat)codec->getEncodedFormat());
321        if (env->ExceptionCheck()) {
322            return nullObjectReturn("OOM in encodedFormatToString()");
323        }
324        env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
325        env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
326        env->SetObjectField(options, gOptions_mimeFieldID, mimeType);		    .....		
		
		     //如果仅仅只是需要计算宽高,那么就直接返回一个空指针,不需要创建一个新的Bitmap,那么就可以不用将bitmap加载到内存就能够计算得到它的宽高
339        if (onlyDecodeSize) {
340            return nullptr;
341        }
342    }
343
344    // 很重要,图片宽高的计算:
		// 如果scale不为1,那么就进行相应的缩放,可见图片的宽= 实际宽 * inTargetDensity/inDensity + 0.5f,高同理
345    if (scale != 1.0f) {
346        willScale = true;
347        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
348        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
349    }
350
	    ....
	    
	    //....调用HeapAllocator去分配空间
	   
	   //根据输入流解码得到的bitmap 
	   SkBitmap decodingBitmap;
     if (!decodingBitmap.setInfo(bitmapInfo) || !decodingBitmap.tryAllocPixels(decodeAllocator)) {
         return nullptr;
     }

		 //.9图处理
	   jbyteArray ninePatchChunk = NULL;
432    if (peeker.mPatch != NULL) {
433        if (willScale) {
434            scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight);
435        }
436
437        size_t ninePatchArraySize = peeker.mPatch->serializedSize();
438        ninePatchChunk = env->NewByteArray(ninePatchArraySize);
439        if (ninePatchChunk == NULL) {
440            return nullObjectReturn("ninePatchChunk == null");
441        }
442
443        jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
444        if (array == NULL) {
445            return nullObjectReturn("primitive array == null");
446        }
447
			//为mNinePatchChunk分配空间
448        memcpy(array, peeker.mPatch, peeker.mPatchSize);
449        env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
450    }
451
452    jobject ninePatchInsets = NULL;
453    if (peeker.mHasInsets) {
454        ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID,
455                peeker.mOpticalInsets[0], peeker.mOpticalInsets[1],
456                peeker.mOpticalInsets[2], peeker.mOpticalInsets[3],
457                peeker.mOutlineInsets[0], peeker.mOutlineInsets[1],
458                peeker.mOutlineInsets[2], peeker.mOutlineInsets[3],
459                peeker.mOutlineRadius, peeker.mOutlineAlpha, scale);
460        if (ninePatchInsets == NULL) {
461            return nullObjectReturn("nine patch insets == null");
462        }
463        if (javaBitmap != NULL) {
464            env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets);
465        }
466    }
		 //定义SkBitmap对象,native层存储的Bitmap对象
468    SkBitmap outputBitmap;
469    if (willScale) {
475        const float sx = scaledWidth / float(decodingBitmap.width());
476        const float sy = scaledHeight / float(decodingBitmap.height());
477
479        SkBitmap::Allocator* outputAllocator;
480        if (javaBitmap != nullptr) {
481            outputAllocator = &recyclingAllocator;
482        } else {
483            outputAllocator = &defaultAllocator;
484        }
485
486        SkColorType scaledColorType = decodingBitmap.colorType();
490        outputBitmap.setInfo(bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
492        if (!outputBitmap.tryAllocPixels(outputAllocator)) {
496            return nullObjectReturn("allocation failed for scaled bitmap");
497        }
498
499        SkPaint paint;
503        paint.setBlendMode(SkBlendMode::kSrc);
504        paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
505
			  //调用SKCanvas将bitmap画出来,最终会调用Graphics的draw()方法。
506        SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
507        canvas.scale(sx, sy);
508        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
509    } else {
510        outputBitmap.swap(decodingBitmap);
511    }

		 //处理边距
513    if (padding) {
514        if (peeker.mPatch != NULL) {
515            GraphicsJNI::set_jrect(env, padding,
516                    peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop,
517                    peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom);
518        } else {
519            GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
520        }
521    }
522
		 //设置outBitmap的属性
524    if (outputBitmap.pixelRef() == NULL) {
525        return nullObjectReturn("Got null SkPixelRef");
526    }
527
528    if (!isMutable && javaBitmap == NULL) {
530        outputBitmap.setImmutable();
531    }
532
533    bool isPremultiplied = !requireUnpremultiplied;

534    if (javaBitmap != nullptr) {
535        bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
536        outputBitmap.notifyPixelsChanged();
538        return javaBitmap;
539    }
540
541    int bitmapCreateFlags = 0x0;
542    if (isMutable) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Mutable;
543    if (isPremultiplied) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Premultiplied;
544
545    if (isHardware) {
546        sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap);
547        if (!hardwareBitmap.get()) {
548            return nullObjectReturn("Failed to allocate a hardware bitmap");
549        }
550        return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,ninePatchChunk, ninePatchInsets, -1);
552    }
		 //这里调用createBitmap()生成Java的Bitmap对象,其实native已经创建了真正的Bitmap,
    return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
557}

createBitmap()生成bitmap对象:

jobject createBitmap(JNIEnv* env, Bitmap* bitmap,int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,int density) {
   bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
   bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
  
   assert_premultiplied(bitmap->info(), isPremultiplied);
   BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
   
   //调用Bitmap的构造函数创建Bitmap对象,bitmap对象存储于native,Java持有的是native的一个BitmapWrapper,将BitmapWrapper赋给Java的mNativePtr。
   jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
           reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
            isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);

   if (env->ExceptionCheck() != 0) {
       ALOGE("*** Uncaught exception returned from Java call!\n");
       env->ExceptionDescribe();
   }
   return obj;
}

Bitmap分析

从上面的创建过程可知,Bitmap无法直接在Java层通过new Bitmap()创建出来,而是需要通过BitmapFactory的decodeFile()或者decodeStream()等等方法去创建,而这些方法内部调用的是native
层的decodeStream()方法,创建的实际bitmap会是保存在native,Java层根据拿到的是native层的BitmapWrapper引用,用于访问native层bitmap。

Bitmap内存计算

Bitmap是内存中的大户,那么现在来看看一个Bitmap的内存是如何计算的,从上面的Bitmap源码可以看到,Bitmap定义了一个枚举,用于表示Bitmap的格式用来表示Bitmap一个像素点的存储格式,如下:

public enum Config { 
   
        ALPHA_8     (1),
        
        RGB_565     (3),
        
        @Deprecated
        ARGB_4444   (4),
        
        ARGB_8888   (5),
        
        RGBA_F16    (6),
        
        HARDWARE    (7);
        
 }

那么bitmap的大小如何计算?想要知道一个bitmap所占用内存大小,可以通过getAllocationByteCount()去获取:

public final int getAllocationByteCount() {
        if (mRecycled) {
            //如果bitmap已经被回收,那么就直接返回0
            return 0;
        }
        //否则根据mNativePtr引用去native中计算内存大小,mNativePtr是native bitmap的引用。
        return nativeGetAllocationByteCount(mNativePtr);
    }

Bitmap.cpp中计算内存的方法:

static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) {
   //根据bitmapPtr找到native存储的bitmap对象
   LocalScopedBitmap bitmapHandle(bitmapPtr);
   return static_cast<jint>(bitmapHandle->getAllocationByteCount());

注意getByteCount()和getAllocationByteCount()的区别:

getByteCount() & getAllocationByteCount():

不使用inBitmap时两者相等,当使用了inBitmap时,且inBitmap的图片内存比新创建图片所需要的内存大,那么getByteCount()返回新创建的图片所需要的内存,getAllocationByteCount()返回被复用的bitmap的内存。

现在来看下上面各个配置下像素点的内存计算方法:

ALPHA_8:

每个像素只有一个透明度,即只保存透明度,8位存储,用1个字节即可表示一个像素点;

RGB_565:

每个像素点需要用Red\greeen\blue三个色素值表示,red占5位,green占6位,blue占5位,即一个像素点占16位,即2个字节;

ARGB_4444:

已经废弃(质量不好),每个像素点需要用alpha、Red、greeen、blue四个色素值表示,alpha、red、green、blue各占4位,即一个像素点占16位,即2个字节;

ARGB_8888:

每个像素点需要用alpha、Red、greeen、blue四个色素值表示,alpha、red、green、blue各占8位,即一个像素点占32位,即4个字节;

RGBA_F16:

每个像素点需要用alpha、Red、greeen、blue四个色素值表示,alpha、red、green、blue各占16位,即一个像素点占64位,即8个字节;

HARDWARE:

图片存储于GPU内存中,数据格式还是ARGB_8888.

图片的内存计算:

图片所占内存 = 宽 * 高 * 一个像素点占用字节。

从上面创建bitmap可知,bitmap的宽高计算如下:

 scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
 scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
 scale = inTargetDensity / inDensity
 

由此得出图片内存计算公式:

bitmapMemory = scaledWidth * scaledHeight * 一个像素点的大小 = (原始with * targetDensity / inDensity + 0.5f) * (原始height * targetDensity / inDensity + 0.5f)

那这样计算的结果是否正确呢?来验证一下:

在mipmap-xxhdpi(inDensity = 480)下放一张144 * 144的图片,然后去计算大小,再获取大小打印出来,运行于分辨率480(inTargetDEnsity)的华为手机上,图片默认配置为ARGB_8888,

private void caculateBitmap(){
  BitmapFactory.Options options = new BitmapFactory.Options();
  options.inJustDecodeBounds = true;

  Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_bitmap);

  System.out.println("bitmap size: "+bitmap.getAllocationByteCount());  //打印结果为 :82944B
}

然后套用上面的公式来计算:

(144 * 480 / 480 + 0.5)* ( 144 * 480 / 480 + 0.5) * 4 = int(144.5) * int(144.5) * 4 = 144 * 144 * 4 = 82944B

因此可以验证得到,我们上面的内存计算公式是对的。

Bitmap内存优化

Bitmap内存优化可以从三方面入手:

  1. Bitmap复用
  2. Bitmap格式选择以及合理缩放
  3. Bitmap回收

Bitmap复用

bitmap复用有两种手段:

  1. 使用LruCache和DiskLruCache对bitmap进行缓存;
  2. 使用Options.inBitmap,可以复用一个已存在的bitmap的内存,那么不需要重新在Native去申请内存、释放内存的操作,可以显著的提高性能,但是这里要注意一下版本兼容的问题,参考官方例子Managing Bitmap Memory

Bitmap内存管理版本变化:

  • Android 2.2(API 8) 以及之前版本,当进行GC时,app线程会暂停,所以这样会降低性能,Android 2.3 增加了并行GC,当bitmap不再被引用时,可以并行的去进行回收
  • Android 2.3.3(API 10) 以及之前版本,bitmap的像素数据存储在native内存中,和bitmap对象本身分离,有可能会导致application超过了它的内存限制引起crash
  • Android 3.0 (API11) 到 Android7.0(API 25) ,像素数据以及bitmap对象存储在虚拟机堆中,所以当使用这些版本时,要尤其注重bitmap的回收
  • Android 8.0 (API 26)以及之后版本,又将bitmap数据存储在native的堆中,且当bitmap没有再被引用时,GC会自动去回收这个像素数据的内存空间,一般不需要手动的去recycle(),除非确认已经不再需要该 bitmap了,那么可以调用recycle去回收,bitmap不会立刻回收,而会被标记成“dead",此后再调用该bitmap去获取像素数据则会抛出异常

Android3.0之后提供了BitmapFactory.Options.inBitmap去设置复用bitmap,当设置了inBitmap,那么解码方法就会尝试去复用该bitmap的内存,这样能够显著的提高性能,不用重复去申请释放内存,但是使用inBitmap也有限制,在Android4.4之前,只有复用bitmap和待创建bitmap的大小相等时才能复用;复用bitmap必须设置成mutable的,创建的bitmap也会是mutable,即使通常从resource解码出bitmap是immutable的bitmap,复用示例:

 private void reUseBitmap() {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        options.inDensity = 480;
        options.inTargetDensity = 480;
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_avatar_default,options);
        iv1.setImageBitmap(bitmap);
        recycleBitmap.setIsDisplayed(true);
        cache.put(String.valueOf(R.mipmap.ic_avatar_default),bitmap);
        recycleBitmap.setIsCache(true);
        
        System.out.println("--:bitmap:"+bitmap); //android.graphics.Bitmap@924e1c2
        System.out.println("--bitmap.getByteCount:"+bitmap.getByteCount()); //32400
        System.out.println("--bitmap.getAllocationByteCount"+bitmap.getAllocationByteCount());  //32400
        
        //由于初始bitmap没有复用其他的bitmap,所以它的getByteCount()和getAllocationByteCount()大小一样。

        BitmapFactory.Options reUse = new BitmapFactory.Options();
        reUse.inBitmap = cache.get(String.valueOf(R.mipmap.ic_avatar_default));
        reUse.inMutable = true;
         reUse.inDensity = 480;
        reUse.inTargetDensity = 240; //这里targetDensity设置为原始bitmap的一半,那么根据上面的内存计算公式,这个内存大小应该是原来的1/4

        Bitmap reUseBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_bitmap,reUse);
        iv2.setImageBitmap(reUseBitmap);

        System.out.println("--reUseBitmap.getByteCount:"+reUseBitmap.getByteCount()); //8100 ,当前bitmap所需要的内存大小
        System.out.println("--reUseBitmap.getAllocationByteCount"+reUseBitmap.getAllocationByteCount()); //32400,当前bitmap实际的内存大小,即复用bitmap的大小,的确为上面的4倍。
        System.out.println("--:reUseBitmap:"+reUseBitmap); //android.graphics.Bitmap@924e1c2
       
    }
    

从上面打印可知,两个bitmap的内存地址一致,说明第二个bitmap的确是复用了第一个的内存地址,而且要注意复用之后,显示原来bitmap的ImageView也会显示复用后的bitmap,且复用的内存空间应该大于需要创建的新Bitmap,否则会有IllegalArgumentException异常。

可见,我们可以通过LruCache或者DiskLruCache对bitmap进行缓存,或者使用in Bitmap对bitmap进行复用,减少底层再进行内存申请、释放以及解码等等过程,可以显著的提高性能。

Bitmap合理缩放

由上面的结论得知,bitmap所占内存由宽高、inDensity、屏幕密度、图片像素格式计算得出,那么想要减少内存占用,就可以考虑从这里入手,屏幕密度是硬件限制,无法改变。那么可以考虑从其他各个方面入手:

  1. 对图片进行压缩,在创建bitmap时,使用inSampleSize进行缩小,例如ImageView的大小是100 * 100,而图片的大小是200 * 200,那么可以将图片缩小一倍,宽高变小了,占用内存自然也小了;

  2. 合理使用图片的像素格式,从上面计算知道,ARGB_8888占用内存为4个字节,而RGB_565为2个字节,那么在保证图片质量的情况下,可以考虑采用这个格式,系统默认是ARGB_8888,ARGB_4444已经被废弃了,就不考 虑了;

  3. 小图需要放大时,可以使用矩阵,例如:

        Matrix matrix = new Matrix();
         matrix.postScale(2,2,0,0);
         iv1.setImageMatrix(matrix);
         iv1.setScaleType(ImageView.ScaleType.MATRIX);
         iv1.setImageBitmap(bitmap);  //对图片使用了放大,但是图片所占内存和原来一样。
    

Bitmap回收

Android2.3.3以及之前版本,需要使用recycle()去进行回收,因为如果需要展示非常大的bitmap在app上,可能会引起OOM,所以需要及时使用recycle()去回收,回收后就不能再对bitmap操作了,可以通过一个引用计数去对bitmap进行管理,当无引用时就及时回收,Android3.0之后应该关注的是Bitmap的复用问题,native会自动帮助bitmap回收,可以不手动调用recycle(),示例如下:

    private int cacheRefCount = 0;  //缓存计数
    private int displayRefCount = 0;   //展示计数
    private boolean hasBeenDisplayed = false;

    public void setIsDisplayed(boolean isDisplayed) {
        synchronized (this) {
            if (isDisplayed) {
                displayRefCount++;
                hasBeenDisplayed = true;
            } else {
                displayRefCount--;
            }
        }
        checkState();
    }


    public void setIsCache(boolean isCache) {
        synchronized (this) {
            if (isCache) {
                cacheRefCount++;
            } else {
                cacheRefCount--;
            }
        }
        checkState();
    }

    private synchronized void checkState() {
        if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap()) {
            bitmap.recycle();
        }
    }

    private synchronized boolean hasValidBitmap() {
        return bitmap != null && !bitmap.isRecycled();
    }

猜你喜欢

转载自blog.csdn.net/qq_26984087/article/details/89320850