มีปัญหาหน่วยความจำแปลก ๆ ในขณะที่โหลดรูปภาพไปยังวัตถุบิตแมป


1288

ฉันมีมุมมองรายการพร้อมปุ่มภาพสองปุ่มในแต่ละแถว เมื่อคุณคลิกแถวรายการมันจะเปิดกิจกรรมใหม่ ฉันต้องสร้างแท็บของตัวเองเพราะมีปัญหากับเลย์เอาต์ของกล้อง กิจกรรมที่เปิดตัวสำหรับผลลัพธ์คือแผนที่ หากฉันคลิกที่ปุ่มของฉันเพื่อเปิดภาพตัวอย่าง (โหลดภาพออกจากการ์ด SD) แอปพลิเคชันจะส่งกลับจากกิจกรรมกลับไปที่listviewกิจกรรมไปยังตัวจัดการผลลัพธ์เพื่อเปิดใช้งานกิจกรรมใหม่ของฉันซึ่งไม่มีอะไรมากไปกว่าวิดเจ็ตรูปภาพ

ListAdapterตัวอย่างภาพในมุมมองรายการจะถูกดำเนินการด้วยเคอร์เซอร์และ มันทำให้มันค่อนข้างง่าย แต่ฉันไม่แน่ใจว่าฉันจะใส่ภาพที่ปรับขนาดได้อย่างไร (Ie Smaller บิตขนาดเล็กไม่ใช่พิกเซลเป็นปุ่มsrcสำหรับภาพทันทีฉันเลยปรับขนาดรูปภาพที่หลุดจากกล้องโทรศัพท์

ปัญหาคือฉันได้รับข้อผิดพลาดหน่วยความจำไม่เพียงพอเมื่อพยายามย้อนกลับและเปิดใช้งานกิจกรรมที่ 2 อีกครั้ง

  • มีวิธีที่ฉันสามารถสร้างรายการอะแดปเตอร์ได้อย่างง่ายดายทีละแถวที่ฉันสามารถปรับขนาดได้ทันที ( bit ฉลาด )?

สิ่งนี้จะดีกว่าเนื่องจากฉันยังต้องทำการเปลี่ยนแปลงคุณสมบัติของวิดเจ็ต / องค์ประกอบในแต่ละแถวเนื่องจากฉันไม่สามารถเลือกแถวด้วยหน้าจอสัมผัสได้เนื่องจากปัญหาโฟกัส ( ฉันสามารถใช้ลูกลูกกลิ้ง )

  • ฉันรู้ว่าฉันสามารถปรับขนาดวงนอกและบันทึกภาพของฉันได้ แต่นั่นไม่ใช่สิ่งที่ฉันต้องการทำจริงๆ

ทันทีที่ฉันปิดการใช้งานภาพในมุมมองรายการมันทำงานได้ดีอีกครั้ง

FYI: นี่คือวิธีที่ฉันทำ:

String[] from = new String[] { DBHelper.KEY_BUSINESSNAME,DBHelper.KEY_ADDRESS,DBHelper.KEY_CITY,DBHelper.KEY_GPSLONG,DBHelper.KEY_GPSLAT,DBHelper.KEY_IMAGEFILENAME  + ""};
int[] to = new int[] {R.id.businessname,R.id.address,R.id.city,R.id.gpslong,R.id.gpslat,R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);

ที่ไหนเป็นR.id.imagefilenameButtonImage

นี่คือ LogCat ของฉัน:

01-25 05:05:49.877: ERROR/dalvikvm-heap(3896): 6291456-byte external allocation too large for this process.
01-25 05:05:49.877: ERROR/(3896): VM wont let us allocate 6291456 bytes
01-25 05:05:49.877: ERROR/AndroidRuntime(3896): Uncaught handler: thread main exiting due to uncaught exception
01-25 05:05:49.917: ERROR/AndroidRuntime(3896): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:149)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:174)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.drawable.Drawable.createFromPath(Drawable.java:729)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.resolveUri(ImageView.java:484)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.setImageURI(ImageView.java:281)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.setViewImage(SimpleCursorAdapter.java:183)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.bindView(SimpleCursorAdapter.java:129)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.CursorAdapter.getView(CursorAdapter.java:150)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.obtainView(AbsListView.java:1057)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.makeAndAddView(ListView.java:1616)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.fillSpecific(ListView.java:1177)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.layoutChildren(ListView.java:1454)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.onLayout(AbsListView.java:937)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1108)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:922)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:999)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:920)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.performTraversals(ViewRoot.java:771)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1103)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Handler.dispatchMessage(Handler.java:88)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Looper.loop(Looper.java:123)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.app.ActivityThread.main(ActivityThread.java:3742)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invokeNative(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invoke(Method.java:515)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at dalvik.system.NativeStart.main(Native Method)
01-25 05:10:01.127: ERROR/AndroidRuntime(3943): ERROR: thread attach failed 

ฉันยังมีข้อผิดพลาดใหม่เมื่อแสดงภาพ:

01-25 22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
01-25 22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
01-25 22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
01-25 22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
01-25 22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed

8
ฉันแก้ไขได้โดยหลีกเลี่ยง Bitmap.decodeStream หรือ decodeFile และใช้วิธี BitmapFactory.decodeFileDescriptor
Fraggle

1
ฉันยังประสบปัญหาที่คล้ายกันสองสามสัปดาห์ก่อนและฉันแก้ไขมันโดยการปรับขนาดภาพลงเพื่อจุดที่เหมาะสมที่สุด ฉันได้เขียนวิธีการที่สมบูรณ์ในบล็อกของฉันcodingjunkiesforum.wordpress.com/2014/06/12/และอัปโหลดตัวอย่างโครงการที่สมบูรณ์ด้วยรหัสแนวโน้ม OOM เทียบกับรหัสหลักฐาน OOM athttps: //github.com/shailendra123/BitmapHandlingDemo
Shailendra Singh

5
คำตอบที่ได้รับการยอมรับสำหรับคำถามนี้กำลังถูกกล่าวถึงในmeta
rene

4
สิ่งนี้จะเกิดขึ้นเมื่อคุณไม่ได้อ่านคู่มือผู้พัฒนา Android
Pedro Varela

2
สิ่งนี้เกิดขึ้นเพราะสถาปัตยกรรมของ Android ไม่ดี ควรปรับขนาดภาพเองเช่น ios และ UWP ทำเช่นนี้ ฉันไม่ต้องทำสิ่งนี้ด้วยตัวเอง นักพัฒนา Android คุ้นเคยกับนรกนั้นและคิดว่ามันทำงานได้อย่างที่ควรจะเป็น
เข้าถึงถูกปฏิเสธ

คำตอบ:


650

Android การฝึกอบรมระดับ " แสดงบิตแมปได้อย่างมีประสิทธิภาพ " ข้อเสนอบางข้อมูลที่ดีสำหรับการทำความเข้าใจและการจัดการกับข้อยกเว้นjava.lang.OutOfMemoryError: bitmap size exceeds VM budgetเมื่อโหลดบิตแมป


อ่านบิตแมปขนาดและประเภท

BitmapFactoryชั้นมีวิธีการถอดรหัสหลายคน ( decodeByteArray(), decodeFile(), decodeResource()ฯลฯ ) สำหรับการสร้างBitmapจากแหล่งต่างๆ เลือกวิธีการถอดรหัสที่เหมาะสมที่สุดตามแหล่งข้อมูลภาพของคุณ วิธีการเหล่านี้พยายามที่จะจัดสรรหน่วยความจำสำหรับบิตแมปที่สร้างขึ้นและสามารถทำให้เกิดOutOfMemoryข้อยกเว้นได้อย่างง่ายดาย วิธีการถอดรหัสแต่ละประเภทมีลายเซ็นเพิ่มเติมที่ให้คุณระบุตัวเลือกการถอดรหัสผ่านBitmapFactory.Optionsคลาส การตั้งค่าinJustDecodeBoundsคุณสมบัติการtrueขณะถอดรหัสจัดสรรหน่วยความจำหลีกเลี่ยงกลับnullสำหรับวัตถุบิตแมป แต่การตั้งค่าoutWidth, และoutHeight outMimeTypeเทคนิคนี้ช่วยให้คุณอ่านขนาดและประเภทของข้อมูลภาพก่อนการสร้าง (และการจัดสรรหน่วยความจำ) ของบิตแมป

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

เพื่อหลีกเลี่ยงjava.lang.OutOfMemoryข้อยกเว้นให้ตรวจสอบขนาดของบิตแมปก่อนที่จะถอดรหัสเว้นแต่คุณจะเชื่อถือแหล่งที่มาอย่างแน่นอนเพื่อให้ข้อมูลภาพที่มีขนาดที่คาดการณ์ได้ซึ่งเหมาะกับหน่วยความจำที่มีอยู่


โหลดเวอร์ชั่นที่ย่อขนาดลงในหน่วยความจำ

ตอนนี้ทราบขนาดภาพแล้วพวกเขาสามารถนำมาใช้ในการตัดสินใจว่าควรโหลดภาพเต็มลงในหน่วยความจำหรือควรโหลดรุ่นย่อยตัวอย่างแทน นี่คือปัจจัยที่ควรพิจารณา:

  • การใช้งานหน่วยความจำโดยประมาณของการโหลดภาพเต็มในหน่วยความจำ
  • จำนวนหน่วยความจำที่คุณยินดีที่จะยอมรับในการโหลดภาพนี้เนื่องจากข้อกำหนดด้านหน่วยความจำอื่น ๆ ของแอปพลิเคชันของคุณ
  • ขนาดขององค์ประกอบ ImageView หรือ UI เป้าหมายที่จะโหลดรูปภาพ
  • ขนาดหน้าจอและความหนาแน่นของอุปกรณ์ปัจจุบัน

ยกตัวอย่างเช่นมันไม่คุ้มค่าในการโหลดภาพ 1024x768 พิกเซลหน่วยความจำถ้ามันจะได้รับการแสดงในภาพขนาดย่อ 128x96 ImageViewพิกเซลใน

ที่จะบอกถอดรหัสเพื่อ subsample ภาพโหลดรุ่นเล็กในหน่วยความจำตั้งค่าinSampleSizeเพื่อtrueคุณในBitmapFactory.Optionsวัตถุ ตัวอย่างเช่นรูปภาพที่มีความละเอียด 2048x1536 ที่ถอดรหัสด้วยinSampleSize4 จะสร้างบิตแมปที่ประมาณ 512x384 การโหลดสิ่งนี้ลงในหน่วยความจำจะใช้ 0.75MB แทนที่จะเป็น 12MB สำหรับภาพเต็ม (สมมติว่ามีการกำหนดค่าบิตแมปเป็นARGB_8888) ต่อไปนี้เป็นวิธีการคำนวณค่าขนาดตัวอย่างที่เป็นกำลังสองตามความกว้างและความสูงเป้าหมาย:

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

หมายเหตุ : กำลังสองค่าจะถูกคำนวณเนื่องจากตัวถอดรหัสใช้ค่าสุดท้ายโดยการปัดเศษลงเป็นกำลังสองที่ใกล้ที่สุดตาม inSampleSizeเอกสาร

หากต้องการใช้วิธีนี้ให้ถอดรหัสก่อนโดยinJustDecodeBoundsตั้งค่าเป็นtrueส่งผ่านตัวเลือกผ่านแล้วถอดรหัสอีกครั้งโดยใช้inSampleSizeค่าใหม่และinJustDecodeBoundsตั้งค่าเป็นfalse:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
    int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

วิธีนี้ทำให้ง่ายต่อการโหลดบิตแมปขนาดใหญ่โดยพลการลงในImageViewที่แสดงภาพขนาดย่อขนาด 100x100 พิกเซลดังที่แสดงในรหัสตัวอย่างต่อไปนี้:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

คุณสามารถทำตามกระบวนการที่คล้ายกันเพื่อถอดรหัสบิตแมปจากแหล่งอื่น ๆ โดยการแทนที่BitmapFactory.decode*วิธีการที่เหมาะสมตามที่ต้องการ


21
คำตอบนี้ถูกอภิปรายในmeta
rene

9
คำตอบนี้ (ยกเว้นข้อมูลที่เข้าถึงได้ผ่านลิงก์) ไม่ได้นำเสนอวิธีแก้ไขปัญหาสำหรับคำตอบ ส่วนสำคัญของลิงก์ควรรวมเข้ากับคำถาม
FallenAngel

7
คำตอบนี้เช่นคำถามและคำตอบอื่น ๆ คือ Community Wiki ดังนั้นนี่คือสิ่งที่ชุมชนสามารถแก้ไขได้ด้วยการแก้ไขสิ่งที่ไม่ต้องการการแทรกแซงจากผู้ดูแล
Martijn Pieters

ลิงค์ปัจจุบันไปยังเนื้อหาและการสนับสนุน Kotlin สามารถดูได้ที่: developer.android.com/topic/performance/graphics/load-bitmap
Panos Gr

890

ในการแก้ไขข้อผิดพลาด OutOfMemory คุณควรทำดังนี้:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap preview_bitmap = BitmapFactory.decodeStream(is, null, options);

inSampleSizeตัวเลือกนี้จะลดการใช้หน่วยความจำ

นี่เป็นวิธีการที่สมบูรณ์ ก่อนอื่นให้อ่านขนาดภาพโดยไม่ต้องถอดรหัสเนื้อหา จากนั้นก็พบว่าinSampleSizeค่าที่ดีที่สุดควรเป็นพลังของ 2 และในที่สุดภาพจะถูกถอดรหัส

// Decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(new FileInputStream(f), null, o);

        // The new size we want to scale to
        final int REQUIRED_SIZE=70;

        // Find the correct scale value. It should be the power of 2.
        int scale = 1;
        while(o.outWidth / scale / 2 >= REQUIRED_SIZE && 
              o.outHeight / scale / 2 >= REQUIRED_SIZE) {
            scale *= 2;
        }

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
    } catch (FileNotFoundException e) {}
    return null;
}

