图像载入的方式:

       Android开发中消耗内存较多一般都是在图像上面。本文就主要介绍如何正确的展现图像降低对内存的开销,有效的避免oom现象。

首先我们知道我的获取图像的来源一般有三种源头:

1.从网络载入

2.从文件读取

3.从资源文件载入

       针对这三种情况我们一般使用BitmapFactory的:decodeStream,

decodeFile,decodeResource,这三个函数来获取到bitmap然后再调用ImageView的setImageBitmap函数进行展现。

我们的内存去哪里了(为什么被消耗了这么多):

       事实上我们的内存就是去bitmap里了,BitmapFactory的每一个decode函数都会生成一个bitmap对象,用于存放解码后的图像,然后返回该引用。假设图像数据较大就会造成bitmap对象申请的内存较多。假设图像过多就会造成内存不够用自然就会出现out of memory的现象。

如何才是正确的载入图像:

       我们知道我们的手机屏幕有着一定的分辨率(如:840*480),图像也有自己的像素(如高清图片:1080*720)。假设将一张840*480的图片载入铺满840*480的屏幕上这就是最合适的了,此时显示效果最好。假设将一张1080*720的图像放到840*480的屏幕并不会得到更好的显示效果(和840*480的图像显示效果是一致的),反而会浪费很多其它的内存。

       我们一般的做法是将一张网络获取的照片或拍摄的照片放到一个一定大小的控件上面进行展现。这里就以nexus 5x手机拍摄的照片为例说明,其摄像头的像素为1300万(拍摄图像的分辨率为4032×3024),而屏幕的分辨率为1920x1080。其摄像头的分辨率要比屏幕的分辨率大得多,假设不正确图像进行处理就直接显示在屏幕上。就会浪费掉非常多的内存(假设内存不够用直接就oom了),并且并没有达到更好的显示效果。

       为了降低内存的开销。我们在载入图像时就应该參照控件(如:263pixel*263pixel)的宽高像素来获取合适大小的bitmap。

以下就一边看代码一边解说:

    public static Bitmap getFitSampleBitmap(String file_path, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file_path, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(file_path, options);
}
public static int getFitInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {
int inSampleSize = 1;
if (options.outWidth > reqWidth || options.outHeight > reqHeight) {
int widthRatio = Math.round((float) options.outWidth / (float) reqWidth);
int heightRatio = Math.round((float) options.outHeight / (float) reqHeight);
inSampleSize = Math.min(widthRatio, heightRatio);
}
return inSampleSize;
}

BitmapFactory提供了BitmapFactory.Option,用于设置图像相关的參数。在调用decode的时候我们能够将其传入来对图像进行相关设置。

这里我们主要介绍option里的两个成员:inJustDecodeBounds(Boolean类型) 和inSampleSize(int类型)。

inJustDecodeBounds :假设设置为true则表示decode函数不会生成bitmap对象,仅是将图像相关的參数填充到option对象里,这样我们就能够在不生成bitmap而获取到图像的相关參数了。

       inSampleSize:表示对图像像素的缩放比例。假设值为2。表示decode后的图像的像素为原图像的1/2。

在上面的代码里我们封装了个简单的getFitInSampleSize函数(将传入的option.outWidth和option.outHeight与控件的width和height相应相除再取当中较小的值)来获取一个适当的inSampleSize。

       在设置了option的inSampleSize后我们将inJustDecodeBounds设置为false再次调用decode函数时就能生成bitmap了。

这里须要注意的是假设我们decodeFile解析的文件是外部存储里的文件,我们须要在Manifists加上文件的读写权限,不然获取的bitmap会为null.

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

同理我们编写decodeResource的重载函数

 public static Bitmap getFitSampleBitmap(Resources resources, int resId, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resId, options);
}

