java.lang.OutOfMemoryError: kích thước bitmap vượt quá ngân sách VM - Android


159

Tôi đã phát triển một ứng dụng sử dụng nhiều hình ảnh trên Android.

Các ứng dụng chạy cùng một lúc, lấp đầy các thông tin trên màn hình ( Layouts, Listviews, Textviews, ImageViews, vv) và người dùng đọc thông tin.

Không có hình ảnh động, không có hiệu ứng đặc biệt hoặc bất cứ thứ gì có thể lấp đầy bộ nhớ. Đôi khi các drawable có thể thay đổi. Một số là tài nguyên Android và một số là các tệp được lưu trong một thư mục trong SDCARD.

Sau đó, người dùng thoát ( onDestroyphương thức được thực thi và ứng dụng sẽ nằm trong bộ nhớ của VM) và sau đó tại một thời điểm nào đó, người dùng sẽ nhập lại.

Mỗi lần người dùng vào ứng dụng, tôi có thể thấy bộ nhớ ngày càng tăng cho đến khi người dùng nhận được java.lang.OutOfMemoryError.

Vì vậy, cách tốt nhất / chính xác để xử lý nhiều hình ảnh là gì?

Tôi có nên đặt chúng trong các phương thức tĩnh để chúng không được tải mọi lúc không? Tôi có phải làm sạch bố cục hoặc hình ảnh được sử dụng trong bố cục theo cách đặc biệt không?


4
Điều này có thể giúp đỡ nếu bạn có nhiều ngăn kéo thay đổi. Nó hoạt động kể từ khi tôi tự thực hiện chương trình :) androidactivity.wordpress.com/2011/09/24/ Kẻ

Câu trả lời:


72

Có vẻ như bạn bị rò rỉ bộ nhớ. Vấn đề không phải là xử lý nhiều hình ảnh, đó là hình ảnh của bạn sẽ không bị xử lý khi hoạt động của bạn bị phá hủy.

Thật khó để nói lý do tại sao điều này mà không nhìn vào mã của bạn. Tuy nhiên, bài viết này có một số mẹo có thể giúp:

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

Cụ thể, sử dụng các biến tĩnh có khả năng làm cho mọi thứ tồi tệ hơn, không tốt hơn. Bạn có thể cần thêm mã loại bỏ các cuộc gọi lại khi ứng dụng của bạn vẽ lại - nhưng một lần nữa, không có đủ thông tin ở đây để nói chắc chắn.


8
Điều này đúng và cũng có nhiều hình ảnh (và không bị rò rỉ bộ nhớ) có thể gây ra outOfMemory vì nhiều lý do như nhiều ứng dụng khác đang chạy, phân mảnh bộ nhớ, v.v. Tôi đã học được rằng khi làm việc với nhiều hình ảnh nếu bạn nhận được outOfMemory một cách có hệ thống, giống như luôn luôn làm như vậy, sau đó là một rò rỉ. Nếu bạn nhận được nó như một lần một ngày hoặc một cái gì đó, đó là bởi vì bạn quá gần với giới hạn. Đề nghị của tôi trong trường hợp này là cố gắng loại bỏ một số thứ và thử lại. Ngoài ra Bộ nhớ hình ảnh nằm ngoài bộ nhớ Heap, vì vậy hãy chú ý cả hai.
Daniel Benedykt

15
Nếu bạn nghi ngờ rò rỉ bộ nhớ, một cách nhanh chóng để theo dõi việc sử dụng heap là thông qua lệnh adb shell dumpsys meminfo. Nếu bạn thấy việc sử dụng heap tăng lên trong một vài chu kỳ gc (hàng nhật ký GC_ * trong logcat), bạn có thể chắc chắn rằng mình bị rò rỉ. Sau đó tạo một đống heap (hoặc một số tại các thời điểm khác nhau) thông qua adb hoặc DDMS và phân tích nó thông qua công cụ cây thống trị trên MAT MAT. Bạn sẽ sớm tìm ra đối tượng nào có một đống giữ lại tăng theo thời gian. Đó là rò rỉ bộ nhớ của bạn. Tôi đã viết một bài báo với một số chi tiết ở đây: macgyverdev.blogspot.com/2011/11/ Khăn
Johan Norén

98

Một trong những lỗi phổ biến nhất mà tôi thấy khi phát triển Ứng dụng Android là lỗi java.lang.OutOfMemoryError: Kích thước bitmap vượt quá lỗi VM ngân sách. Tôi đã gặp lỗi này thường xuyên đối với các hoạt động sử dụng nhiều bitmap sau khi thay đổi hướng: Hoạt động bị hủy, được tạo lại và các bố cục được thổi phồng từ XML tiêu thụ bộ nhớ VM có sẵn cho bitmap.