31
โปรดทราบว่า 10 อาจไม่ใช่ค่าที่ดีที่สุดสำหรับ inSampleSize แต่เอกสารแนะนำให้ใช้พลังของ 2
Mirko N.

70
ฉันกำลังเผชิญกับปัญหาแบบเดียวกันกับ Chrispix แต่ฉันไม่คิดว่าวิธีการแก้ปัญหาที่นี่จริงๆจะแก้ปัญหา แต่ค่อนข้างก้าวเท้าเลี่ยงมัน การเปลี่ยนขนาดตัวอย่างจะลดจำนวนหน่วยความจำที่ใช้ (ในราคาคุณภาพของภาพซึ่งอาจโอเคสำหรับภาพตัวอย่าง) แต่จะไม่ป้องกันข้อยกเว้นหากมีการถอดรหัสภาพที่มีขนาดใหญ่พอหากมีการสตรีมภาพหลายภาพ ถอดรหัส หากฉันพบทางออกที่ดีกว่า (และอาจไม่มี) ฉันจะโพสต์คำตอบที่นี่
Flynn81

4
คุณต้องการขนาดที่เหมาะสมเพื่อจับคู่หน้าจอด้วยความหนาแน่นของพิกเซลเท่านั้นสำหรับการซูมเข้าและคุณสามารถถ่ายภาพตัวอย่างที่ความหนาแน่นสูงกว่าได้
stealthcopter

4
REQUIRED_SIZE เป็นขนาดใหม่ที่คุณต้องการปรับขนาด
Fedor

8
วิธีนี้ช่วยฉันได้ แต่คุณภาพของภาพแย่มาก ฉันใช้ viewfilpper เพื่อแสดงภาพคำแนะนำใด ๆ
user1106888

372

ฉันได้ปรับปรุงโค้ดของ Fedor เล็กน้อย มันเป็นแบบเดียวกัน แต่ไม่มี (ในความคิดของฉัน) น่าเกลียดในขณะที่วนและมันมักจะส่งผลในพลังของสอง ความรุ่งโรจน์ถึง Fedor สำหรับการแก้ปัญหาดั้งเดิมฉันติดอยู่จนกระทั่งฉันพบเขาแล้วฉันก็สามารถทำสิ่งนี้ :)

 private Bitmap decodeFile(File f){
    Bitmap b = null;

        //Decode image size
    BitmapFactory.Options o = new BitmapFactory.Options();
    o.inJustDecodeBounds = true;

    FileInputStream fis = new FileInputStream(f);
    BitmapFactory.decodeStream(fis, null, o);
    fis.close();

    int scale = 1;
    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
        scale = (int)Math.pow(2, (int) Math.ceil(Math.log(IMAGE_MAX_SIZE / 
           (double) Math.max(o.outHeight, o.outWidth)) / Math.log(0.5)));
    }

    //Decode with inSampleSize
    BitmapFactory.Options o2 = new BitmapFactory.Options();
    o2.inSampleSize = scale;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, o2);
    fis.close();

    return b;
}

40
ใช่คุณพูดถูก แต่ก็ไม่สวย ฉันพยายามทำให้ทุกคนชัดเจน ขอบคุณสำหรับรหัสของคุณ
Fedor

10
@Thomas Vervest - มีปัญหาใหญ่กับรหัสนั้น ^ ไม่ยกกำลัง 2 ให้กลายเป็นพลัง แต่ xors 2 พร้อมผลลัพธ์ คุณต้องการ Math.pow (2.0, ... ) มิฉะนั้นจะดูดี
DougW

6
โอ้โหเป็นสิ่งที่ดีมาก! ไม่ดีของฉันฉันจะแก้ไขทันทีขอบคุณสำหรับการตอบกลับ!
Thomas Vervest

8
คุณกำลังสร้างสอง FileInputStreams BitmapFactory.decodeStream()ใหม่หนึ่งสำหรับแต่ละการเรียกไปยัง คุณไม่ต้องบันทึกการอ้างอิงไปยังแต่ละรายการเพื่อปิดการอ้างอิงในfinallyบล็อก
matsev

1
@Babibu เอกสารไม่ได้ระบุว่ากระแสข้อมูลถูกปิดสำหรับคุณดังนั้นฉันคิดว่ามันยังคงปิดอยู่ ที่น่าสนใจและเกี่ยวข้องกับการอภิปรายสามารถพบได้ที่นี่ หมายเหตุความคิดเห็นโดย Adrian Smith ซึ่งเกี่ยวข้องโดยตรงกับการอภิปรายของเรา
โทมัส Vervest

232

ฉันมาจากประสบการณ์ iOS และฉันรู้สึกหงุดหงิดที่จะค้นพบปัญหากับบางสิ่งพื้นฐานเพื่อการโหลดและแสดงภาพ ท้ายที่สุดทุกคนที่มีปัญหานี้กำลังพยายามแสดงภาพที่มีขนาดพอสมควร อย่างไรก็ตามนี่คือการเปลี่ยนแปลงสองอย่างที่แก้ไขปัญหาของฉัน (และทำให้แอปตอบสนองได้ดีมาก)

1) ทุกครั้งที่คุณBitmapFactory.decodeXYZ()ทำให้แน่ใจว่าได้ส่งผ่านBitmapFactory.Optionsด้วยการinPurgeableตั้งค่าเป็นtrue(และควรมีการinInputShareableตั้งค่าเป็นtrue) ด้วย

2) Bitmap.createBitmap(width, height, Config.ARGB_8888)การใช้งานไม่เคย ฉันหมายถึงไม่เคย! ฉันไม่เคยมีสิ่งนั้นที่ไม่เพิ่มข้อผิดพลาดของหน่วยความจำหลังจากผ่านไปสองสามครั้ง ไม่มีจำนวนเงินrecycle(), System.gc()สิ่งที่ช่วย มันยกข้อยกเว้นเสมอ อีกวิธีหนึ่งที่ใช้งานได้จริงคือการมีภาพจำลองใน drawables ของคุณ (หรือบิตแมปอื่นที่คุณถอดรหัสโดยใช้ขั้นตอนที่ 1 ด้านบน) ช่วยลดสิ่งที่คุณต้องการจากนั้นจัดการบิตแมปที่เกิดขึ้น (เช่นส่งต่อไปยัง Canvas เพื่อความสนุกสนานมากขึ้น) Bitmap.createScaledBitmap(srcBitmap, width, height, false)ดังนั้นสิ่งที่คุณควรจะใช้แทนคือ: Config.ARGB_4444หากมีเหตุผลอะไรก็ตามที่คุณต้องใช้พลังของสัตว์สร้างวิธีแล้วที่ผ่านน้อย

นี่เกือบจะรับประกันว่าจะช่วยให้คุณประหยัดชั่วโมงถ้าไม่ได้วัน ทุกอย่างที่พูดเกี่ยวกับการปรับขนาดภาพ ฯลฯ ไม่ได้ผลจริงๆ (เว้นแต่คุณจะพิจารณาวิธีการแก้ปัญหาขนาดภาพที่ผิดหรือลดขนาดลง)


22
BitmapFactory.Options options = new BitmapFactory.Options(); options.inPurgeable = true;และ Bitmap.createScaledBitmap(srcBitmap, width, height, false);แก้ไขปัญหาของฉันได้โดยไม่มีข้อยกเว้นหน่วยความจำใน android 4.0.0 ขอบคุณเพื่อน!
Jan-Terje Sørensen

5
ในการเรียก Bitmap.createScaledBitmap () คุณควรใช้จริงเป็นพารามิเตอร์การตั้งค่าสถานะ มิฉะนั้นคุณภาพของภาพจะไม่ราบรื่นเมื่อปรับขนาดขึ้น ตรวจสอบกระทู้stackoverflow.com/questions/2895065/…
rOrlig

