深入了解Bitmap源码解析及经验总结

Bitmap的分析与使用

在Android应用里,最耗费内存的就是图片资源。而且在Android系统中,读取位图Bitmap时,分给虚拟机中的图片的堆栈大小只有8M,如果超出了,就会出现OutOfMemory异常。所以,对于图片的内存优化,是Android应用开发中比较重要的内容。

先看看关于Bitmap13个的用法
  • 1、Drawable 的用法
Resources resources = mContext.getResources();
Drawable drawable = resources.getDrawable(R.drawable.a);
imageview.setBackground(drawable);

2、BitmapDrawable的用法Bitmap


Resources r = this.getContext().getResources();
Inputstream is = r.openRawResource(R.drawable.my_background_image);
BitmapDrawable  bmpDraw = new BitmapDrawable(is);
Bitmap bmp = bmpDraw.getBitmap();
  • 3、Bitmap –》BitmapFactory.decodeResource
Bitmap bmp=BitmapFactory.decodeResource(r, R.drawable.icon);
Bitmap newb = Bitmap.createBitmap( 300, 300, Config.ARGB_8888 ); 
  • 4、Bitmap –》BitmapFactory.decodeStream
InputStream is = getResources().openRawResource(R.drawable.icon);  
Bitmap mBitmap = BitmapFactory.decodeStream(is);
  • 5、Drawable → Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {

Bitmap bitmap = Bitmap

.createBitmap(

drawable.getIntrinsicWidth(),

drawable.getIntrinsicHeight(),

drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888

: Bitmap.Config.RGB_565);

Canvas canvas = new Canvas(bitmap);

// canvas.setBitmap(bitmap);

drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),

drawable.getIntrinsicHeight());

drawable.draw(canvas);

return bitmap;

}
  • 6、从资源中获取Bitmap
Resources res=getResources();

Bitmap bmp=BitmapFactory.decodeResource(res, R.drawable.pic);
  • 7、Bitmap → byte[]

private byte[] Bitmap2Bytes(Bitmap bm){

ByteArrayOutputStream baos = new ByteArrayOutputStream();

bm.compress(Bitmap.CompressFormat.PNG, 100, baos);

return baos.toByteArray();

}
  • 8、byte[] → Bitmap
private Bitmap Bytes2Bimap(byte[] b){

if(b.length!=0){

return BitmapFactory.decodeByteArray(b, 0, b.length);

}

else {

return null;

}

}
  • 9、保存bitmap
static boolean saveBitmap2file(Bitmap bmp,String filename){

CompressFormat format= Bitmap.CompressFormat.JPEG;

int quality = 100;

OutputStream stream = null;

try {

stream = new FileOutputStream("/sdcard/" + filename);

} catch (FileNotFoundException e) {

// TODO Auto-generated catch block

Generated by Foxit PDF Creator © Foxit Software

http://www.foxitsoftware.com For evaluation only.

e.printStackTrace();

}

return bmp.compress(format, quality, stream);

}
  • 10、将图片按自己的要求缩放

Bitmap bm = BitmapFactory.decodeStream(getResources()

.openRawResource(R.drawable.dog));

// 获得图片的宽高

int width = bm.getWidth();

int height = bm.getHeight();

// 设置想要的大小

int newWidth = 320;

int newHeight = 480;

// 计算缩放比例

float scaleWidth = ((float) newWidth) / width;

float scaleHeight = ((float) newHeight) / height;

// 取得想要缩放的matrix参数

Matrix matrix = new Matrix();

matrix.postScale(scaleWidth, scaleHeight);

// 得到新的图片

Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix,

true);

// 放在画布上

canvas.drawBitmap(newbm, 0, 0, paint);
  • 11、File图片转Bitmap
Bitmap bt = BitmapFactory.decodeFile("/sdcard/myImage/" + "head.jpg");//图片地址
  • 12、图片转Bitmap