对于decodeStream重载,和从file中载入和从resource中载入稍有不同,因对stream是一种有顺序的字符流,对其decode一次后,其顺序就会发生变化,再次进行第二次decode的时候就不能解码成功了。这也是为什么当我们对inputStream decode两次的时候会得到一个null值的bitmap的原因。

所以我们对stream类型的源须要进行转换,转换有两种思路:

1. 将inputStream的字节流读取后放到一个byte[]数组里。然后使用BitmapFactory.decodeByteArray两次decode进行压缩——可是发现这样的方法事实上治标不治本,不建议使用。详细原因接下来会介绍。

2. 将inputStream的字节流读取到一个文件中,然后通过处理file的方式来进行处理就可以——推荐。优点多,后面介绍。

1.通过decodeByteArray的形式:

public static Bitmap getFitSampleBitmap(InputStream inputStream, int width, int height) throws Exception {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
byte[] bytes = readStream(inputStream);
//BitmapFactory.decodeStream(inputStream, null, options);
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
// return BitmapFactory.decodeStream(inputStream, null, options);
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
} /*
* 从inputStream中获取字节流 数组大小
* */
public static byte[] readStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
outStream.close();
inStream.close();
return outStream.toByteArray();
}

我们发现这里的处理方式大致一看还能够,然后我们会发如今readStream函数中会返回一个byte[]数组,在这个数组的大小即为原始图像的大小,因此并没有起到节省内存的效果。

因此推荐使用第二中方式通过保存本地文件后再解码

 public static Bitmap getFitSampleBitmap(InputStream inputStream, String catchFilePath,int width, int height) throws Exception {
return getFitSampleBitmap(catchStreamToFile(catchFilePath, inputStream), width, height);
}
/*
* 将inputStream中字节流保存至文件
* */
public static String catchStreamToFile(String catchFile,InputStream inStream) throws Exception { File tempFile=new File(catchFile);
try {
if (tempFile.exists()) {
tempFile.delete();
}
tempFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream fileOutputStream=new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
}
inStream.close();
fileOutputStream.close();
return catchFile;
}

这里我们能够看到,我们通过调用catchStreamToFile先将文件保存到指定文件名称里,然后再利用两次decodeFile的形式来处理stream流的。

这样做的优点是什么呢:

1.避免了超大的中间内存变量的生成,所以自然就避免了oom现象。

2.对于从file和resource中载入图片其本质都是从文件中载入图片的。

3.一般inputStream都是应用于网络中获取图片的方式。我们採用了用文件进行缓存的方式进行图片载入还有效的避免了来回切换activity页面时多次从网络中下载同一种图片。从而造成的卡顿现象,使用这样的方法,我们载入一次后,再进行第二次载入时,我们能够推断下是否是和第一次载入时的url是一致的,假设是那么直接从使用getFitSampleBitmap file的重载从第一次缓存的catchfile中载入就可以,这样大大提高了载入速度(在主程序里我们能够用一个map变量保存下url和catchFileName的相应关系)。

内存对照

       这样我们载入相关代码就完毕了。最后我们通过一个demo来对照下正确载入图像和不处理的载入图像时的内存消耗吧。这里我们就写一个手机拍摄头像的程序吧。

还是一样一边看代码一边解说吧:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
tools:context=".Activity.MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" /> <ImageView
android:layout_margin="32dp"
android:id="@+id/img_preview"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/res_photo"
/>
<Button
android:id="@+id/btn_take_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TAKE PHOTO"/>
</LinearLayout>

界面非常easy:就是一个用拍照的Button和一个用于显示头像的ImageView,当中ImageView大小为100dp*100dp.