11
นั่นเป็นคำแนะนำที่ยอดเยี่ยมจริงๆ หวังว่าฉันจะให้ +1 เพิ่มเติมแก่คุณเพื่อนำ Google ไปใช้สำหรับข้อผิดพลาดข้อผิดพลาดที่น่าประหลาดใจ ฉันหมายถึง ... หากไม่ใช่ข้อผิดพลาดเอกสารจำเป็นต้องมีสัญญาณไฟนีออนที่กระพริบอย่างจริงจังว่า "นี่เป็นวิธีที่คุณทำกับภาพถ่าย" ทำให้ฉันต้องดิ้นรนกับเรื่องนี้เป็นเวลา 2 ปีและเพิ่งพบโพสต์นี้ หาที่ดี
Yevgeny Simkin

การย่อขนาดรูปภาพของคุณช่วยได้แน่นอน แต่นี่เป็นขั้นตอนสำคัญและสิ่งที่จะแก้ไขปัญหานี้ให้ฉันได้ในที่สุด ปัญหาที่เกิดขึ้นกับการปรับขนาดภาพของคุณคือถ้าคุณมีภาพจำนวนมากหรือถ้าภาพต้นฉบับมีขนาดใหญ่มากคุณก็ยังสามารถพบปัญหาเดียวกันได้ +1 กับคุณ Ephraim
เดฟ

10
ณ วันที่ Lollipop BitmapFactory.Options.inPurgeableและBitmapFactory.Options.inInputShareableเลิกใช้งานdeveloper.android.com/reference/android/graphics/ …
Denis Kniazhev

93

มันเป็นบั๊กที่รู้จักไม่ใช่เพราะไฟล์ขนาดใหญ่ เนื่องจาก Android แคช Drawables หน่วยความจำของคุณจะหมดหลังจากใช้ภาพไม่กี่ภาพ แต่ฉันได้พบวิธีอื่นโดยการข้ามระบบแคชเริ่มต้นของ Android

วิธีแก้ไข : ย้ายรูปภาพไปยังโฟลเดอร์ "ทรัพย์สิน" และใช้ฟังก์ชั่นต่อไปนี้เพื่อรับ BitmapDrawable:

public static Drawable getAssetImage(Context context, String filename) throws IOException {
    AssetManager assets = context.getResources().getAssets();
    InputStream buffer = new BufferedInputStream((assets.open("drawable/" + filename + ".png")));
    Bitmap bitmap = BitmapFactory.decodeStream(buffer);
    return new BitmapDrawable(context.getResources(), bitmap);
}

79

ฉันมีปัญหาเดียวกันนี้และแก้ไขได้โดยหลีกเลี่ยงฟังก์ชัน BitmapFactory.decodeStream หรือ decodeFile และใช้แทน BitmapFactory.decodeFileDescriptor

decodeFileDescriptor ดูเหมือนว่าจะเรียกวิธีการเนทิฟที่แตกต่างกว่า decodeStream / decodeFile

ยังไงก็ตามสิ่งที่ได้ผลคือสิ่งนี้ (โปรดทราบว่าฉันได้เพิ่มตัวเลือกบางตัวตามที่มีอยู่ด้านบน แต่นั่นไม่ใช่สิ่งที่ทำให้เกิดความแตกต่างที่สำคัญคือการเรียกBitmapFactory.decodeFileDescriptorแทนdecodeStreamหรือdecodeFile ):

private void showImage(String path)   {

    Log.i("showImage","loading:"+path);
    BitmapFactory.Options bfOptions=new BitmapFactory.Options();
    bfOptions.inDither=false;                     //Disable Dithering mode
    bfOptions.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
    bfOptions.inInputShareable=true;              //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
    bfOptions.inTempStorage=new byte[32 * 1024]; 

    File file=new File(path);
    FileInputStream fs=null;
    try {
        fs = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        //TODO do something intelligent
        e.printStackTrace();
    }

    try {
        if(fs!=null) bm=BitmapFactory.decodeFileDescriptor(fs.getFD(), null, bfOptions);
    } catch (IOException e) {
        //TODO do something intelligent
        e.printStackTrace();
    } finally{ 
        if(fs!=null) {
            try {
                fs.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    //bm=BitmapFactory.decodeFile(path, bfOptions); This one causes error: java.lang.OutOfMemoryError: bitmap size exceeds VM budget

    im.setImageBitmap(bm);
    //bm.recycle();
    bm=null;                        
}

ฉันคิดว่ามีปัญหากับฟังก์ชั่นพื้นเมืองที่ใช้ใน decodeStream / decodeFile ฉันยืนยันว่ามีการเรียกเมธอดเนทีฟที่ต่างออกไปเมื่อใช้ decodeFileDescriptor สิ่งที่ฉันได้อ่านก็คือ "รูปภาพ (Bitmaps) ไม่ได้ถูกจัดสรรในแบบจาวามาตรฐาน แต่ผ่านการโทรแบบดั้งเดิมการจัดสรรจะดำเนินการนอกฮีปเสมือนจริง แต่ถูก นับด้วย! "


1
ผลลัพธ์เดียวกันจาก memeory จริง ๆ แล้วมันไม่สำคัญว่าวิธีการที่คุณใช้นั้นขึ้นอยู่กับจำนวนของไบต์ที่คุณกำลังอ่านข้อมูลที่ให้หน่วยความจำ
PiyushMishra

72

ฉันคิดว่าวิธีที่ดีที่สุดในการหลีกเลี่ยงOutOfMemoryErrorคือการเผชิญหน้าและเข้าใจมัน

ฉันสร้างแอพโดยเจตนาเพื่อให้เกิดOutOfMemoryErrorและติดตามการใช้หน่วยความจำ

หลังจากที่ฉันทำการทดลองกับแอพนี้จำนวนมากฉันได้รับข้อสรุปดังต่อไปนี้:

ฉันจะพูดถึงเวอร์ชั่น SDK ก่อน Honey Comb ก่อน

  1. บิตแมปถูกเก็บในฮีปดั้งเดิม แต่จะได้รับการรวบรวมขยะโดยอัตโนมัติการเรียกรีไซเคิล () ไม่จำเป็น

  2. หาก {VM heap size} + {จัดสรรหน่วยความจำดั้งเดิมฮีปฮีป}> = {ขีด จำกัด ขนาด VM heap สำหรับอุปกรณ์} และคุณกำลังพยายามสร้างบิตแมป OOM จะถูกโยนทิ้ง

    ประกาศ: VM HEAP SIZE จะถูกนับแทนหน่วยความจำ VM ALLOCATED

  3. ขนาด VM Heap จะไม่ลดขนาดหลังจากโตขึ้นแม้ว่าหน่วยความจำ VM ที่จัดสรรจะถูกลดขนาด

  4. ดังนั้นคุณต้องรักษาหน่วยความจำ VM สูงสุดไว้ให้ต่ำที่สุดเท่าที่จะทำได้เพื่อให้ VM Heap Size โตขึ้นมากเกินไปเพื่อประหยัดหน่วยความจำที่มีอยู่สำหรับบิตแมป

  5. เรียก System.gc () ด้วยตนเองไม่มีความหมายระบบจะเรียกมันก่อนที่จะพยายามเพิ่มขนาดฮีพ

  6. Native Heap Size จะไม่ย่อขนาดเช่นกัน แต่จะไม่นับรวมกับ OOM ดังนั้นไม่จำเป็นต้องกังวล

ถ้าอย่างนั้นเรามาพูดถึง SDK เริ่มต้นจาก Honey Comb

  1. บิตแมปถูกเก็บไว้ในกอง VM หน่วยความจำดั้งเดิมจะไม่นับสำหรับ OOM

  2. เงื่อนไขสำหรับ OOM นั้นง่ายกว่ามาก: {VM heap size}> = {ข้อ จำกัด ขนาด VM heap สำหรับอุปกรณ์}

  3. ดังนั้นคุณมีหน่วยความจำที่พร้อมใช้งานมากขึ้นในการสร้างบิตแมปที่มีขีด จำกัด ขนาดฮีปเดียวกัน OOM จึงมีโอกาสน้อยที่จะถูกโยนทิ้ง

นี่คือข้อสังเกตของฉันเกี่ยวกับการเก็บขยะและความจำรั่ว

คุณสามารถดูได้ด้วยตนเองในแอป หากกิจกรรมดำเนินการ AsyncTask ที่ยังคงทำงานอยู่หลังจากที่กิจกรรมถูกทำลายกิจกรรมจะไม่ได้รับการเก็บขยะจนกว่า AsyncTask จะเสร็จสิ้น

นี่เป็นเพราะ AsyncTask เป็นตัวอย่างของคลาสภายในที่ไม่ระบุชื่อมันเก็บการอ้างอิงของกิจกรรม

การเรียก AsyncTask.cancel (จริง) จะไม่หยุดการดำเนินการถ้างานถูกบล็อกในการดำเนินการ IO ในเธรดพื้นหลัง

การเรียกกลับเป็นคลาสภายในที่ไม่ระบุชื่อเช่นกันดังนั้นหากอินสแตนซ์แบบคงที่ในโครงการของคุณเก็บไว้และไม่ปล่อยพวกเขาหน่วยความจำจะถูกปล่อยออกมา

หากคุณกำหนดเวลางานที่ทำซ้ำหรือล่าช้าตัวอย่างเช่นตัวจับเวลาและคุณไม่ได้โทรยกเลิก () และกำจัด () ใน onPause () หน่วยความจำจะถูกปล่อยออกมา


AsyncTask ไม่จำเป็นต้องเป็น "อินสแตนซ์ของคลาสภายในที่ไม่ระบุชื่อ" และเช่นเดียวกันกับ Callbackks คุณสามารถสร้างคลาสสาธารณะใหม่ในไฟล์ของตัวเองที่ขยาย AsyncTask หรือแม้แต่private static classในคลาสเดียวกัน พวกเขาจะไม่ถือการอ้างอิงใด ๆ กับกิจกรรม (เว้นแต่คุณจะให้พวกเขาแน่นอน)
Simon Forsberg

65

ฉันได้เห็นคำถามมากมายเกี่ยวกับข้อยกเว้นของ OOM และการแคชเมื่อเร็ว ๆ นี้ คู่มือนักพัฒนามีบทความที่ดีจริง ๆเกี่ยวกับเรื่องนี้ แต่บางคนก็มีแนวโน้มที่จะล้มเหลวในการใช้มันในวิธีที่เหมาะสม

ด้วยเหตุนี้ฉันจึงเขียนแอปพลิเคชันตัวอย่างที่แสดงให้เห็นถึงการแคชในสภาพแวดล้อม Android การใช้งานนี้ยังไม่ได้รับ OOM

ดูที่ท้ายคำตอบนี้เพื่อดูลิงค์ไปยังซอร์สโค้ด

ที่ต้องการ:

  • Android API 2.1 หรือสูงกว่า (ฉันไม่สามารถจัดการเพื่อรับหน่วยความจำที่มีอยู่สำหรับแอปพลิเคชันใน API 1.6 - ซึ่งเป็นรหัสชิ้นเดียวที่ไม่ทำงานใน API 1.6)
  • แพ็คเกจสนับสนุน Android

ภาพหน้าจอ

คุณสมบัติ:

  • เก็บแคชหากมีการเปลี่ยนแปลงการวางแนวโดยใช้ซิงเกิล
  • ใช้หนึ่งในแปดของหน่วยความจำแอปพลิเคชันที่กำหนดให้กับแคช (แก้ไขหากคุณต้องการ)
  • บิตแมปขนาดใหญ่จะถูกปรับสัดส่วน (คุณสามารถกำหนดพิกเซลสูงสุดที่คุณต้องการอนุญาต)
  • ควบคุมว่ามีการเชื่อมต่ออินเทอร์เน็ตก่อนดาวน์โหลดบิตแมป
  • ตรวจสอบให้แน่ใจว่าคุณกำลังยกตัวอย่างหนึ่งงานต่อแถวเท่านั้น
  • หากคุณกำลังเหวี่ยงListViewออกไปมันก็จะไม่ดาวน์โหลดบิตแมประหว่าง

ไม่รวมถึง:

  • การแคชดิสก์ สิ่งนี้ควรนำไปใช้งานได้ง่ายเพียงแค่ชี้ไปที่งานอื่นที่หยิบบิตแมปจากดิสก์

รหัสตัวอย่าง:

ภาพที่กำลังดาวน์โหลดเป็นภาพ (75x75) จาก Flickr อย่างไรก็ตามใส่รูปภาพที่คุณต้องการประมวลผลแล้วแอปพลิเคชันจะย่อขนาดลงหากเกินขนาดสูงสุด ในแอปพลิเคชั่นนี้ URL นั้นอยู่ในStringอาร์เรย์

LruCacheมีวิธีการที่ดีในการจัดการกับบิตแมป อย่างไรก็ตามในแอปพลิเคชันนี้ฉันวางอินสแตนซ์ของLruCacheคลาสแคชภายในที่ฉันสร้างขึ้นเพื่อให้แอปพลิเคชันมีความเป็นไปได้มากขึ้น

สิ่งที่สำคัญของ Cache.java ( loadBitmap()วิธีนี้สำคัญที่สุด):

public Cache(int size, int maxWidth, int maxHeight) {
    // Into the constructor you add the maximum pixels
    // that you want to allow in order to not scale images.
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;

    mBitmapCache = new LruCache<String, Bitmap>(size) {
        protected int sizeOf(String key, Bitmap b) {
            // Assuming that one pixel contains four bytes.
            return b.getHeight() * b.getWidth() * 4;
        }
    };

    mCurrentTasks = new ArrayList<String>();    
}

/**
 * Gets a bitmap from cache. 
 * If it is not in cache, this method will:
 * 
 * 1: check if the bitmap url is currently being processed in the
 * BitmapLoaderTask and cancel if it is already in a task (a control to see
 * if it's inside the currentTasks list).
 * 
 * 2: check if an internet connection is available and continue if so.
 * 
 * 3: download the bitmap, scale the bitmap if necessary and put it into
 * the memory cache.
 * 
 * 4: Remove the bitmap url from the currentTasks list.
 * 
 * 5: Notify the ListAdapter.
 * 
 * @param mainActivity - Reference to activity object, in order to
 * call notifyDataSetChanged() on the ListAdapter.
 * @param imageKey - The bitmap url (will be the key).
 * @param imageView - The ImageView that should get an
 * available bitmap or a placeholder image.
 * @param isScrolling - If set to true, we skip executing more tasks since
 * the user probably has flinged away the view.
 */
public void loadBitmap(MainActivity mainActivity, 
        String imageKey, ImageView imageView,
        boolean isScrolling) {
    final Bitmap bitmap = getBitmapFromCache(imageKey); 

    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.ic_launcher);
        if (!isScrolling && !mCurrentTasks.contains(imageKey) && 
                mainActivity.internetIsAvailable()) {
            BitmapLoaderTask task = new BitmapLoaderTask(imageKey,
                    mainActivity.getAdapter());
            task.execute();
        }
    } 
}

คุณไม่จำเป็นต้องแก้ไขอะไรในไฟล์ Cache.java หากคุณไม่ต้องการใช้การแคชดิสก์

สิ่งที่สำคัญของ MainActivity.java:

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (view.getId() == android.R.id.list) {
        // Set scrolling to true only if the user has flinged the       
        // ListView away, hence we skip downloading a series
        // of unnecessary bitmaps that the user probably
        // just want to skip anyways. If we scroll slowly it
        // will still download bitmaps - that means
        // that the application won't wait for the user
        // to lift its finger off the screen in order to
        // download.
        if (scrollState == SCROLL_STATE_FLING) {
            mIsScrolling = true;
        } else {
            mIsScrolling = false;
            mListAdapter.notifyDataSetChanged();
        }
    } 
}

