圖像載入的方式:
Android開發中消耗內存較多通常都是在圖像上面。本文就主要介紹如何正確的展示圖像下降對內存的開銷,有效的避免oom現象。
首先咱們知道個人獲取圖像的來源通常有三種源頭:
1.從網絡載入
2.從文件讀取
3.從資源文件載入
針對這三種狀況咱們通常使用BitmapFactory的:decodeStream,
decodeFile,decodeResource,這三個函數來獲取到bitmap而後再調用ImageView的setImageBitmap函數進行展示。
咱們的內存去哪裏了(爲何被消耗了這麼多):
事實上咱們的內存就是去bitmap裏了,BitmapFactory的每個decode函數都會生成一個bitmap對象,用於存放解碼後的圖像,而後返回該引用。假設圖像數據較大就會形成bitmap對象申請的內存較多。假設圖像過多就會形成內存不夠用天然就會出現out of memory的現象。java
如何纔是正確的載入圖像:
咱們知道咱們的手機屏幕有着必定的分辨率(如:840*480),圖像也有本身的像素(如高清圖片:1080*720)。假設將一張840*480的圖片載入鋪滿840*480的屏幕上這就是最合適的了,此時顯示效果最好。假設將一張1080*720的圖像放到840*480的屏幕並不會獲得更好的顯示效果(和840*480的圖像顯示效果是一致的),反而會浪費不少其它的內存。
咱們通常的作法是將一張網絡獲取的照片或拍攝的照片放到一個必定大小的控件上面進行展示。這裏就以nexus 5x手機拍攝的照片爲例說明,其攝像頭的像素爲1300萬(拍攝圖像的分辨率爲4032×3024),而屏幕的分辨率爲1920x1080。其攝像頭的分辨率要比屏幕的分辨率大得多,假設不正確圖像進行處理就直接顯示在屏幕上。就會浪費掉很是多的內存(假設內存不夠用直接就oom了),而且並無達到更好的顯示效果。
爲了下降內存的開銷。咱們在載入圖像時就應該參照控件(如:263pixel*263pixel)的寬高像素來獲取合適大小的bitmap。android
如下就一邊看代碼一邊解說:git
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的時候咱們可以將其傳入來對圖像進行相關設置。github
這裏咱們主要介紹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了。markdown
這裏需要注意的是假設咱們decodeFile解析的文件是外部存儲裏的文件,咱們需要在Manifists加上文件的讀寫權限,否則獲取的bitmap會爲null.網絡
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
同理咱們編寫decodeResource的重載函數app
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的緣由。ide
因此咱們對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載入方式的詢問。纔有了我第二次對文章的修該,歡迎你們點多多關注個人博客。相應文章的提問我都會盡可能及時回答和改動我不正確的地方。