引言
在面试的过程中,经常会被问到的一个经典问题:
如果给定一个1000 * 20000(宽1000px,高20000px)的大图,怎样正常加载显示并且不发生OOM?
我们可以计算一下如果我们将上面所说的大图加载进来,会占用多少内存呢?
图片加载占用多少内存是由下面三个元素决定的:
- 图片原始的宽度和高度
- 图片的色彩空间
- 图片的缩放比
1、图片原始的宽度和高度:图片的宽度和高度的乘积代表了图片总像素点的数量。
2、图片的色彩空间:每个像素点存储的信息,即每个像素点占用了多少字节,默认的情况下每个像素点用Bitmap.Config.ARGB8888来表示,即每个色彩通道占8个比特,即占用4个字节。我们可以通过在BitmapFactory.Options的inPreferredConfig属性进行调整,例如我们经常使用到的是Bitmap.Config.RGB565(不考虑透明度的情况下)。
3、图片的缩放比:即对原始图片的宽高进行缩放,对应的属性是BitmapFactory.Options的inSampleSize,这是采样率的意思,默认值为1,取值范围必须要大于等于1并且是2的倍数。例如,我们将inSampleSize设置为2,表示图片的原始宽高都变为原始的1/2,那么总像素点就变成了原始的1/4。
所以图片占用内存的公式为:
图片占用内存 =(原始宽 * 缩放比)* (原始高 * 缩放比)* 色彩空间
那么我们计算一下上面的图片占用的内存:
1000 * 20000 * 4 = 80000000 字节 = 80MB。
一张大图就占了80M的内存,如果在一些内存比较小的手机中,就很有可能发生OOM的现象。
那么到底应该如何加载这种大尺寸的图片才不至于让其占用太多内存,从而导致OOM出现呢?
图片采样加载
从上面的分析可以知道,图片的占用内存是由图片原始的宽度和高度、图片的色彩空间、图片的缩放比这三个因素决定的,图片的原始宽度和高度不能被改变了,那么我们可以从图片的色彩空间和图片的缩放比进行入手,这就引入了第一种方案,即图片的采样加载。
我们直接来看该方案的代码实现:
private void loadBigImg() {
try {
//从assets中读取图片的输入流
InputStream inputStream = getAssets().open("test.jpg");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
//获取图片的宽高
BitmapFactory.decodeStream(inputStream, null, options);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
//在布局中设置了ImageView的宽高位Match_Parent,因此这里就是屏幕的宽高
int screenWidth = metrics.widthPixels;
int screenHeight = metrics.heightPixels;
inputStream.reset(); //不加这一行,在android 7.0以上的手机图片显示不出来
BitmapFactory.Options newOptions = new BitmapFactory.Options();
newOptions.inJustDecodeBounds = false;
newOptions.inSampleSize = calculateInSampleSize(options, screenWidth, screenHeight);
//图片的色彩空间采用 Bitmap.Config.RGB_565
newOptions.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, newOptions);
mImgView.setImageBitmap(bitmap);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 计算图片缩放比
* @param options 存储着原始图片的宽度和高度
* @param requireWidth 需要缩放后的宽度
* @param requireHeight 需要缩放后的高度
* @return 图片缩放比
*/
private int calculateInSampleSize(BitmapFactory.Options options, int requireWidth, int requireHeight) {
int width = options.outWidth;
int height = options.outHeight;
int sampleSize = 1;
while (width > requireWidth || height > requireHeight) {
sampleSize *= 2;
width = width / 2;
height = height / 2;
}
return sampleSize;
}
上述的代码从图片的色彩空间(采用Bitmap.Config.RGB_565)和图片的缩放比这两方面入手,将大图缩放到屏幕的宽高,最终在手机中图片的显示效果为:
我们可以看到,图片正常显示出来了,图片的高度完整地占据了屏幕高度。我们计算一下采用采样率加载的方式来加载这张大图,需要占用多少内存。
图片的原始宽度为568px,原始高度为16361,色彩空间为2字节,inSampleSize为8,那么所占内存=568 * 16361 * 1/8 * 1/8 * 2 = 290407字节 = 290KB。
可以看到采用该方式的确起到了将大尺寸图片缩放并且不会OOM的效果,但是同时也突出了一个问题,图片显示在屏幕上特别小,导致很多细节看的不清楚。
图片按区域加载
第二种加载方式是图片按区域的加载,这种方式可以做到不对图片进行缩放,只加载原图的局部区域,并通过手势的滑动来更新需要展示的原图区域。
首先我们来认识一下BitmapRegionDecoder这个类。
BitmapRegionDecoder主要用来显示图片的某一块区域,所以它应该提供一个方法来传入图片,并且提供一个方法来显示要展示的区域。
- BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,支持的参数有文件路径、文件描述符、文件的输入流等。
- BitmapRegionDecoder提供了decodeRegion方法来显示指定的区域,该方法支持的参数有Rect对象和BitmapFactory.Options。Rect对象表示需要展示的区域,而BitmapFactory.Options表示展示的区域所用到的inSampleSize、inPreferredConfig属性等。
下面来看具体的实例:
首先我们要自定义一个ImageView,叫做LargeImgView
package com.example.runningh.mydemo.test_big_img;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by hwldzh on 2018/7/8
* 类描述:
*/
public class LargeImgView extends View {
private BitmapRegionDecoder mDecoder;
private int mImgWidth;
private int mImgHeight;
private float mPreX;
private float mPreY;
private static BitmapFactory.Options options = new BitmapFactory.Options();
static {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
private Rect mRect = new Rect();
public LargeImgView(Context context) {
super(context);
}
public LargeImgView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
//传入原始图片的输入流
public void setInputStream(InputStream inputStream) {
try {
mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
mImgWidth = mDecoder.getWidth(); //获取原始图片的宽度
mImgHeight = mDecoder.getHeight(); //获取原始图片的高度
requestLayout();
invalidate();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mDecoder == null) {
super.onDraw(canvas);
return;
}
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bitmap, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//默认从顶端开始展示
mRect.left = 0;
mRect.right = getMeasuredWidth();
mRect.top = 0;
mRect.bottom = getMeasuredHeight();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//这里需要优化的,因为在滑动的过程中不断重绘,肯定会造成卡顿
int action = event.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mPreX = event.getX();
mPreY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float curX = event.getX();
float curY = event.getY();
int deltaX = (int) (curX - mPreX);
int deltaY = (int) (curY - mPreY);
if (mImgWidth > getWidth()) {
mRect.offset(-deltaX, 0);
checkWidth();
invalidate();
}
if (mImgHeight > getHeight()) {
mRect.offset(0, -deltaY);
checkHeight();
invalidate();
}
mPreX = curX;
mPreY = curY;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
private void checkWidth() {
if (mRect.right > mImgWidth) {
mRect.right = mImgWidth;
mRect.left = mImgWidth - getMeasuredWidth();
}
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = getMeasuredWidth();
}
}
private void checkHeight() {
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = getMeasuredHeight();
}
if (mRect.bottom > mImgHeight) {
mRect.bottom = mImgHeight;
mRect.top = mImgHeight - getMeasuredHeight();
}
}
}
Activity:
private void loadBigImg() {
try {
InputStream inputStream = getAssets().open("test.jpg");
mImgView.setInputStream(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.runningh.mydemo.test_big_img.LargeImgView
android:id="@+id/test_img_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
最后展示出来的效果如下图所示:
总结:
使用BitmapRegionFactory显示局部区域,并且没有对图片进行压缩,并且解决了OOM的问题。
但是同时也出现了一个问题就是在滑动的时候会很快,因为每一次滑动都在重绘。
网上的解决方法说可以参考下《世界地图》的项目,将绘制放在单独的线程:
https://github.com/johnnylambada/WorldMap/blob/master/library/src/com/sigseg/android/map/ImageSurfaceView.java
网友评论