// Inside ListAdapter...
@Override
public View getView(final int position, View convertView, ViewGroup parent) {           
    View row = convertView;
    final ViewHolder holder;

    if (row == null) {
        LayoutInflater inflater = getLayoutInflater();
        row = inflater.inflate(R.layout.main_listview_row, parent, false);  
        holder = new ViewHolder(row);
        row.setTag(holder);
    } else {
        holder = (ViewHolder) row.getTag();
    }   

    final Row rowObject = getItem(position);

    // Look at the loadBitmap() method description...
    holder.mTextView.setText(rowObject.mText);      
    mCache.loadBitmap(MainActivity.this,
            rowObject.mBitmapUrl, holder.mImageView,
            mIsScrolling);  

    return row;
}

getView()ได้รับการเรียกบ่อยมาก โดยปกติแล้วเราไม่ควรดาวน์โหลดรูปภาพที่นั่นหากเรายังไม่ได้ทำการตรวจสอบเพื่อให้แน่ใจว่าเราจะไม่เริ่มกระทู้ต่อจำนวนไม่สิ้นสุด Cache.java ตรวจสอบว่ามีrowObject.mBitmapUrlอยู่แล้วในงานและถ้าเป็นมันจะไม่เริ่มต้นใหม่ ดังนั้นเราจึงมีโอกาสมากที่สุดไม่เกินข้อ จำกัด คิวงานจากAsyncTaskสระว่ายน้ำ

ดาวน์โหลด:

คุณสามารถดาวน์โหลดรหัสที่มาจากhttps://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip


คำสุดท้าย:

ฉันได้ทดสอบสิ่งนี้มาหลายสัปดาห์แล้วฉันยังไม่ได้รับการยกเว้น OOM แม้แต่ครั้งเดียว ฉันได้ทดสอบสิ่งนี้กับตัวจำลองบน Nexus One ของฉันและบน Nexus S ของฉันฉันได้ทดสอบ URL รูปภาพที่มีภาพที่มีคุณภาพระดับ HD สิ่งเดียวที่ทำให้คอขวดใช้เวลาในการดาวน์โหลดนานกว่า

มีเพียงสถานการณ์เดียวที่เป็นไปได้ที่ฉันสามารถจินตนาการได้ว่า OOM จะปรากฏขึ้นและนั่นคือถ้าเราดาวน์โหลดรูปภาพขนาดใหญ่จริง ๆ และก่อนที่พวกเขาจะถูกปรับขนาดและใส่ในแคชจะทำให้มีหน่วยความจำเพิ่มขึ้น แต่นั่นก็ไม่ใช่สถานการณ์ที่สมบูรณ์แบบอยู่แล้วและเป็นไปไม่ได้ที่จะแก้ไขด้วยวิธีที่เป็นไปได้มากขึ้น

รายงานข้อผิดพลาดในความคิดเห็น! :-)


43

ฉันทำสิ่งต่อไปนี้เพื่อถ่ายภาพและปรับขนาดทันที หวังว่านี่จะช่วยได้

Bitmap bm;
bm = Bitmap.createScaledBitmap(BitmapFactory.decodeFile(filepath), 100, 100, true);
mPicture = new ImageView(context);
mPicture.setImageBitmap(bm);    

26
วิธีการนี้จะปรับบิตแมป แต่มันไม่ได้แก้ปัญหา OutOfMemory เพราะบิตแมปเต็มกำลังถูกถอดรหัสอยู่แล้ว
Fedor

5
ฉันจะดูว่าฉันสามารถดูรหัสเดิมของฉันได้หรือไม่ แต่ฉันคิดว่ามันแก้ปัญหาหน่วยความจำไม่เพียงพอ จะตรวจสอบรหัสเดิมของฉันอีกครั้ง
Chrispix

2
ในตัวอย่างนี้อย่างน้อยดูเหมือนว่าคุณไม่ได้อ้างอิงถึงบิตแมปแบบเต็มดังนั้นจึงเป็นการประหยัดหน่วยความจำ
NoBugs

สำหรับฉันมันแก้ปัญหาหน่วยความจำ แต่ลดคุณภาพของภาพ
Pamela Sillah

35

ดูเหมือนว่านี่เป็นปัญหาที่ใช้เวลานานมากโดยมีคำอธิบายที่แตกต่างกันมากมาย ฉันได้รับคำแนะนำของคำตอบที่นำเสนอโดยทั่วไปสองคำตอบที่นี่ แต่ไม่มีคำตอบใด ๆ ที่แก้ไขปัญหาของฉันของ VM ที่อ้างว่าไม่สามารถจ่ายไบต์เพื่อทำการถอดรหัสส่วนหนึ่งของกระบวนการ หลังจากการขุดฉันได้เรียนรู้ว่าปัญหาที่แท้จริงที่นี่คือกระบวนการถอดรหัสที่นำออกจากกอง NATIVE

ดูที่นี่: BitmapFactory OOM ผลักดันฉันถั่ว

นั่นนำฉันไปสู่หัวข้อสนทนาอื่นที่ฉันพบวิธีแก้ไขปัญหานี้เพิ่มเติมอีกสองสามข้อ หนึ่งคือการโทรSystem.gc();ด้วยตนเองหลังจากที่ภาพของคุณจะปรากฏขึ้น แต่นั่นทำให้แอปของคุณใช้หน่วยความจำมากขึ้นในความพยายามที่จะลดฮีปดั้งเดิม ทางออกที่ดีกว่า ณ วันที่เปิดตัว 2.0 (Donut) คือการใช้ตัวเลือก BitmapFactory "inPurgeable" ดังนั้นฉันก็เพิ่มเพียงหลังจากo2.inPurgeable=true;o2.inSampleSize=scale;