Bitmap trên bố trí hoạt động trước đó không được phân bổ chính xác bởi trình thu gom rác vì chúng đã tham chiếu chéo đến hoạt động của chúng. Sau nhiều thí nghiệm tôi đã tìm thấy một giải pháp khá tốt cho vấn đề này.

Đầu tiên, hãy đặt thuộc tính Id id trên chế độ xem chính của bố cục XML của bạn:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

Sau đó, trên onDestroy() phương thức Hoạt động của bạn, hãy gọi unbindDrawables()phương thức chuyển tham chiếu đến Chế độ xem cha và sau đó thực hiện a System.gc().

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

Điều này unbindDrawables()Phương pháp khám phá cây xem đệ quy và:

  1. Loại bỏ các cuộc gọi lại trên tất cả các drawables nền
  2. Loại bỏ trẻ em trên mỗi nhóm xem

10
Ngoại trừ ... java.lang.UnsupportedOperationException: removeAllViews () không được hỗ trợ trong AdapterView

Cảm ơn điều này giúp giảm đáng kể kích thước heap của tôi vì tôi có rất nhiều drawable ... @GJTorikian vì điều đó chỉ cần thử bắt tại removeAllViews () hầu hết các lớp con Viewgroup đều hoạt động tốt.
Eric Chen

5
Hoặc chỉ cần thay đổi điều kiện: if (xem thể hiện của Viewgroup &&! (Xem thể hiện của Adaptor))
Anke

2
@Adam Varhegyi, bạn có thể gọi một cách rõ ràng chức năng tương tự cho chế độ xem thư viện trong onDestroy. Giống như unbindDrawables (galleryView); hy vọng điều này có hiệu quả với bạn ...
hp.android


11

Để tránh vấn đề này, bạn có thể sử dụng phương thức gốc Bitmap.recycle()trước khi đối tượng null-ing Bitmap(hoặc đặt giá trị khác). Thí dụ:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

Và tiếp theo bạn có thể thay đổi cách myBitmapgọi System.gc()như sau:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
điều này sẽ không hoạt động nếu cố gắng thêm các yếu tố đó vào một listview. Bạn có gợi ý nào về điều đó không?
Rafael Sanches

@ddmytrenko Bạn đang tái chế hình ảnh trước khi gán nó. Điều đó có ngăn cản các pixel của nó hiển thị không?
IgorGanapolsky

8

Tôi đã gặp vấn đề chính xác này. Heap khá nhỏ nên những hình ảnh này có thể vượt khỏi tầm kiểm soát khá nhanh liên quan đến bộ nhớ. Một cách là cung cấp cho người thu gom rác một gợi ý để thu thập bộ nhớ trên bitmap bằng cách gọi phương thức tái chế của nó .

Ngoài ra, phương thức onDestroy không được đảm bảo để được gọi. Bạn có thể muốn chuyển logic này / dọn sạch vào hoạt động onPause. Kiểm tra sơ đồ / bảng Vòng đời hoạt động trên trang này để biết thêm thông tin.


Cảm ơn bạn về thông tin. Tôi đang quản lý tất cả các Drawables chứ không phải Bitmap, vì vậy tôi không thể gọi Recycl. Bất kỳ đề nghị khác cho drawables? Cảm ơn Daniel
Daniel Benedykt

Cảm ơn onPauselời đề nghị. Tôi đang sử dụng Bitmaptất cả 4 tab có hình ảnh, vì vậy việc hủy hoạt động có thể không xảy ra quá sớm (nếu người dùng duyệt tất cả các tab).
Azurespot

7

Giải thích này có thể giúp: http://code.google.com.vn/p/android/issues/detail?id=8488#c80

"Mẹo nhanh:

1) KHÔNG BAO GIỜ tự gọi System.gc (). Điều này đã được tuyên truyền như là một sửa chữa ở đây, và nó không hoạt động. Đừng làm việc đó. Nếu bạn nhận thấy trong lời giải thích của tôi, trước khi nhận được OutOfMemoryError, JVM đã chạy bộ sưu tập rác nên không có lý do gì để thực hiện lại (điều này làm chậm chương trình của bạn). Làm một cái ở cuối hoạt động của bạn chỉ là che đậy vấn đề. Nó có thể khiến bitmap được đưa vào hàng đợi hoàn thiện nhanh hơn, nhưng không có lý do gì bạn không thể gọi đơn giản là tái chế trên mỗi bitmap thay thế.

