Android 图像处理之Bitmap系列2

Bitmap缩放代码示例

我们知道如果要对一个图片进行缩放,那么可以采取动画的形式,但是缩放动画并不能自由的控制。如果想要自由的控制图片的缩放,可以参取自定义View的方式,直接上代码
自定义View
MyBitMap.java

public class MyBitMap extends View {

    private Bitmap initialBitmap;
    private Bitmap scaleBitmap;
    private float mCurrentProgress = 0;

    private Paint mPaint;

    public MyBitMap(Context context) {
        super(context);
        init();
    }

    public MyBitMap(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        initialBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pull_image);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.YELLOW);
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = (widthSpecMode == MeasureSpec.EXACTLY) ? widthSpecSize : initialBitmap.getWidth();
        int height = (heightSpecMode == MeasureSpec.EXACTLY) ? heightSpecSize : initialBitmap.getHeight();
        float initRate = (float) initialBitmap.getWidth() / (float) initialBitmap.getHeight();
        float desRate = (float) width / (float) height;
        if (initRate > desRate) {
            scaleBitmap = initialBitmap.createScaledBitmap(initialBitmap, width, (int) (width / initRate), false);
        } else if (initRate < desRate) {
            scaleBitmap = initialBitmap.createScaledBitmap(initialBitmap, (int) (height * initRate), height, false);
        } else {
            scaleBitmap = initialBitmap.createScaledBitmap(initialBitmap, width, height, false);
        }

        setMeasuredDimension(width, height);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawRect(new Rect(0, 0, getMeasuredWidth(), getMeasuredHeight()), mPaint);
        canvas.scale(mCurrentProgress, mCurrentProgress, getMeasuredWidth() / 2, getMeasuredHeight() / 2);
        canvas.drawBitmap(scaleBitmap, (getMeasuredWidth() - scaleBitmap.getWidth()) / 2, (getMeasuredHeight() - scaleBitmap.getHeight()) / 2, null);

    }

    public void setCurrentProgress(float currentProgress) {
        mCurrentProgress = currentProgress;
    }
}

主界面文件activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <SeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:max="100"
        android:progress="0"/>

    <com.lbb.demo.MyBitMap
        android:id="@+id/first_view"
        android:layout_width="90dp"
        android:layout_height="90dp"
        android:layout_centerInParent="true"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="30dp"/>
</RelativeLayout>

主activity

public class MainActivity extends Activity {
    private SeekBar seekBar;
    private MyBitMap mFirstView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        seekBar = (SeekBar) findViewById(R.id.seekbar);
        mFirstView = (MyBitMap) findViewById(R.id.first_view);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                float current = (float) progress / (float) seekBar.getMax();
                mFirstView.setCurrentProgress(current);
                mFirstView.postInvalidate();
            }
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
            }
        });
    }
}

最后的运行结果:

Bitmap对象的回收

参考 Android 那些你所不知道的Bitmap对象详解

Android2.3.3(API 10)及之前的版本中,Bitmap对象与其像素数据是分开存储的,Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)中或者说Derict Memory(直接内存)中,这使得存储在Native Memory中的像素数据的释放是不可预知的,我们可以调用recycle()方法来对Native Memory中的像素数据进行释放,前提是你可以清楚的确定Bitmap已不再使用了,如果你调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现”Canvas: trying to use a recycled bitmap”错误,而在Android3.0(API 11)之后,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做

最佳实践

    public Bitmap test() {
        Bitmap result;
        Bitmap temp = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
        if (temp != null) {
            //Android2.3.3之前的版本立即释放bitmap在native heap本地堆中的像素数据,Android2.3.3之后的版本,像素数据是放在java heap中的,所以像素数据并没有立刻得到释放,此时需要等到方法域过了,同时下一次gc才会随着bitmap对象的释放而释放像素数据
            //但是此时isRecycled还是返回true的,所以如果对该bitmap draw的话还是会抛异常
            temp.recycle();

            //这句话其实是没啥用的,因为temp本身是一个局部变量,引用在栈中,bitmap对象在堆中,所以过了这个方法域,那么如果发生了GC,该堆中的bitmap因为没有强引用
            //指向所以会被回收的,跟普通的对象回收其实没啥区别
            //temp = null;
        }
        result = Bitmap.createScaledBitmap(temp,
                desiredWidth, desiredHeight, true);
        return result;
    }

temp这个bitmap不使用了所以调用recycle方法,如果在Android2.3.3之前的版本那么会立即释放bitmap的本地内存,之后的版本会把bitmap的mRecycled属性置为true,表示该bitmap不能再被使用了,所以该bitmap不能被draw了,所以下一次发生GC的时候,java堆中的bitmap对象就会被回收。另一方面不调用recycle方法也是可以的,因为现在手机大多数都是在Android2.3.3之上,temp是一个局部引用,所以方法域一过java堆中的bitmap对象肯定可以被回收的,所以recycle意义不大,虽然如果但是调用recycle方法仍然是个很好的习惯

temp = null; 不要这样做,没啥效果,上面有解释

源码