เพิ่มเติมในหัวข้อนั้นที่นี่: ขีด จำกัด ของหน่วยความจำฮีปมีเพียง 6M หรือไม่?

ตอนนี้ต้องบอกว่าทั้งหมดนี้ฉันเป็นคนโง่กับ Java และ Android ด้วย ดังนั้นหากคุณคิดว่านี่เป็นวิธีที่ดีในการแก้ไขปัญหานี้คุณอาจพูดถูก ;-) แต่นี่เป็นสิ่งมหัศจรรย์สำหรับฉันและฉันพบว่ามันเป็นไปไม่ได้เลยที่จะเรียกใช้ VM จากแคชกองตอนนี้ ข้อเสียเปรียบเพียงอย่างเดียวที่ฉันพบคือคุณกำลังทำลายภาพที่แคชไว้ของคุณ ซึ่งหมายความว่าถ้าคุณย้อนกลับไปที่รูปภาพนั้นคุณจะวาดมันใหม่ทุกครั้ง ในกรณีที่แอปพลิเคชันของฉันทำงานนั่นไม่ใช่ปัญหาจริงๆ ไมล์สะสมของคุณอาจแตกต่างกันไป


คงที่ OOM คงที่สำหรับฉัน
Artem Russakovskii

35

น่าเสียดายที่หากไม่มีการทำงานใด ๆ ข้างต้นให้เพิ่มส่วนนี้ลงในไฟล์Manifestของคุณ ภายในแท็กแอปพลิเคชัน

 <application
         android:largeHeap="true"

1
คุณช่วยอธิบายสิ่งนี้ได้จริงหรือไม่? เพียงแค่บอกให้คนอื่นเพิ่มสิ่งนี้ก็ไม่ได้ช่วยอะไร
ชิงทรัพย์แรบไบ

1
นี่เป็นวิธีแก้ปัญหาที่แย่มาก โดยทั่วไปคุณไม่ได้พยายามแก้ไขปัญหา ขอให้ระบบ Android จัดสรรพื้นที่ฮีปให้มากขึ้นสำหรับแอปพลิเคชันของคุณแทน สิ่งนี้จะมีผลกระทบที่เลวร้ายมากต่อแอปของคุณเช่นแอปของคุณใช้พลังงานแบตเตอรี่จำนวนมากเนื่องจาก GC ต้องผ่านพื้นที่กองขนาดใหญ่เพื่อล้างหน่วยความจำและประสิทธิภาพของแอปของคุณจะช้าลง
Prakash

2
แล้วทำไม android ถึงทำให้เราเพิ่ม android นี้: largeHeap = "true" ในไฟล์ Manifest? ตอนนี้คุณเป็น Android ที่ท้าทาย
Himanshu Mori


28

ฉันมีวิธีแก้ปัญหาที่มีประสิทธิภาพมากกว่าซึ่งไม่จำเป็นต้องปรับสเกลใด ๆ เพียงถอดรหัสบิตแมปของคุณเพียงครั้งเดียวแล้วแคชในแผนที่กับชื่อของมัน จากนั้นก็เรียกบิตแมปกับชื่อและตั้งใน ImageView ไม่มีอะไรเพิ่มเติมที่ต้องทำ

สิ่งนี้จะทำงานได้เพราะข้อมูลไบนารี่ที่แท้จริงของบิตแมปที่ถอดรหัสไม่ได้ถูกจัดเก็บไว้ในกอง VM ของ dalvik มันถูกเก็บไว้ภายนอก ดังนั้นทุกครั้งที่คุณถอดรหัสบิตแมปมันจะจัดสรรหน่วยความจำภายนอกฮีป VM ซึ่งไม่เคยเรียกคืนโดย GC

เพื่อช่วยให้คุณเข้าใจสิ่งนี้ได้ดีขึ้นลองจินตนาการว่าคุณได้เก็บภาพของคุณไว้ในโฟลเดอร์ภาพวาด คุณเพิ่งได้รับภาพโดยทำ getResources (). getDrwable (R.drawable.) สิ่งนี้จะไม่ถอดรหัสรูปภาพของคุณทุกครั้ง แต่จะใช้อินสแตนซ์ที่ถอดรหัสแล้วทุกครั้งที่คุณโทรหา ดังนั้นในสาระสำคัญมันจะถูกเก็บไว้

ในขณะนี้เนื่องจากภาพของคุณอยู่ในไฟล์บางแห่ง (หรืออาจมาจากเซิร์ฟเวอร์ภายนอก) มันเป็นความรับผิดชอบของคุณที่จะแคชอินสแตนซ์บิตแมปที่ถอดรหัสเพื่อนำกลับมาใช้ใหม่ได้ทุกเมื่อที่ต้องการ

หวังว่านี่จะช่วยได้


4
"แล้วแคชในแผนที่กับชื่อของมัน" คุณแคชรูปภาพของคุณอย่างไร
Vincent

3
คุณลองสิ่งนี้จริงหรือ แม้ว่าข้อมูลพิกเซลไม่ได้ถูกจัดเก็บจริงภายในกอง Dalvik ขนาดของมันในหน่วยความจำดั้งเดิมจะรายงานไปยัง VM และนับรวมกับหน่วยความจำที่มีอยู่
ErikR

3
@ เร็ว ๆ นี้ฉันคิดว่ามันไม่ยากที่จะเก็บไว้ในแผนที่ ฉันขอแนะนำบางอย่างเช่นแผนที่ HashMap <KEY, Bitmap> ซึ่ง Key สามารถเป็นสตริงของแหล่งข้อมูลหรืออะไรก็ได้ที่เหมาะสมสำหรับคุณ สมมติว่าคุณใช้เส้นทางเป็น KEY คุณเก็บมันไว้ใน map.put (Path, Bitmap) และรับมันผ่าน map.get (Path)
Rafael T

3
คุณอาจต้องการใช้ HashMap <String, SoftReference <Bitmap>> หากคุณกำลังใช้ Cache อิมเมจมิฉะนั้นคุณอาจมีหน่วยความจำไม่เพียงพอ - ฉันยังไม่คิดว่า "มันจัดสรรหน่วยความจำนอกฮีป VM ซึ่งไม่เคยเรียกคืนโดย GC "เป็นความจริงหน่วยความจำจะยึดตามที่ผมเข้าใจก็อาจจะมีความล่าช้าซึ่งเป็นสิ่งที่ bitmap.recycle () ใช้สำหรับเป็นคำใบ้ที่จะเรียกคืนข่าวต้นแล้ว ...
Dori

28

ฉันได้แก้ไขปัญหาเดียวกันในลักษณะต่อไปนี้

Bitmap b = null;
Drawable d;
ImageView i = new ImageView(mContext);
try {
    b = Bitmap.createBitmap(320,424,Bitmap.Config.RGB_565);
    b.eraseColor(0xFFFFFFFF);
    Rect r = new Rect(0, 0,320 , 424);
    Canvas c = new Canvas(b);
    Paint p = new Paint();
    p.setColor(0xFFC0C0C0);
    c.drawRect(r, p);
    d = mContext.getResources().getDrawable(mImageIds[position]);
    d.setBounds(r);
    d.draw(c);

    /*   
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inTempStorage = new byte[128*1024];
        b = BitmapFactory.decodeStream(mContext.getResources().openRawResource(mImageIds[position]), null, o2);
        o2.inSampleSize=16;
        o2.inPurgeable = true;
    */
} catch (Exception e) {

}
i.setImageBitmap(b);

ไม่เป็นไร แต่ฉันใช้หลายบิตแมปสำหรับวาดวงกลมใน OnCreate และการเรียกใช้กิจกรรม 4-5 ครั้งดังนั้นวิธีล้างบิตแมปและวิธีลบบิตแมปและสร้างอีกครั้งเป็นครั้งที่สองเมื่อกิจกรรม 0nCreate ..
ckpatel

27

มีสองประเด็นที่นี่ ....

  • หน่วยความจำบิตแมปไม่ได้อยู่ในกอง VM แต่อยู่ในกองดั้งเดิม - ดูBitmapFactory OOM ผลักดันฉันถั่ว
  • การรวบรวมขยะสำหรับฮีปดั้งเดิมนั้นมีความเกียจคร้านกว่าฮีป VM - ดังนั้นคุณต้องมีความก้าวร้าวมากในการทำบิตแมปการรายงานและบิตแมป = โมฆะทุกครั้งที่คุณทำกิจกรรมของ onPause หรือ onDestroy

มันอยู่ในกอง VM ตั้งแต่ Android 2.3+
FindOut_Quran

27

สิ่งนี้ได้ผลสำหรับฉัน!

public Bitmap readAssetsBitmap(String filename) throws IOException {
    try {
        BitmapFactory.Options options = new BitmapFactory.Options(); 
        options.inPurgeable = true;
        Bitmap bitmap = BitmapFactory.decodeStream(assets.open(filename), null, options);
        if(bitmap == null) {
            throw new IOException("File cannot be opened: It's value is null");
        } else {
            return bitmap;
        }
    } catch (IOException e) {
        throw new IOException("File cannot be opened: " + e.getMessage());
    }
}

20

ไม่มีคำตอบใดที่ได้ผลสำหรับฉัน แต่ฉันคิดวิธีแก้ปัญหาที่น่าเกลียดอย่างน่ากลัวที่แก้ปัญหาได้ ฉันเพิ่มรูปภาพขนาดเล็กมาก 1x1 พิกเซลให้กับโครงการของฉันเป็นทรัพยากรและโหลดลงใน ImageView ของฉันก่อนที่จะโทรเข้าไปในคอลเล็กชันขยะ ฉันคิดว่าอาจเป็นเพราะ ImageView นั้นไม่ได้ปล่อย Bitmap ดังนั้น GC ไม่เคยหยิบมันขึ้นมา มันน่าเกลียด แต่ตอนนี้ดูเหมือนว่าจะทำงานได้

if (bitmap != null)
{
  bitmap.recycle();
  bitmap = null;
}
if (imageView != null)
{
  imageView.setImageResource(R.drawable.tiny); // This is my 1x1 png.
}
System.gc();

imageView.setImageBitmap(...); // Do whatever you need to do to load the image you want.

ดูเหมือนว่า imageView จะไม่รีไซเคิล bitmap ด้วยตัวเอง ช่วยฉันด้วยขอบคุณ
Dmitry Zaytsev

@ ไมค์คุณสามารถเพิ่มรหัสที่สมบูรณ์ของเครื่องมือสร้างภาพหรือมิฉะนั้นคุณสามารถให้ลิงค์ของการโหลดภาพบิตแมป ถ้าฉันใช้รีไซเคิลบนบิตแมปมุมมองรายการทั้งหมดของฉันจะแสดง แต่รายการทั้งหมดจะแสดงว่าง
TNR