java代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mTakePhoneButton;
private ImageView mPreviewImageView;
public static final int TAKE_PHOTO = 0;
private String photoPath = Environment.getExternalStorageDirectory() + "/outout_img.jpg";
private Uri imageUri; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
init();
mTakePhoneButton.setOnClickListener(this);
} private void init() {
mTakePhoneButton = (Button) findViewById(R.id.btn_take_photo);
mPreviewImageView = (ImageView) findViewById(R.id.img_preview);
} @Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_take_photo:
File file = new File(photoPath);
imageUri = Uri.fromFile(file);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
break; }
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
Bitmap bitmap = null;
int requestWidth = mPreviewImageView.getWidth();
int requestHeight = mPreviewImageView.getHeight();
//不处理直接载入
bitmap = BitmapFactory.decodeFile(photoPath);
//缩放后载入:从file中载入
// bitmap = BitmapUtils.getFitSampleBitmap(photoPath,
// requestWidth, requestHeight);
mPreviewImageView.setImageBitmap(bitmap); }
break;
}
}
}

这里简单的实现了一个调用相机的功能,点击button调用系统自带相机。然后再onActivityResult里载入拍摄的照片。

这里我们重点关注载入照片的部分:

Bitmap bitmap = null;
int requestWidth = mPreviewImageView.getWidth();
int requestHeight = mPreviewImageView.getHeight();
//不处理直接载入
bitmap = BitmapFactory.decodeFile(photoPath);
//缩放后载入:从file中载入
// bitmap = BitmapUtils.getFitSampleBitmap(photoPath,
// requestWidth, requestHeight);
mPreviewImageView.setImageBitmap(bitmap);

这里提供了两种载入照片的方式:

1.不做不论什么处理直接载入。

2.就是调用我们之前写的代码缩放后载入(这里的BitmapUtils就是将之前的代码封装成的一个工具类)。

最后我们看看在两种方式下分别的内存消耗对照图吧:

调用BitmapUtils载入的:

没拍摄照片前:



拍摄照片后:

直接载入的方式:

没拍摄照片前:



拍摄照片后:

我们能够大致计算下,在没有採用压缩方式处理的时候:

图片分辨率为4032×3024採用的是RGB_8888编码:即每一个像素点占用4个字节。因此载入一张高清图片所用到的内存大小=4032×3024×4/1024/1024=40+M.

而採用正确的载入方式呢(其屏幕显示效果一致):

图片所用到的内存大小=263×263×4/1024/1024=0.26M.

最后将全部代码上传至GitHub:包括了所以载入函数。还有拍摄相机的demo,当中github里的代码比文章里的要多一些,里面还分别測试了从stream里和rersouces里载入图片

ps:对于不同手机执行直接载入图像方式的时候可能会不能正在执行,直接就oom了。

地址:https://github.com/CoolThink/EfficientLoadingPicture.git(欢迎加星或fork)

最后感谢github上xumengyin对inputStream载入方式的询问。才有了我第二次对文章的修该,欢迎大家点多多关注我的博客。相应文章的提问我都会尽量及时回答和改动我不正确的地方。