2) Luôn gọi tái chế () trên bitmap bạn không cần nữa. Ít nhất, trong onDestroy của hoạt động của bạn đi qua và tái chế tất cả các bitmap bạn đang sử dụng. Ngoài ra, nếu bạn muốn các cá thể bitmap được thu thập từ đống dalvik nhanh hơn, thì sẽ không hại gì khi xóa bất kỳ tham chiếu nào đến bitmap.

3) Gọi tái chế () và sau đó System.gc () vẫn có thể không xóa bitmap khỏi heap Dalvik. KHÔNG ĐƯỢC QUAN TÂM về điều này. tái chế () đã thực hiện công việc của mình và giải phóng bộ nhớ riêng, sẽ chỉ mất một chút thời gian để thực hiện các bước tôi đã vạch ra trước đó để thực sự xóa bitmap khỏi đống Dalvik. Đây KHÔNG phải là vấn đề lớn vì phần lớn bộ nhớ riêng đã miễn phí!

4) Luôn cho rằng có một lỗi trong khung cuối cùng. Dalvik đang làm chính xác những gì nó phải làm. Nó có thể không phải là những gì bạn mong đợi hoặc những gì bạn muốn, nhưng đó là cách nó hoạt động. "


5

Tôi cũng có chính xác vấn đề đấy. Sau một vài thử nghiệm tôi thấy rằng lỗi này xuất hiện đối với tỷ lệ hình ảnh lớn. Tôi giảm tỷ lệ hình ảnh và vấn đề biến mất.

PS Lúc đầu, tôi đã cố gắng giảm kích thước hình ảnh mà không thu nhỏ hình ảnh xuống. Điều đó đã không dừng lại lỗi.


4
bạn có thể đăng mã về cách bạn đã chia tỷ lệ hình ảnh không, tôi đang đối mặt với cùng một vấn đề và tôi nghĩ điều này có thể giải quyết nó. Cảm ơn!
Gligor

@Gix - Tôi tin rằng anh ta có nghĩa là anh ta phải giảm kích thước của hình ảnh trước khi đưa chúng vào tài nguyên dự án, do đó làm giảm dấu chân bộ nhớ của các hình vẽ. Đối với điều này, bạn nên sử dụng trình chỉnh sửa ảnh bạn chọn. Tôi thích pixlr.com
Kyle Clegg

5

Điểm sau thực sự giúp tôi rất nhiều. Có thể có những điểm khác nữa, nhưng những điều này rất quan trọng:

  1. Sử dụng bối cảnh ứng dụng (thay vì Activity.this) khi có thể.
  2. Dừng và phát hành chủ đề của bạn trong phương thức hoạt động onPause ()
  3. Phát hành lượt xem / cuộc gọi lại của bạn trong phương thức hoạt động của onDestroy ()

4

Tôi đề nghị một cách thuận tiện để giải quyết vấn đề này. Chỉ cần gán giá trị "android: configChanges" như được theo dõi trong Mainfest.xml cho hoạt động bị lỗi của bạn. như thế này:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

giải pháp đầu tiên tôi đưa ra đã thực sự giảm tần suất lỗi OOM xuống mức thấp. Nhưng, nó không giải quyết được vấn đề hoàn toàn. Và sau đó tôi sẽ đưa ra giải pháp thứ 2:

Như OOM chi tiết, tôi đã sử dụng quá nhiều bộ nhớ thời gian chạy. Vì vậy, tôi giảm kích thước hình ảnh trong ~ / res / drawable của dự án của tôi. Chẳng hạn như một bức ảnh đủ tiêu chuẩn có độ phân giải 128X128, có thể được thay đổi kích thước thành 64x64, cũng phù hợp với ứng dụng của tôi. Và sau khi tôi làm như vậy với một đống ảnh, lỗi OOM không xảy ra nữa.


1
Điều này hoạt động vì bạn đang tránh khởi động lại ứng dụng của bạn. Vì vậy, nó không phải là một giải pháp quá nhiều vì nó là một sự tránh né. Nhưng đôi khi đó là tất cả những gì bạn cần. Và phương thức onConfigurationChanged () mặc định trong Activity sẽ chuyển hướng cho bạn, vì vậy nếu bạn không cần bất kỳ thay đổi UI nào để thực sự thích nghi với các hướng khác nhau, bạn có thể hoàn toàn hài lòng với kết quả. Mặc dù vậy, coi chừng những thứ khác có thể khởi động lại bạn. Bạn có thể muốn sử dụng cái này: android: configChanges = "keyboardHidden | direction | keyboard | locale | mcc | mnc | touchscreen | screenLayout | fontScale"
Carl