@ ไมค์คุณสามารถบอกได้ไหมว่าฉันทำ imageView = null ก่อนที่จะโทรเข้าสู่การรวบรวมขยะมันดีหรือไม่?
กำลัง

@TNR ฉันคิดว่าสิ่งที่คุณหายไปนี่คือbitmapในรหัสข้างต้นเป็นภาพที่แสดงก่อนหน้านี้แล้วคุณต้องรีไซเคิลที่ล้างการอ้างอิงใด ๆ ให้มันimageViewลืมมันด้วยการตั้งค่าเล็ก ๆ น้อย ๆ แทนgc(); และหลังจากทั้งหมดนี้: โหลดภาพใหม่ของคุณไปbitmapและแสดงมัน...ในรหัสข้างต้น
TWiStErRob

นี่เป็นสิ่งที่ผิด คุณควรล้างเนื้อหาภาพของคุณก่อนรีไซเคิลบิตแมป (แทนในขณะที่มันจะปรากฏจริงและใช้งาน)
FindOut_Quran

20

คำตอบที่ดีที่นี่ แต่ฉันต้องการชั้นเรียนที่ใช้งานได้อย่างเต็มที่เพื่อแก้ไขปัญหานี้ .. ดังนั้นฉันจึงได้

นี่คือคลาส BitmapHelperของฉันที่มีการพิสูจน์ OutOfMemoryError :-)

import java.io.File;
import java.io.FileInputStream;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

public class BitmapHelper
{

