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对象的回收
在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++程序员必须自己回收所分配的每一块内存,否则就会造成内存泄露,最终导致程序崩溃。