bitmap.java

    public void recycle() {
        if (!mRecycled && mFinalizer.mNativeBitmap != 0) {
            if (nativeRecycle(mNativeBitmap)) {
                // return value indicates whether native pixel object was actually recycled.
                // false indicates that it is still in use at the native level and these
                // objects should not be collected now. They will be collected later when the
                // Bitmap itself is collected.
                mBuffer = null;
                mNinePatchChunk = null;
            }
            mRecycled = true; //不管怎么样,此时mRecycled属性为true
        }
    }
    /**
     * Returns true if this bitmap has been recycled. If so, then it is an error
     * to try to access its pixels, and the bitmap will not draw.
     *
     * @return true if the bitmap has been recycled
     */
    public final boolean isRecycled() { //不管是在Android2.3.3之前还是之后,调用了recycle方法,此时返回一定是true
        return mRecycled;
    }

nativeRecycle(mNativeBitmap) 看代码的注释,指示本地的像素数据是否会回收了,false表示还不能被回收只有等到bitmap自身被回收才能回收像素数据,
自己做了下测试,android 5.0版本,调试,可以看到此时返回false的,所以此时是否也表明了此时像素数据是放在java堆中的,Android2.3.3版本以下没做测试,但是我觉得此时应该返回true,因为此时像素数据放在了本地heap嘛,所以立即得到释放,为什么得到立即释放,因为c/c++堆中的内存的释放不需要等GC,直接调用free/delete方法即可

Canvas.java

    public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) {
        throwIfCannotDraw(bitmap);
        native_drawBitmap(mNativeCanvasWrapper, bitmap.ni(), left, top,
                paint != null ? paint.mNativePaint : 0, mDensity, mScreenDensity, bitmap.mDensity);
    }
    protected static void throwIfCannotDraw(Bitmap bitmap) {
        if (bitmap.isRecycled()) {
            throw new RuntimeException("Canvas: trying to use a recycled bitmap " + bitmap);
        }
        .........
    }

看到了吧,所以如果对一个调用了recycle方法的bitmap再进行draw就会报Canvas: trying to use a recycled bitmap 运行时异常了

解码bitmap优化意见
BitmapFactory.Options.inBitmap的这个字段,假如这个字段被设置了,我们在解码Bitmap的时候,他会去重用inBitmap设置的Bitmap,减少内存的分配和释放,提高了应用的性能,然而在Android 4.4之前,BitmapFactory.Options.inBitmap设置的Bitmap必须和我们需要解码的Bitmap的大小一致才行,在Android4.4以后,BitmapFactory.Options.inBitmap设置的Bitmap大于或者等于我们需要解码的Bitmap的大小就OK了

额外话

堆内存释放

java 堆内存的回收是靠GC的,程序不能自己控制
但是C/C++ 堆内存的回收,就需要自己去释放了
malloc,free是c语言中用法
new,delete是c++中用法

栈内存嘛,过了方法域就自动释放咯

bitmap组成

一部分是图片的相关描述信息,另一部分就是最重要的像素信息(这部分是有byte数组组成的)

android堆内存模型

Java堆 java中new出来的对象
Native堆 c/c++ malloc/new出来的对象

android中每个App的Java堆内存大小都是被严格的限制的,Native堆是没有做任何限制的。每个对象都是使用Java的new在堆内存实例化,这是内存中相对安全的一块区域。内存有垃圾回收机制,所以当App不在使用内存的时候,系统GC就会自动把java堆内存回收,并不会回收Native堆

当java new申请的内存超过java堆的阈值时,就会抛出OOM异常.程序发生OMM并不表示RAM不足,而是因为程序申请的java heap对象超过了dalvik vm heapgrowthlimit。也就是说,在RAM充足的情况下,也可能发生OOM。(这个阈值可以是48M、24M、16M等,视机型而定),可以通过adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。

native heap的增长并不受dalvik vm heapsize的限制
只要RAM有剩余空间,程序员可以一直在native heap上申请空间,当然如果 RAM快耗尽,memory killer会杀进程释放RAM。大家使用一些软件时,有时候会闪退,就可能是软件在native层申请了比较多的内存导致的。比如,我就碰到过UC web在浏览内容比较多的网页时闪退,原因就是其native heap增长到比较大的值,占用了大量的RAM,被memory killer杀掉了。

native heap的释放不受java GC的控制,必须手动释放
不幸的是,内存进行垃圾回收的过程正是问题所在。当内存进行垃圾回收时,内存不仅仅进行了垃圾回收,还把 Android 应用完全终止了。这也是用户在使用 App 时最常见的卡顿或短暂假死的原因之一。这会让正在使用 App 的用户非常郁闷,然后他们可能会焦躁地滑动屏幕或者点击按钮,但 App 唯一的响应就是:在 App 恢复正常之前,请求用户耐心等待.相比之下,Native堆是由C++程序的new进行分配的。在Native堆里面有更多可用内存,App只被设备的物理可用内存限制,而且没有垃圾回收机制或其他东西拖后腿。但是c++程序员必须自己回收所分配的每一块内存,否则就会造成内存泄露,最终导致程序崩溃

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值