    //decodes image and scales it to reduce memory consumption
    public static Bitmap decodeFile(File bitmapFile, int requiredWidth, int requiredHeight, boolean quickAndDirty)
    {
        try
        {
            //Decode image size
            BitmapFactory.Options bitmapSizeOptions = new BitmapFactory.Options();
            bitmapSizeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(new FileInputStream(bitmapFile), null, bitmapSizeOptions);

            // load image using inSampleSize adapted to required image size
            BitmapFactory.Options bitmapDecodeOptions = new BitmapFactory.Options();
            bitmapDecodeOptions.inTempStorage = new byte[16 * 1024];
            bitmapDecodeOptions.inSampleSize = computeInSampleSize(bitmapSizeOptions, requiredWidth, requiredHeight, false);
            bitmapDecodeOptions.inPurgeable = true;
            bitmapDecodeOptions.inDither = !quickAndDirty;
            bitmapDecodeOptions.inPreferredConfig = quickAndDirty ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;

            Bitmap decodedBitmap = BitmapFactory.decodeStream(new FileInputStream(bitmapFile), null, bitmapDecodeOptions);

            // scale bitmap to mathc required size (and keep aspect ratio)

            float srcWidth = (float) bitmapDecodeOptions.outWidth;
            float srcHeight = (float) bitmapDecodeOptions.outHeight;

            float dstWidth = (float) requiredWidth;
            float dstHeight = (float) requiredHeight;

            float srcAspectRatio = srcWidth / srcHeight;
            float dstAspectRatio = dstWidth / dstHeight;

            // recycleDecodedBitmap is used to know if we must recycle intermediary 'decodedBitmap'
            // (DO NOT recycle it right away: wait for end of bitmap manipulation process to avoid
            // java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@416ee7d8
            // I do not excatly understand why, but this way it's OK

            boolean recycleDecodedBitmap = false;

            Bitmap scaledBitmap = decodedBitmap;
            if (srcAspectRatio < dstAspectRatio)
            {
                scaledBitmap = getScaledBitmap(decodedBitmap, (int) dstWidth, (int) (srcHeight * (dstWidth / srcWidth)));
                // will recycle recycleDecodedBitmap
                recycleDecodedBitmap = true;
            }
            else if (srcAspectRatio > dstAspectRatio)
            {
                scaledBitmap = getScaledBitmap(decodedBitmap, (int) (srcWidth * (dstHeight / srcHeight)), (int) dstHeight);
                recycleDecodedBitmap = true;
            }

            // crop image to match required image size

            int scaledBitmapWidth = scaledBitmap.getWidth();
            int scaledBitmapHeight = scaledBitmap.getHeight();

            Bitmap croppedBitmap = scaledBitmap;

            if (scaledBitmapWidth > requiredWidth)
            {
                int xOffset = (scaledBitmapWidth - requiredWidth) / 2;
                croppedBitmap = Bitmap.createBitmap(scaledBitmap, xOffset, 0, requiredWidth, requiredHeight);
                scaledBitmap.recycle();
            }
            else if (scaledBitmapHeight > requiredHeight)
            {
                int yOffset = (scaledBitmapHeight - requiredHeight) / 2;
                croppedBitmap = Bitmap.createBitmap(scaledBitmap, 0, yOffset, requiredWidth, requiredHeight);
                scaledBitmap.recycle();
            }

            if (recycleDecodedBitmap)
            {
                decodedBitmap.recycle();
            }
            decodedBitmap = null;

            scaledBitmap = null;
            return croppedBitmap;
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     * compute powerOf2 or exact scale to be used as {@link BitmapFactory.Options#inSampleSize} value (for subSampling)
     * 
     * @param requiredWidth
     * @param requiredHeight
     * @param powerOf2
     *            weither we want a power of 2 sclae or not
     * @return
     */
    public static int computeInSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight, boolean powerOf2)
    {
        int inSampleSize = 1;

        // Raw height and width of image
        final int srcHeight = options.outHeight;
        final int srcWidth = options.outWidth;

        if (powerOf2)
        {
            //Find the correct scale value. It should be the power of 2.

            int tmpWidth = srcWidth, tmpHeight = srcHeight;
            while (true)
            {
                if (tmpWidth / 2 < dstWidth || tmpHeight / 2 < dstHeight)
                    break;
                tmpWidth /= 2;
                tmpHeight /= 2;
                inSampleSize *= 2;
            }
        }
        else
        {
            // Calculate ratios of height and width to requested height and width
            final int heightRatio = Math.round((float) srcHeight / (float) dstHeight);
            final int widthRatio = Math.round((float) srcWidth / (float) dstWidth);

            // Choose the smallest ratio as inSampleSize value, this will guarantee
            // a final image with both dimensions larger than or equal to the
            // requested height and width.
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }

        return inSampleSize;
    }

    public static Bitmap drawableToBitmap(Drawable drawable)
    {
        if (drawable instanceof BitmapDrawable)
        {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    public static Bitmap getScaledBitmap(Bitmap bitmap, int newWidth, int newHeight)
    {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;

        // CREATE A MATRIX FOR THE MANIPULATION
        Matrix matrix = new Matrix();
        // RESIZE THE BIT MAP
        matrix.postScale(scaleWidth, scaleHeight);

        // RECREATE THE NEW BITMAP
        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
        return resizedBitmap;
    }

}

สำหรับทุกคนที่ใช้สิ่งนี้: ฉันเพิ่งแก้ไขข้อบกพร่อง: "int scaledBitmapHeight = scaledBitmap.getWidth ();" เป็นสิ่งผิดปกติ (ชัดแจ้งฉันแทนที่ด้วย "int scaledBitmapHeight = scaledBitmap.getHeight ();"
Pascal

19

มันใช้งานได้สำหรับฉัน

Bitmap myBitmap;

BitmapFactory.Options options = new BitmapFactory.Options(); 
options.InPurgeable = true;
options.OutHeight = 50;
options.OutWidth = 50;
options.InSampleSize = 4;

File imgFile = new File(filepath);
myBitmap = BitmapFactory.DecodeFile(imgFile.AbsolutePath, options);

และนี่คือ C # monodroid คุณสามารถเปลี่ยนเส้นทางของภาพได้อย่างง่ายดาย สิ่งสำคัญที่นี่คือตัวเลือกที่จะตั้ง


16

ดูเหมือนว่าสถานที่ที่เหมาะสมในการแบ่งปันคลาสยูทิลิตี้ของฉันสำหรับการโหลดและการประมวลผลภาพกับชุมชนคุณสามารถใช้และแก้ไขได้อย่างอิสระ

package com.emil;

import java.io.IOException;
import java.io.InputStream;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/**
 * A class to load and process images of various sizes from input streams and file paths.
 * 
 * @author Emil http://stackoverflow.com/users/220710/emil
 *
 */
public class ImageProcessing {

    public static Bitmap getBitmap(InputStream stream, int sampleSize, Bitmap.Config bitmapConfig) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForSampling(sampleSize, bitmapConfig);
        Bitmap bm = BitmapFactory.decodeStream(stream,null,options);
        if(ImageProcessing.checkDecode(options)){
            return bm;
        }else{
            throw new IOException("Image decoding failed, using stream.");
        }
    }

    public static Bitmap getBitmap(String imgPath, int sampleSize, Bitmap.Config bitmapConfig) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForSampling(sampleSize, bitmapConfig);
        Bitmap bm = BitmapFactory.decodeFile(imgPath,options);
        if(ImageProcessing.checkDecode(options)){
            return bm;
        }else{
            throw new IOException("Image decoding failed, using file path.");
        }
    }

    public static Dimensions getDimensions(InputStream stream) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForDimensions();
        BitmapFactory.decodeStream(stream,null,options);
        if(ImageProcessing.checkDecode(options)){
            return new ImageProcessing.Dimensions(options.outWidth,options.outHeight);
        }else{
            throw new IOException("Image decoding failed, using stream.");
        }
    }

    public static Dimensions getDimensions(String imgPath) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForDimensions();
        BitmapFactory.decodeFile(imgPath,options);
        if(ImageProcessing.checkDecode(options)){
            return new ImageProcessing.Dimensions(options.outWidth,options.outHeight);
        }else{
            throw new IOException("Image decoding failed, using file path.");
        }
    }

    private static boolean checkDecode(BitmapFactory.Options options){
        // Did decode work?
        if( options.outWidth<0 || options.outHeight<0 ){
            return false;
        }else{
            return true;
        }
    }

    /**
     * Creates a Bitmap that is of the minimum dimensions necessary
     * @param bm
     * @param min
     * @return
     */
    public static Bitmap createMinimalBitmap(Bitmap bm, ImageProcessing.Minimize min){
        int newWidth, newHeight;
        switch(min.type){
        case WIDTH:
            if(bm.getWidth()>min.minWidth){
                newWidth=min.minWidth;
                newHeight=ImageProcessing.getScaledHeight(newWidth, bm);
            }else{
                // No resize
                newWidth=bm.getWidth();
                newHeight=bm.getHeight();
            }
            break;
        case HEIGHT:
            if(bm.getHeight()>min.minHeight){
                newHeight=min.minHeight;
                newWidth=ImageProcessing.getScaledWidth(newHeight, bm);
            }else{
                // No resize
                newWidth=bm.getWidth();
                newHeight=bm.getHeight();
            }
            break;
        case BOTH: // minimize to the maximum dimension
        case MAX:
            if(bm.getHeight()>bm.getWidth()){
                // Height needs to minimized
                min.minDim=min.minDim!=null ? min.minDim : min.minHeight;
                if(bm.getHeight()>min.minDim){
                    newHeight=min.minDim;
                    newWidth=ImageProcessing.getScaledWidth(newHeight, bm);
                }else{
                    // No resize
                    newWidth=bm.getWidth();
                    newHeight=bm.getHeight();
                }
            }else{
                // Width needs to be minimized
                min.minDim=min.minDim!=null ? min.minDim : min.minWidth;
                if(bm.getWidth()>min.minDim){
                    newWidth=min.minDim;
                    newHeight=ImageProcessing.getScaledHeight(newWidth, bm);
                }else{
                    // No resize
                    newWidth=bm.getWidth();
                    newHeight=bm.getHeight();
                }
            }
            break;
        default:
            // No resize
            newWidth=bm.getWidth();
            newHeight=bm.getHeight();
        }
        return Bitmap.createScaledBitmap(bm, newWidth, newHeight, true);
    }

    public static int getScaledWidth(int height, Bitmap bm){
        return (int)(((double)bm.getWidth()/bm.getHeight())*height);
    }

    public static int getScaledHeight(int width, Bitmap bm){
        return (int)(((double)bm.getHeight()/bm.getWidth())*width);
    }

    /**
     * Get the proper sample size to meet minimization restraints
     * @param dim
     * @param min
     * @param multipleOf2 for fastest processing it is recommended that the sample size be a multiple of 2
     * @return
     */
    public static int getSampleSize(ImageProcessing.Dimensions dim, ImageProcessing.Minimize min, boolean multipleOf2){
        switch(min.type){
        case WIDTH:
            return ImageProcessing.getMaxSampleSize(dim.width, min.minWidth, multipleOf2);
        case HEIGHT:
            return ImageProcessing.getMaxSampleSize(dim.height, min.minHeight, multipleOf2);
        case BOTH:
            int widthMaxSampleSize=ImageProcessing.getMaxSampleSize(dim.width, min.minWidth, multipleOf2);
            int heightMaxSampleSize=ImageProcessing.getMaxSampleSize(dim.height, min.minHeight, multipleOf2);
            // Return the smaller of the two
            if(widthMaxSampleSize<heightMaxSampleSize){
                return widthMaxSampleSize;
            }else{
                return heightMaxSampleSize;
            }
        case MAX:
            // Find the larger dimension and go bases on that
            if(dim.width>dim.height){
                return ImageProcessing.getMaxSampleSize(dim.width, min.minDim, multipleOf2);
            }else{
                return ImageProcessing.getMaxSampleSize(dim.height, min.minDim, multipleOf2);
            }
        }
        return 1;
    }

    public static int getMaxSampleSize(int dim, int min, boolean multipleOf2){
        int add=multipleOf2 ? 2 : 1;
        int size=0;
        while(min<(dim/(size+add))){
            size+=add;
        }
        size = size==0 ? 1 : size;
        return size;        
    }

    public static class Dimensions {
        int width;
        int height;

        public Dimensions(int width, int height) {
            super();
            this.width = width;
            this.height = height;
        }

        @Override
        public String toString() {
            return width+" x "+height;
        }
    }

    public static class Minimize {
        public enum Type {
            WIDTH,HEIGHT,BOTH,MAX
        }
        Integer minWidth;
        Integer minHeight;
        Integer minDim;
        Type type;

        public Minimize(int min, Type type) {
            super();
            this.type = type;
            switch(type){
            case WIDTH:
                this.minWidth=min;
                break;
            case HEIGHT:
                this.minHeight=min;
                break;
            case BOTH:
                this.minWidth=min;
                this.minHeight=min;
                break;
            case MAX:
                this.minDim=min;
                break;
            }
        }

        public Minimize(int minWidth, int minHeight) {
            super();
            this.type=Type.BOTH;
            this.minWidth = minWidth;
            this.minHeight = minHeight;
        }

    }

    /**
     * Estimates size of Bitmap in bytes depending on dimensions and Bitmap.Config
     * @param width
     * @param height
     * @param config
     * @return
     */
    public static long estimateBitmapBytes(int width, int height, Bitmap.Config config){
        long pixels=width*height;
        switch(config){
        case ALPHA_8: // 1 byte per pixel
            return pixels;
        case ARGB_4444: // 2 bytes per pixel, but depreciated
            return pixels*2;
        case ARGB_8888: // 4 bytes per pixel
            return pixels*4;
        case RGB_565: // 2 bytes per pixel
            return pixels*2;
        default:
            return pixels;
        }
    }

    private static BitmapFactory.Options getOptionsForDimensions(){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds=true;
        return options;
    }

    private static BitmapFactory.Options getOptionsForSampling(int sampleSize, Bitmap.Config bitmapConfig){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inDither = false;
        options.inSampleSize = sampleSize;
        options.inScaled = false;
        options.inPreferredConfig = bitmapConfig;
        return options;
    }
}

16

Camera/Galleryในตอนหนึ่งของใบสมัครของฉันฉันต้องการที่จะใช้ภาพทั้งจาก หากผู้ใช้คลิกที่ภาพจากกล้อง (อาจจะ 2MP, 5MP หรือ 8MP) ขนาดภาพที่แตกต่างจากkBที่จะMBs หากขนาดภาพน้อยกว่า (หรือสูงถึง 1-2MB) โค้ดด้านบนใช้งานได้ดี แต่ถ้าฉันมีภาพที่มีขนาดมากกว่า 4MB หรือ 5MB ให้OOMใส่เฟรม :(

จากนั้นฉันได้ทำงานเพื่อแก้ไขปัญหานี้และในที่สุดฉันก็ได้ปรับปรุงด้านล่างเป็นรหัสของ Fedor (เครดิตทั้งหมดให้กับ Fedor สำหรับการแก้ปัญหาที่ดี) รหัส :)

private Bitmap decodeFile(String fPath) {
    // Decode image size
    BitmapFactory.Options opts = new BitmapFactory.Options();
    /*
     * If set to true, the decoder will return null (no bitmap), but the
     * out... fields will still be set, allowing the caller to query the
     * bitmap without having to allocate the memory for its pixels.
     */
    opts.inJustDecodeBounds = true;
    opts.inDither = false; // Disable Dithering mode
    opts.inPurgeable = true; // Tell to gc that whether it needs free
                                // memory, the Bitmap can be cleared
    opts.inInputShareable = true; // Which kind of reference will be used to
                                    // recover the Bitmap data after being
                                    // clear, when it will be used in the
                                    // future

    BitmapFactory.decodeFile(fPath, opts);

    // The new size we want to scale to
    final int REQUIRED_SIZE = 70;

    // Find the correct scale value. 
    int scale = 1;

    if (opts.outHeight > REQUIRED_SIZE || opts.outWidth > REQUIRED_SIZE) {

        // Calculate ratios of height and width to requested height and width
        final int heightRatio = Math.round((float) opts.outHeight
                / (float) REQUIRED_SIZE);
        final int widthRatio = Math.round((float) opts.outWidth
                / (float) REQUIRED_SIZE);

        // Choose the smallest ratio as inSampleSize value, this will guarantee
        // a final image with both dimensions larger than or equal to the
        // requested height and width.
        scale = heightRatio < widthRatio ? heightRatio : widthRatio;//
    }

    // Decode bitmap with inSampleSize set
    opts.inJustDecodeBounds = false;

    opts.inSampleSize = scale;

    Bitmap bm = BitmapFactory.decodeFile(fPath, opts).copy(
            Bitmap.Config.RGB_565, false);

    return bm;

}

ฉันหวังว่านี่จะช่วยเพื่อนที่ประสบปัญหาเดียวกัน!

หาข้อมูลเพิ่มเติมโปรดดูที่นี้


14

ฉันเพิ่งพบปัญหานี้สองสามนาทีที่ผ่านมา ฉันแก้ไขมันด้วยการทำงานที่ดีขึ้นในการจัดการอะแดปเตอร์ listview ของฉัน ฉันคิดว่ามันเป็นปัญหากับหลายร้อยภาพ 50x50px ที่ฉันใช้ปรากฎว่าฉันพยายามขยายมุมมองที่กำหนดเองของฉันทุกครั้งที่มีการแสดงแถว เพียงแค่ทดสอบเพื่อดูว่าแถวนั้นสูงเกินจริงหรือไม่ฉันได้ตัดข้อผิดพลาดนี้ออกไปหรือไม่และฉันใช้บิตแมปนับร้อย นี่เป็นจริงสำหรับสปินเนอร์ แต่อะแดปเตอร์ฐานทำงานเหมือนกันทั้งหมดสำหรับ ListView การแก้ไขแบบง่ายนี้ยังปรับปรุงประสิทธิภาพของอะแดปเตอร์อย่างมาก

@Override
public View getView(final int position, View convertView, final ViewGroup parent) {

    if(convertView == null){
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.spinner_row, null);
    }
...

3
ฉันไม่สามารถขอบคุณมากพอสำหรับสิ่งนี้! ฉันกำลังไล่ตามปัญหาที่ผิดก่อนที่จะเห็นสิ่งนี้ คำถามสำหรับคุณ: เนื่องจากแถวแต่ละแถวของฉันมีชื่อและรูปถ่ายที่ไม่ซ้ำกันฉันจึงต้องใช้อาร์เรย์ convertView เพื่อเก็บค่าของแถวแต่ละแถว ฉันไม่เห็นว่าการใช้ตัวแปรเดียวจะช่วยให้คุณทำเช่นนั้นได้อย่างไร ฉันพลาดอะไรไปรึเปล่า?
PeteH

13

ฉันใช้เวลาตลอดทั้งวันในการทดสอบวิธีแก้ปัญหาเหล่านี้และสิ่งเดียวที่ใช้ได้ผลสำหรับฉันคือแนวทางด้านบนเพื่อให้ได้ภาพและเรียก GC ด้วยตนเองซึ่งฉันรู้ว่าไม่ควรจะจำเป็น แต่มันเป็นสิ่งเดียวที่ทำงานได้ เมื่อฉันทำให้แอปของฉันอยู่ภายใต้การทดสอบการโหลดจำนวนมากสลับระหว่างกิจกรรม แอพของฉันมีรายการรูปขนาดย่อในมุมมองแบบรายการ (ให้พูดกิจกรรม A) และเมื่อคุณคลิกที่รูปใดรูปหนึ่งมันจะนำคุณไปยังกิจกรรมอื่น (ให้พูดกิจกรรม B) ที่แสดงภาพหลักสำหรับรายการนั้น เมื่อฉันจะสลับไปมาระหว่างสองกิจกรรมในที่สุดฉันจะได้รับข้อผิดพลาด OOM และแอปจะปิดลง

เมื่อฉันลงครึ่งหนึ่งของลิสต์วิวมันจะพัง

ตอนนี้เมื่อฉันใช้สิ่งต่อไปนี้ในกิจกรรม B ฉันสามารถผ่านรายการทั้งหมดโดยไม่มีปัญหาและดำเนินต่อไปและไปและไป ... และมันเร็วมาก

@Override
public void onDestroy()
{   
    Cleanup();
    super.onDestroy();
}

private void Cleanup()
{    
    bitmap.recycle();
    System.gc();
    Runtime.getRuntime().gc();  
}

รักทางออกของคุณ! ฉันใช้เวลาหลายชั่วโมงในการแก้ไขข้อผิดพลาดนี้ด้วย แก้ไข: น่าเศร้าที่ปัญหายังคงมีอยู่เมื่อฉันเปลี่ยนทิศทางหน้าจอของฉันในโหมดแนวนอน ...
Xarialon

สิ่งนี้ช่วยฉันได้ในที่สุดพร้อมกับ: - ตัวเลือก BitmapFactory.Options = ใหม่ BitmapFactory.Options (); options.InPurgeable = true; options.InSampleSize = 2;
3833732

13

ปัญหานี้เกิดขึ้นในตัวเลียนแบบ Android เท่านั้น ฉันยังประสบปัญหานี้ในเครื่องจำลอง แต่เมื่อฉันตรวจสอบในอุปกรณ์แล้วมันทำงานได้ดี

ดังนั้นโปรดตรวจสอบในอุปกรณ์ มันอาจจะทำงานในอุปกรณ์


12

2 เซนต์ของฉัน: ฉันแก้ไขข้อผิดพลาด OOM ด้วยบิตแมปโดย:

a) ปรับขนาดภาพของฉันด้วยปัจจัย 2