public Bitmap drawableToBitamp(int drawableResource) {  //可以取raw里面的资源
        BitmapFactory.Options opt = new BitmapFactory.Options();
        opt.inPreferredConfig = Bitmap.Config.RGB_565;
        opt.inPurgeable = true;
        opt.inInputShareable = true;
        InputStream is = this.getResources().openRawResource(drawableResource);
        BitmapFactory.decodeStream(is, null, opt);
        return BitmapFactory.decodeStream(is, null, opt);
    }

  • 13、bitmap的用法小结

BitmapFactory.Options option = new BitmapFactory.Options();
option.inSampleSize = 2; //将图片设为原来宽高的1/2,防止内存溢出
Bitmap bm = BitmapFactory.decodeFile("",option);//文件流
URL url = new URL("");
InputStream is = url.openStream();
Bitmap bm = BitmapFactory.decodeStream(is);
android:scaleType:
android:scaleType//是控制图片如何resized/moved来匹对ImageView的size。ImageView.ScaleType /

要及时回收Bitmap的内存

Bitmap类有一个方法recycle(),从方法名可以看出意思是回收。这里就有疑问了,Android系统有自己的垃圾回收机制,可以不定期的回收掉不使用的内存空间,当然也包括Bitmap的空间。那为什么还需要这个方法呢?

Bitmap类的构造方法都是私有的,所以开发者不能直接new出一个Bitmap对象,只能通过BitmapFactory类的各种静态方法来实例化一个Bitmap。仔细查看BitmapFactory的源代码可以看到,生成Bitmap对象最终都是通过JNI调用方式实现的。所以,加载 Bitmap到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java部分的,一部分是C部分的。这个Bitmap对象是由Java部分分配的,不用的时候系统就会自动回收了,但是那个对应的C可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用 recycle()方法来释放C部分的内存。从Bitmap类的源代码也可以看到,recycle()方法里也的确是调用了JNI方法了的。

那如果不调用recycle(),是否就一定存在内存泄露呢?也不是的。Android的每个应用都运行在独立的进程里,有着独立的内存,如果整个进程被应用本身或者系统杀死了,内存也就都被释放掉了,当然也包括C部分的内存。

Android对于进程的管理是非常复杂的。简单的说,Android系统的进程分为几个级别,系统会在内存不足的情况下杀死一些低优先级的进程,以提供给其它进程充足的内存空间。在实际项目开发过程中,有的开发者会在退出程序的时候使用 Process.killProcess(Process.myPid())的方式将自己的进程杀死,但是有的应用仅仅会使用调用 Activity.finish()方法的方式关闭掉所有的Activity。

经验分享:

Android手机的用户,根据习惯不同,可能会有两种方式退出整个应用程序:一种是按Home键直接退到桌面;另一种是从应用程序的退出按钮或者按 Back键退出程序。那么从系统的角度来说,这两种方式有什么区别呢?按Home键,应用程序并没有被关闭,而是成为了后台应用程序。按Back键,一般来说,应用程序关闭了,但是进程并没有被杀死,而是成为了空进程(程序本身对退出做了特殊处理的不考虑在内)。

Android系统已经做了大量进程管理的工作,这些已经可以满足用户的需求。个人建议,应用程序在退出应用的时候不需要手动杀死自己所在的进程。对于应用程序本身的进程管理,交给Android系统来处理就可以了。应用程序需要做的,是尽量做好程序本身的内存管理工作。

一般来说,如果能够获得Bitmap对象的引用,就需要及时的调用Bitmap的recycle()方法来释放Bitmap占用的内存空间,而不要等Android系统来进行释放。

下面是释放Bitmap的示例代码片段。

// 先判断是否已经回收 

if(bitmap != null && !bitmap.isRecycled()){   

        // 回收并且置为null 

        bitmap.recycle();   

        bitmap = null;   

}   

System.gc();

从上面的代码可以看到,bitmap.recycle()方法用于回收该Bitmap所占用的内存,接着将bitmap置空,最后使用 System.gc()调用一下系统的垃圾回收器进行回收,可以通知垃圾回收器尽快进行回收。这里需要注意的是,调用System.gc()并不能保证立即开始进行回收过程,而只是为了加快回收的到来。