3

Tôi cũng thất vọng bởi lỗi outofmemory. Và vâng, tôi cũng thấy rằng lỗi này xuất hiện rất nhiều khi thu nhỏ hình ảnh. Lúc đầu, tôi đã thử tạo kích thước hình ảnh cho tất cả các mật độ, nhưng tôi thấy điều này làm tăng đáng kể kích thước của ứng dụng của tôi. Vì vậy, bây giờ tôi chỉ sử dụng một hình ảnh cho tất cả mật độ và nhân rộng hình ảnh của mình.

Ứng dụng của tôi sẽ đưa ra một lỗi ngoài luồng bất cứ khi nào người dùng chuyển từ hoạt động này sang hoạt động khác. Đặt các drawable của tôi thành null và gọi System.gc () không hoạt động, cũng không tái chế bitmapDrawables của tôi với getBitMap (). Recycl (). Android sẽ tiếp tục đưa ra lỗi outofmemory với cách tiếp cận đầu tiên và nó sẽ đưa ra thông báo lỗi canvas bất cứ khi nào nó cố gắng sử dụng bitmap tái chế với cách tiếp cận thứ hai.

Tôi đã thực hiện một cách tiếp cận thậm chí thứ ba. Tôi đặt tất cả các chế độ xem thành null và nền thành màu đen. Tôi thực hiện việc dọn dẹp này trong phương thức onStop () của mình. Đây là phương thức được gọi ngay khi hoạt động không còn hiển thị. Nếu bạn làm điều đó trong phương thức onPause (), người dùng sẽ thấy một nền đen. Không lý tưởng. Đối với việc thực hiện nó trong phương thức onDestroy (), không có gì đảm bảo rằng nó sẽ được gọi.

Để ngăn màn hình đen xảy ra nếu người dùng nhấn nút quay lại trên thiết bị, tôi tải lại hoạt động trong phương thức onRestart () bằng cách gọi các phương thức startActivity (getIntent ()) và sau đó kết thúc ().

Lưu ý: không thực sự cần thiết phải thay đổi nền thành màu đen.


1

Các phương thức BitmapFactory.decode *, được thảo luận trong bài học Tải lớn Bitmap hiệu quả , không nên được thực thi trên luồng UI chính nếu dữ liệu nguồn được đọc từ đĩa hoặc vị trí mạng (hoặc thực sự là bất kỳ nguồn nào khác ngoài bộ nhớ). Thời gian tải dữ liệu này là không thể đoán trước và phụ thuộc vào nhiều yếu tố (tốc độ đọc từ đĩa hoặc mạng, kích thước hình ảnh, sức mạnh của CPU, v.v.). Nếu một trong các tác vụ này chặn luồng UI, hệ thống sẽ đánh dấu ứng dụng của bạn là không phản hồi và người dùng có tùy chọn đóng nó (xem phần Thiết kế cho Phản hồi để biết thêm thông tin).


0

Vâng, tôi đã thử mọi thứ tôi tìm thấy trên internet và không ai trong số họ làm việc. Gọi System.gc () chỉ kéo xuống tốc độ của ứng dụng. Tái chế bitmap trong onDestroy cũng không hiệu quả với tôi.

Điều duy nhất hoạt động bây giờ là có một danh sách tĩnh của tất cả các bitmap để bitmap tồn tại sau khi khởi động lại. Và chỉ sử dụng bitmap đã lưu thay vì tạo cái mới mỗi lần hoạt động nếu được khởi động lại.

Trong trường hợp của tôi, mã trông như thế này:

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

Điều này sẽ không rò rỉ bối cảnh của bạn? (dữ liệu hoạt động)
Rafael Sanches

0

Tôi gặp vấn đề tương tự chỉ với việc chuyển đổi hình nền với kích thước hợp lý. Tôi đã có kết quả tốt hơn với việc đặt ImageView thành null trước khi đưa vào một hình ảnh mới.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

0

FWIW, đây là bộ đệm bitmap nhẹ mà tôi đã mã hóa và đã sử dụng được vài tháng. Đó không phải là tất cả những tiếng chuông và tiếng huýt sáo, vì vậy hãy đọc mã trước khi bạn sử dụng nó.

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.