Android 多种方式正确的载入图像,有效避免oom的更多相关文章

  1. Android 多种方式正确的加载图像,有效避免oom

    图像加载的方式: Android开发中消耗内存较多一般都是在图像上面,本文就主要介绍怎样正确的展现图像减少对内存的开销,有效的避免oom现象.首先我们知道我的获取图像的来源一般有三种源头:1.从网络加 ...

  2. Android多种方式实现相机圆形预览

    效果图如下: 一.为预览控件设置圆角 为控件设置ViewOutlineProvider public RoundTextureView(Context context, AttributeSet at ...

  3. android 设置字体颜色、EditText自己主动输入转换成大写字母的多种方式

    在TextView上面设置某一个字的字体颜色为指定颜色时,能够通过java类SpannableString类和Html语言来实现. (一)SpannableString类方式 private void ...

  4. Spring学习总结(一)——Spring实现IoC的多种方式

    控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法,也有人认为DI只是IoC的另一种说法.没有IoC的程序中我们使用面向对象编程对象的创 ...

  5. idea打包jar的多种方式

    这里总结出用IDEA打包jar包的多种方式,以后的项目打包Jar包可以参考如下形式: 用IDEA自带的打包形式 用Maven插件maven-shade-plugin打包 用Maven插件maven-a ...

  6. PHP 载入图像 imagecreatefrom_gif_jpeg_png 系列函数

    imagecreatefrom 系列函数用于从文件或 URL 载入一幅图像. 载入图像 imagecreatefrom 系列函数用于从文件或 URL 载入一幅图像,成功返回图像资源,失败则返回一个空字 ...

  7. PDO多种方式取得查询结果

    PDO多种方式取得查询结果 01 December 2009 1:26 Tuesday by Sjolzy PDO最大的特点之一是它的灵活性,本节将介绍如何取得查询结果,包括: 数组(数值或关联数组) ...

  8. java 获取classpath下文件多种方式

    java 获取classpath下文件多种方式 一:properties下配置 在resources下定义server.properties register.jks.path=classpath\: ...

  9. Jquery Validate 表单验证的多种方式

    ASP.NET MVC Jquery Validate 表单验证的多种方式 在我们日常开发过程中,前端的表单验证很重要,如果这块处理不当,会出现很多bug .但是如果处理的好,不仅bug会很少,用户体 ...

随机推荐

  1. cygwin下调用make出现的奇怪现象

    <lenovo@root 11:48:03> /cygdrive/d/liuhang/GitHub/rpi_linux/linux$make help 1 [main] make 4472 ...

  2. unity SystemInfo类 获得电量battery

    我觉得用Unity 开发最爽的地方, 不是unity跨平台,而是用其他语言,要用很复杂的逻辑才能完成的功能,unity用一两句代码就能搞定 就比如说获取Android 系统的电量,不用发广播,不用申请 ...

  3. Apache CXF实战之二 集成Sping与Web容器

    本文链接:http://blog.csdn.net/kongxx/article/details/7525481 Apache CXF实战之一 Hello World Web Service 书接上文 ...

  4. NYOJ_77 开灯问题

    题目地址 分析: 用一个数组来保存每盏灯的操作的次数.推断奇偶就可以推断灯的状态. 最后的输出格式须要注意一下空格的位置,思路就是现输出一个.剩下来的输出在前面加一个空格. 空格用_表示: 1_3_5 ...

  5. OpenCV图像处理篇之边缘检測算子

    3种边缘检測算子 灰度或结构等信息的突变位置是图像的边缘,图像的边缘有幅度和方向属性.沿边缘方向像素变化缓慢,垂直边缘方向像素变化剧烈.因此,边缘上的变化能通过梯度计算出来. 一阶导数的梯度算子 对于 ...

  6. CentOS下安装C/C++开发工具包的最佳方式

    如果你使用的是 Fedora, Red Hat, CentOS, 或者 Scientific Linux 系统,使用下面的命令安装GNU的C/C++开发包和编译器. # yum groupinstal ...

  7. 开源3D游戏引擎Irrlicht简介

    Irrlicht简介 Irrlicht在国内也被叫做"鬼火"引擎,是一款用C++编写的开放源代码的高性能游戏引擎.而且是跨平台的,具有很好的移植性,Irrlicht支持OpenGl ...

  8. Coderfroces 862 B . Mahmoud and Ehab and the bipartiteness

     Mahmoud and Ehab and the bipartiteness Mahmoud and Ehab continue their adventures! As everybody in ...

  9. Huawei配置两台交换机堆叠示例

    配置两台交换机堆叠示例(先配置后连线方式,推荐) 一.基本概念 在堆叠中,有以下一些基本概念,如图1所示.图1 堆叠基本概念示意图 1. 角色堆叠中的单台交换机称为成员交换机,按照功能不同可以分为以下 ...

  10. JS取出两个数组的不同或相同元素

    JS合并两个数组的方法 我们在项目过程中,有时候会遇到需要将两个数组合并成为一个的情况.比如: var a = [1,2,3]; var b = [4,5,6]; 有两个数组a.b,需求是将两个数组合 ...