如何调用recycle()方法进行回收已经了解了,那什么时候释放Bitmap的内存比较合适呢?一般来说,如果代码已经不再需要使用 Bitmap对象了,就可以释放了。释放内存以后,就不能再使用该Bitmap对象了,如果再次使用,就会抛出异常。所以一定要保证不再使用的时候释放。比如,如果是在某个Activity中使用Bitmap,就可以在Activity的onStop()或者onDestroy()方法中进行回收。

2) 捕获异常

因为Bitmap是吃内存大户,为了避免应用在分配Bitmap内存的时候出现OutOfMemory异常以后Crash掉,需要特别注意实例化Bitmap部分的代码。通常,在实例化Bitmap的代码中,一定要对OutOfMemory异常进行捕获。
以下是代码示例。

itmap bitmap = null; 

try { 

    // 实例化Bitmap 

    bitmap = BitmapFactory.decodeFile(path); 

} catch (OutOfMemoryError e) { 

    // 

} 

if (bitmap == null) { 

    // 如果实例化失败 返回默认的Bitmap对象 

    return defaultBitmapMap; 

}

这里对初始化Bitmap对象过程中可能发生的OutOfMemory异常进行了捕获。如果发生了OutOfMemory异常,应用不会崩溃,而是得到了一个默认的Bitmap图。
经验分享:
很多开发者会习惯性的在代码中直接捕获Exception。但是对于OutOfMemoryError来说,这样做是捕获不到的。因为 OutOfMemoryError是一种Error,而不是Exception。在此仅仅做一下提醒,避免写错代码而捕获不到 OutOfMemoryError。

```
    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);
```
 而`Bitmap`又是Java对象,这个Java对象又是从native,也就是C/C++中产生的,所以,在Android中Bitmap的内存管理涉及到两部分,一部分是*native*,另一部分是*dalvik*,也就是我们常说的java堆(如果对java堆与栈不了解的同学可以戳),到这里基本就已经了解了创建Bitmap的一些内存中的特性(大家可以使用``adb shell dumpsys meminfo``去查看Bitmap实例化之后的内存使用情况)。

3) 缓存通用的Bitmap对象

有时候,可能需要在一个Activity里多次用到同一张图片。比如一个Activity会展示一些用户的头像列表,而如果用户没有设置头像的话,则会显示一个默认头像,而这个头像是位于应用程序本身的资源文件中的。

如果有类似上面的场景,就可以对同一Bitmap进行缓存。如果不进行缓存,尽管看到的是同一张图片文件,但是使用BitmapFactory类的方法来实例化出来的Bitmap,是不同的Bitmap对象。缓存可以避免新建多个Bitmap对象,避免内存的浪费。

经验分享:

Web开发者对于缓存技术是很熟悉的。其实在Android应用开发过程中,也会经常使用缓存的技术。这里所说的缓存有两个级别,一个是硬盘缓存,一个是内存缓存。比如说,在开发网络应用过程中,可以将一些从网络上获取的数据保存到SD卡中,下次直接从SD卡读取,而不从网络中读取,从而节省网络流量。这种方式就是硬盘缓存。再比如,应用程序经常会使用同一对象,也可以放到内存中缓存起来,需要的时候直接从内存中读取。这种方式就是内存缓存。

4) 压缩图片

这里我们来算一下,在Android中,如果采用Config.ARGB_8888的参数去创建一个Bitmap,这是Google推荐的配置色彩参数,也是Android4.4及以上版本默认创建Bitmap的Config参数(Bitmap.Config.inPreferredConfig的默认值),那么每一个像素将会占用4byte,如果一张手机照片的尺寸为1280×720,那么我们可以很容易的计算出这张图片占用的内存大小为 1280x720x4 = 3686400(byte) = 3.5M,一张未经处理的照片就已经3.5M了! 显而易见,在开发当中,这是我们最需要关注的问题,否则分分钟OOM!
- 那么,我们一般是如何处理Size这个重要的因素的呢?,当然是调整Bitmap的大小到适合的程度啦!辛亏在BitmapFactory中,我们可以很方便的通过BitmapFactory.Options中的options.inSampleSize去设置Bitmap的压缩比,官方给出的说法是

