Android 多種方式正確的加載圖像,有效避免oom

jopen 8年前發布 | 16K 次閱讀 Android開發 移動開發

【原文地址:http://blog.csdn.net/ys408973279/article/details/50269593】

圖像加載的方式:

        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了。

同理我們編寫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);
}

這里需要注意的是如果我們decodeFile解析的文件是外部存儲里的文件,我們需要在Manifists加上文件的讀寫權限,不然獲取的bitmap會為null.

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

然后是decodeStream的相關重載:

 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();
    }

        我們發現在處理stream的時候我們并不是同之前一樣通過調用兩次decodeStream函數來進行設置的,而是將stream轉化成byte[],然后在兩次調用decodeByteArray。其原因是:如果我們兩次調用按照兩次調用decodeStream的方式,會發現我們得到到bitmap為null

內存對比

        這樣我們加載相關代碼就完成了,最后我們通過一個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>

界面很簡單:就是一個用拍照的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);
                try {
                    if (file.exists()) {
                        file.delete();
                    }
                    file.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                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) {
                    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                            != PackageManager.PERMISSION_GRANTED) {
                        //申請WRITE_EXTERNAL_STORAGE權限
                        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                                0);
                    }

                    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加載的:

沒拍攝照片前:

1.jpg

拍攝照片后:

2.png


直接加載的方式:

沒拍攝照片前:

3.png

拍攝照片后:

4.png

        相信看到內存對比圖后也不用我再多說什么了吧,最后將所有代碼上傳至GitHub:包含了所有加載函數,還有拍攝相機的demo,其中github里的代碼比文章里的要多一些,里面還分別測試了從stream里和rersouces里加載圖片ps:對于不同手機運行直接加載圖像方式的時候可能會不能正在運行,直接就oom了。


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

來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2015/1212/3770.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!