b) ใช้ไลบรารีPicassoในอะแดปเตอร์ที่กำหนดเองของฉันสำหรับ ListView โดยใช้ oneView call ใน getView ดังนี้:Picasso.with(context).load(R.id.myImage).into(R.id.myImageView);


ฉันดีใจที่คุณพูดถึง Picasso เพราะทำให้การโหลดภาพง่ายขึ้นมาก คนที่เก็บไว้จากระยะไกลโดยเฉพาะอย่างยิ่ง
Chrispix

12

ใช้รหัสเหล่านี้สำหรับทุกภาพที่เลือกจาก SdCard หรือแบบดึงได้เพื่อแปลงวัตถุบิตแมป

Resources res = getResources();
WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
Display display = window.getDefaultDisplay();
@SuppressWarnings("deprecation")
int width = display.getWidth();
@SuppressWarnings("deprecation")
int height = display.getHeight();
try {
    if (bitmap != null) {
        bitmap.recycle();
        bitmap = null;
        System.gc();
    }
    bitmap = Bitmap.createScaledBitmap(BitmapFactory
        .decodeFile(ImageData_Path.get(img_pos).getPath()),
        width, height, true);
} catch (OutOfMemoryError e) {
    if (bitmap != null) {
        bitmap.recycle();
        bitmap = null;
        System.gc();
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Config.RGB_565;
    options.inSampleSize = 1;
    options.inPurgeable = true;
    bitmapBitmap.createScaledBitmap(BitmapFactory.decodeFile(ImageData_Path.get(img_pos)
        .getPath().toString(), options), width, height,true);
}
return bitmap;

ใช้ภาพ instend เส้นทางของคุณของImageData_Path.get (img_pos) .getPath ()


12

โดยทั่วไปขนาดฮีปอุปกรณ์ Android มีขนาดเพียง 16MB (แตกต่างจากอุปกรณ์ / ระบบปฏิบัติการดูโพสต์ฮีปขนาด ) หากคุณกำลังโหลดภาพและข้ามขนาด 16MB มันจะโยนออกจากข้อยกเว้นหน่วยความจำแทนการใช้บิตแมปสำหรับโหลด รูปภาพจากการ์ด SD หรือจากแหล่งข้อมูลหรือจากเครือข่ายลองใช้getImageUri การโหลดบิตแมปต้องใช้หน่วยความจำเพิ่มเติมหรือคุณสามารถตั้งค่าบิตแมปเป็นโมฆะถ้างานของคุณเสร็จสิ้นด้วยบิตแมปนั้น


1
และถ้า setImageURI ยังคงได้รับข้อยกเว้นให้อ้างถึงstackoverflow.com/questions/15377186//
Mahesh

11

โซลูชันทั้งหมดที่นี่ต้องการการตั้งค่า IMAGE_MAX_SIZE สิ่งนี้ จำกัด อุปกรณ์ที่มีฮาร์ดแวร์ที่มีประสิทธิภาพมากกว่าและหากขนาดภาพต่ำเกินไปมันก็ดูน่าเกลียดบนหน้าจอ HD

ฉันออกมาพร้อมกับโซลูชันที่ทำงานกับ Samsung Galaxy S3 และอุปกรณ์อื่น ๆ รวมถึงอุปกรณ์ที่มีประสิทธิภาพน้อยกว่าและคุณภาพของภาพที่ดีกว่าเมื่อใช้อุปกรณ์ที่มีประสิทธิภาพยิ่งขึ้น

ส่วนสำคัญของมันคือการคำนวณหน่วยความจำสูงสุดที่จัดสรรสำหรับแอพในอุปกรณ์เฉพาะจากนั้นตั้งค่าสเกลให้ต่ำที่สุดโดยไม่เกินหน่วยความจำนี้ นี่คือรหัส:

public static Bitmap decodeFile(File f)
{
    Bitmap b = null;
    try
    {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;

        FileInputStream fis = new FileInputStream(f);
        try
        {
            BitmapFactory.decodeStream(fis, null, o);
        }
        finally
        {
            fis.close();
        }

        // In Samsung Galaxy S3, typically max memory is 64mb
        // Camera max resolution is 3264 x 2448, times 4 to get Bitmap memory of 30.5mb for one bitmap
        // If we use scale of 2, resolution will be halved, 1632 x 1224 and x 4 to get Bitmap memory of 7.62mb
        // We try use 25% memory which equals to 16mb maximum for one bitmap
        long maxMemory = Runtime.getRuntime().maxMemory();
        int maxMemoryForImage = (int) (maxMemory / 100 * 25);

        // Refer to
        // http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
        // A full screen GridView filled with images on a device with
        // 800x480 resolution would use around 1.5MB (800*480*4 bytes)
        // When bitmap option's inSampleSize doubled, pixel height and
        // weight both reduce in half
        int scale = 1;
        while ((o.outWidth / scale) * (o.outHeight / scale) * 4 > maxMemoryForImage)
        scale *= 2;

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        fis = new FileInputStream(f);
        try
        {
            b = BitmapFactory.decodeStream(fis, null, o2);
        }
        finally
        {
            fis.close();
        }
    }
    catch (IOException e)
    {
    }
    return b;
}

ฉันตั้งค่าหน่วยความจำสูงสุดที่ใช้โดยบิตแมปนี้เป็น 25% ของหน่วยความจำสูงสุดที่จัดสรรคุณอาจต้องปรับให้เหมาะกับความต้องการของคุณและตรวจสอบให้แน่ใจว่าบิตแมปนี้ถูกลบทิ้งและไม่อยู่ในหน่วยความจำเมื่อคุณใช้งานเสร็จแล้ว โดยทั่วไปฉันใช้รหัสนี้เพื่อทำการหมุนภาพ (บิตแมปต้นทางและปลายทาง) ดังนั้นแอปของฉันต้องการโหลด 2 บิตแมปในหน่วยความจำในเวลาเดียวกันและ 25% ให้บัฟเฟอร์ที่ดีโดยไม่มีหน่วยความจำหมดเมื่อทำการหมุนภาพ

หวังว่านี่จะช่วยให้ใครบางคนที่นั่น ..


11

ดังกล่าวOutofMemoryExceptionไม่สามารถแก้ไขได้โดยสิ้นเชิงโดยการเรียกSystem.gc()และอื่น ๆ

โดยอ้างถึงวัฏจักรกิจกรรม

สถานะกิจกรรมถูกกำหนดโดยระบบปฏิบัติการของตัวเองขึ้นอยู่กับการใช้หน่วยความจำสำหรับแต่ละกระบวนการและลำดับความสำคัญของแต่ละกระบวนการ

คุณอาจพิจารณาขนาดและความละเอียดของรูปภาพบิตแมปแต่ละภาพที่ใช้ ผมขอแนะนำให้ลดขนาด, resample ไปที่ความละเอียดต่ำกว่า, อ้างถึงการออกแบบของแกลเลอรี่ (PNG ภาพขนาดเล็กหนึ่งภาพและภาพต้นฉบับหนึ่งภาพ)


11

รหัสนี้จะช่วยในการโหลดบิตแมปขนาดใหญ่จาก drawable

public class BitmapUtilsTask extends AsyncTask<Object, Void, Bitmap> {

    Context context;

    public BitmapUtilsTask(Context context) {
        this.context = context;
    }

    /**
     * Loads a bitmap from the specified url.
     * 
     * @param url The location of the bitmap asset
     * @return The bitmap, or null if it could not be loaded
     * @throws IOException
     * @throws MalformedURLException
     */
    public Bitmap getBitmap() throws MalformedURLException, IOException {       

        // Get the source image's dimensions
        int desiredWidth = 1000;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(context.getResources(), R.drawable.green_background , options);

        int srcWidth = options.outWidth;
        int srcHeight = options.outHeight;

        // Only scale if the source is big enough. This code is just trying
        // to fit a image into a certain width.
        if (desiredWidth > srcWidth)
            desiredWidth = srcWidth;

        // Calculate the correct inSampleSize/scale value. This helps reduce
        // memory use. It should be a power of 2
        int inSampleSize = 1;
        while (srcWidth / 2 > desiredWidth) {
            srcWidth /= 2;
            srcHeight /= 2;
            inSampleSize *= 2;
        }
        // Decode with inSampleSize
        options.inJustDecodeBounds = false;
        options.inDither = false;
        options.inSampleSize = inSampleSize;
        options.inScaled = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        options.inPurgeable = true;
        Bitmap sampledSrcBitmap;

        sampledSrcBitmap =  BitmapFactory.decodeResource(context.getResources(), R.drawable.green_background , options);

        return sampledSrcBitmap;
    }

    /**
     * The system calls this to perform work in a worker thread and delivers
     * it the parameters given to AsyncTask.execute()
     */
    @Override
    protected Bitmap doInBackground(Object... item) {
        try { 
          return getBitmap();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

เยี่ยมมากคิดว่าควรใช้ Loader แทน Async Task ดีกว่าไหม
Chrispix

แล้วไงBitmap.Config.ARGB_565ล่ะ หากคุณภาพไม่สำคัญ
Hamzeh Soboh
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.