If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory….For example, inSampleSize == 4 returns
an image that is 1/4 the width/height of the original, and 1/16 the
number of pixels. Any value <= 1 is treated the same as 1.

         很简洁明了啊!也就是说,只要按计算方法设置了这个参数,就可以完成我们Bitmap的Size调整了。那么,应该怎么调整姿势才比较舒服呢?下面先介绍其中一种通过``InputStream``的方式去创建``Bitmap``的方法,上一段从Gallery中获取照片并且将图片Size调整到合适手机尺寸的代码:
    ```
        static final int PICK_PICS = 9;

        public void startGallery(){
            Intent i = new Intent();
            i.setAction(Intent.ACTION_PICK);
            i.setType("image/*");
            startActivityForResult(i,PICK_PICS);
        }

         private int[] getScreenWithAndHeight(){
            WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics dm = new DisplayMetrics();
            wm.getDefaultDisplay().getMetrics(dm);
            return new int[]{dm.widthPixels,dm.heightPixels};
        }

        /**
         *
         * @param actualWidth 图片实际的宽度,也就是options.outWidth
         * @param actualHeight 图片实际的高度,也就是options.outHeight
         * @param desiredWidth 你希望图片压缩成为的目的宽度
         * @param desiredHeight 你希望图片压缩成为的目的高度
         * @return
         */
        private int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
            double wr = (double) actualWidth / desiredWidth;
            double hr = (double) actualHeight / desiredHeight;
            double ratio = Math.min(wr, hr);
            float n = 1.0f;
            //这里我们为什么要寻找 与ratio最接近的2的倍数呢?
            //原因就在于API中对于inSimpleSize的注释:最终的inSimpleSize应该为2的倍数,我们应该向上取与压缩比最接近的2的倍数。
            while ((n * 2) <= ratio) {
                n *= 2;
            }

            return (int) n;
        }

        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            if(resultCode == RESULT_OK){
                switch (requestCode){
                    case PICK_PICS:
                        Uri uri = data.getData();
                        InputStream is = null;
                        try {
                            is = getContentResolver().openInputStream(uri);
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }

                        BitmapFactory.Options options = new BitmapFactory.Options();
                        //当这个参数为true的时候,意味着你可以在解析时候不申请内存的情况下去获取Bitmap的宽和高
                        //这是调整Bitmap Size一个很重要的参数设置
                        options.inJustDecodeBounds = true;
                        BitmapFactory.decodeStream( is,null,options );

                        int realHeight = options.outHeight;
                        int realWidth = options.outWidth;

                        int screenWidth = getScreenWithAndHeight()[0];

                        int simpleSize = findBestSampleSize(realWidth,realHeight,screenWidth,300);
                        options.inSampleSize = simpleSize;
                        //当你希望得到Bitmap实例的时候,不要忘了将这个参数设置为false
                        options.inJustDecodeBounds = false;

                        try {
                            is = getContentResolver().openInputStream(uri);
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }

                        Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);

                        iv.setImageBitmap(bitmap);

                        try {
                            is.close();
                            is = null;
                        } catch (IOException e) {
                            e.printStackTrace();
                        }

                        break;
                }
            }
            super.onActivityResult(requestCode, resultCode, data);
        }
    ```
经验分享:

如果程序的图片的来源都是程序包中的资源,或者是自己服务器上的图片,图片的大小是开发者可以调整的,那么一般来说,就只需要注意使用的图片不要过大,并且注意代码的质量,及时回收Bitmap对象,就能避免OutOfMemory异常的发生。
如果程序的图片来自外界,这个时候就特别需要注意OutOfMemory的发生。一个是如果载入的图片比较大,就需要先缩小;另一个是一定要捕获异常,避免程序Crash。

如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :232203809
微信公众号:终端研发部

(欢迎关注学习和交流)

你可能感兴趣的:(Android总结)