bumptech / glide

An image loading and caching library for Android focused on smooth scrolling
https://bumptech.github.io/glide/
Other
34.61k stars 6.12k forks source link

[OutOfMemoryError] Crash when viewpager scroll too fast #2019

Closed chihung93 closed 6 years ago

chihung93 commented 7 years ago
06-09 00:13:11.607 11876-11920/com.myapp.app E/art: Throwing OutOfMemoryError "Failed to allocate a 11197452 byte allocation with 16776720 free bytes and 79MB until OOM"
06-09 00:13:11.617 11876-11920/com.myapp.app E/PriorityExecutor: Request threw uncaught throwable
                                                                       java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError: Failed to allocate a 11197452 byte allocation with 16776720 free bytes and 79MB until OOM
                                                                           at java.util.concurrent.FutureTask.report(FutureTask.java:93)
                                                                           at java.util.concurrent.FutureTask.get(FutureTask.java:163)
                                                                           at com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor.afterExecute(FifoPriorityThreadPoolExecutor.java:96)
                                                                           at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1120)
                                                                           at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
                                                                           at java.lang.Thread.run(Thread.java:818)
                                                                           at com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor$DefaultThreadFactory$1.run(FifoPriorityThreadPoolExecutor.java:118)
                                                                        Caused by: java.lang.OutOfMemoryError: Failed to allocate a 11197452 byte allocation with 16776720 free bytes and 79MB until OOM
                                                                           at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
                                                                           at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
                                                                           at android.graphics.BitmapFactory.decodeStreamInternal(BitmapFactory.java:752)
                                                                           at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:728)
                                                                           at com.bumptech.glide.load.resource.bitmap.Downsampler.decodeStream(Downsampler.java:329)
                                                                           at com.bumptech.glide.load.resource.bitmap.Downsampler.downsampleWithSize(Downsampler.java:220)
                                                                           at com.bumptech.glide.load.resource.bitmap.Downsampler.decode(Downsampler.java:153)
                                                                           at com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder.decode(StreamBitmapDecoder.java:50)
                                                                           at com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder.decode(StreamBitmapDecoder.java:19)
                                                                           at com.bumptech.glide.load.resource.bitmap.ImageVideoBitmapDecoder.decode(ImageVideoBitmapDecoder.java:39)
                                                                           at com.bumptech.glide.load.resource.bitmap.ImageVideoBitmapDecoder.decode(ImageVideoBitmapDecoder.java:20)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecoder.decodeBitmapWrapper(GifBitmapWrapperResourceDecoder.java:121)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecoder.decodeStream(GifBitmapWrapperResourceDecoder.java:94)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecoder.decode(GifBitmapWrapperResourceDecoder.java:71)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecoder.decode(GifBitmapWrapperResourceDecoder.java:61)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecoder.decode(GifBitmapWrapperResourceDecoder.java:22)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperStreamResourceDecoder.decode(GifBitmapWrapperStreamResourceDecoder.java:24)
                                                                           at com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperStreamResourceDecoder.decode(GifBitmapWrapperStreamResourceDecoder.java:14)
                                                                           at com.bumptech.glide.load.resource.file.FileToStreamDecoder.decode(FileToStreamDecoder.java:39)
                                                                           at com.bumptech.glide.load.resource.file.FileToStreamDecoder.decode(FileToStreamDecoder.java:17)
                                                                           at com.bumptech.glide.load.engine.DecodeJob.loadFromCache(DecodeJob.java:222)
                                                                           at com.bumptech.glide.load.engine.DecodeJob.decodeResultFromCache(DecodeJob.java:85)
                                                                           at com.bumptech.glide.load.engine.EngineRunnable.decodeFromCache(EngineRunnable.java:108)
                                                                           at com.bumptech.glide.load.engine.EngineRunnable.decode(EngineRunnable.java:99)
                                                                           at com.bumptech.glide.load.engine.EngineRunnable.run(EngineRunnable.java:58)
                                                                           at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:422)
                                                                           at java.util.concurrent.FutureTask.run(FutureTask.java:237)
                                                                           at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
                                                                           at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) 
                                                                           at java.lang.Thread.run(Thread.java:818) 
                                                                           at com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor$DefaultThreadFactory$1.run(FifoPriorityThreadPoolExecutor.java:118) 

## Phone : Note 3 At&T OS : Android 5.0

ext {
    supportLibraryVersion = '25.2.0'
    playServicesVersion = '10.2.0'
    retrofit = "2.1.0"
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    /**
     * Support
     * */
    compile "com.android.support:appcompat-v7:$supportLibraryVersion"
    compile "com.android.support:support-v4:$supportLibraryVersion"
    compile "com.android.support:design:$supportLibraryVersion"
    compile "com.android.support:percent:$supportLibraryVersion"

    /**
     * Squareup
     * */
    compile "com.squareup.retrofit2:retrofit:$retrofit"
    compile "com.squareup.retrofit2:converter-gson:$retrofit"
    /**
     *  Google
     * */
    compile "com.google.android.gms:play-services-analytics:$playServicesVersion"
    compile "com.google.firebase:firebase-core:$playServicesVersion"
    compile "com.google.android.gms:play-services-gcm:$playServicesVersion"
    compile "com.google.android.gms:play-services-maps:$playServicesVersion"
    compile 'com.google.guava:guava:22.0-rc1-android'

    compile 'com.squareup.okhttp3:logging-interceptor:3.3.1'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'org.greenrobot:eventbus:3.0.0'
    compile 'com.kyleduo.switchbutton:library:1.4.4'
    compile 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
    compile 'com.github.arimorty:floatingsearchview:2.0.3'
    compile 'org.androidannotations:androidannotations-api:4.1.0'
    compile 'com.wang.avi:library:2.1.3'
    compile 'com.thefinestartist:finestwebview:1.2.7'
    compile 'com.github.pwittchen:reactivenetwork-rx2:0.9.1'
    compile 'me.relex:circleindicator:1.2.2@aar'
    apt 'org.androidannotations:androidannotations:4.1.0'
}
public class ImageHelper {

    static final String TAG = ImageHelper.class.getSimpleName();

    private static void config(String url, ImageView imageView, int width, int height, RequestListener<String, Bitmap> target) {
        if (!TextUtils.isEmpty(url)) {
            try {
                url = UrlHelper.encodeImage(url, width, height);
                if (target == null) {
                    Glide
                            .with(imageView.getContext())
                            .load(url)
                            .asBitmap()
                            .dontAnimate()
                            .placeholder(R.drawable.ic_img_default)
                            .into(imageView);
                } else {
                    Glide
                            .with(imageView.getContext())
                            .load(url)
                            .asBitmap()
                            .dontAnimate()
                            .placeholder(R.drawable.ic_img_default)
                            .listener(target).into(imageView);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.d(TAG, url);
        }
    }

    public static void displayUrl(String url, ImageView imageView, int width, int height) {
        config(url, imageView, width, height, null);
    }

    public static void displayResource(int urlLocal, ImageView imageView) {
        if (imageView != null) {
            displayResource(urlLocal, imageView, ScreenHelper.getWidthPortrait(), ScreenHelper.getWidthPortrait(), null);
        }
    }

    public static void displayResource(int urlLocal, ImageView imageView, int height, int width) {
        if (imageView != null) {
            displayResource(urlLocal, imageView, height, width, null);
        }
    }

    public static void displayResource(int urlLocal, ImageView imageView, RequestListener<Integer, GlideDrawable> listener) {
        if (imageView != null) {
            displayResource(urlLocal, imageView, ScreenHelper.getWidthPortrait(), ScreenHelper.getWidthPortrait(), listener);
        }
    }

    private static void displayResource(int urlLocal, ImageView imageView, int height, int width, RequestListener<Integer, GlideDrawable> listener) {
        if (imageView != null) {
            Glide.with(imageView.getContext())
                    .load(urlLocal)
                    .override(width, height)
                    .listener(listener)
                    .into(imageView);
        }
    }

    public static void displayUrl(String url, int width, int height, ImageView imageView, RequestListener<String, Bitmap> target) {
        config(url, imageView, width, height, target);
    }

    public static void displayUrl(String url, ImageView imageView, int width, int height, RequestListener<String, Bitmap> target) {
        if (!TextUtils.isEmpty(url) && imageView != null) {
            Glide
                    .with(imageView.getContext())
                    .load(url)
                    .asBitmap()
                    .dontAnimate()
                    .override(width, height)
                    .listener(target).into(imageView);
            Log.d(TAG, url);
        }
    }

    public static void clear(View view) {
        if (view != null)
            Glide.clear(view);
    }

    public static void clear(Context context) {
        if (context != null)
            Glide.get(context).clearMemory();
    }
}

## In My Fragment

/**
     * needed to onStop
     **/
    @Override
    public void onStop() {
        super.onStop();
        if (viewPager != null) {
            viewPager.setListener(null);
        }
        if (noImages != null) {
            noImages.setOnTouchListener(null);
        }
        viewPager = null;
        viewPagerAdapter = null;

        ImageHelper.clear(noImages);
        ImageHelper.clear(mActivity);
    }
gajicm93 commented 7 years ago

What kind of Adapter are you using? Try using Glide.clear() in the destroyItem method of you adapter, in case you're using PagerAdapter..

chihung93 commented 7 years ago

In MyFragment, I'm using PagerAdapter.

But Activity Parent I'm using FragmentPagerAdapter.

Like this: I have an Activity A, using FragmentPagerAdapter with MyFragment, In My Fragment, I'm using PagerAdapter for images

InnerViewPager viewPager;
BannerSliderAdapter viewPagerAdapter;

viewPagerAdapter = new BannerSliderAdapter(viewPager.getContext(), items, ScreenHelper.getWidthPortrait(), heightOfHeader);

public class BannerSliderAdapter extends PagerAdapter {

    public static final String TAG = "BannerSliderAdapter";
    private Context context;

    // data
    private List<String> images = new ArrayList<>();
    // for scale
    int widthImage = 0;
    int heightImage = 0;

    private OnItemClickListener listener;

    public void setOnItemClickListener(OnItemClickListener listener) {
        this.listener = listener;
    }

    public BannerSliderAdapter(Context context, List<String> imgs,int widthImage,int heightImage) {
        this.context = context;
        this.images = imgs;
        this.widthImage = widthImage;
        this.heightImage = heightImage;
    }

    @Override
    public Object instantiateItem(final ViewGroup collection, final int position) {
        final ImageView imageView = new ImageView(context);
        imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
        imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        ImageHelper.displayUrl(images.get(position), imageView, widthImage, heightImage);
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (listener != null) {
                    listener.onItemClicked(collection, imageView, images.get(position), position, TAG);
                }
            }
        });
        collection.addView(imageView);
        return imageView;
    }

    @Override
    public int getCount() {
        return images.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public void destroyItem(ViewGroup collection, int position, Object view) {
        View itemView = (View)view;
        collection.removeView(itemView);
        itemView = null;
    }
}
gajicm93 commented 7 years ago
  1. What are the widthImage and heightImage parameters and why are you using them instead of Glides default resizing? They shouldn't be bigger than the view.
  2. Don't use ImageViews native CENTER_CROP, instead use Glides centerCrop transformation. This should allow glide to actually crop the Bitmap itself, making it smaller, instead of Android framework just cropping a larger uncropped Bitmap
  3. In destroyItem method, add Glide.clear(object), and cast it to ImageView.
chihung93 commented 7 years ago

1.

int heightOfHeader = (int) ((float) ScreenHelper.getWidthPortrait() / 1.5f);
public class ScreenHelper {

    public static int getWidth(){
        DisplayMetrics displaymetrics = Resources.getSystem().getDisplayMetrics();
        return displaymetrics.widthPixels;
    }

    public static int getHeight(){
        DisplayMetrics displaymetrics = Resources.getSystem().getDisplayMetrics();
        return displaymetrics.heightPixels;
    }

    public static int getWidthPortrait(){
        return getWidth() > getHeight() ? getHeight() : getWidth();
    }

    public static int getHeightPortrait(){
        return getWidth() > getHeight() ? getWidth() : getHeight();
    }
}

2.How to add attributes (centerCrop) to particular cases in order to easily reuse in ImageHelper?

3.If I clear it in destroyItem, Is it still cached? or it just stops loading.

gajicm93 commented 7 years ago
  1. Ok that code should be fine if image is full screen. But you should know that you don't actually need it at all, as far as Glide is concerned, as it automatically fetches View size and resizes a Bitmap.
  2. You could just pass a RequestOptions.centerCrop() object into displayUrl method, or null otherwise, and then just check if its null..
  3. Yes it caches. clear method just notifies Glide that the Bitmap is no longer needed and can either be reused or recycled..
chihung93 commented 7 years ago

Thank you so much @gajicm93.

1. 1.1 - I have read some issues about out of memory, so I think the best way is to use an override to avoid out of memory on older devices (low-end devices) instead of automatically. 1.2 - If I use override, does it resize a bitmap automatically?

2. 2.1 - Very useful, I don't know that option before. is it in version 3.x? or just for version 4.0 2.2 - What is the difference between CENTER_CROP native and glide? Can you explain me?

  1. This is strange.Because I use imageView.getContext() .If it is destroyed, I think the Bitmap is no longer needed and can either be reused or recycled.
gajicm93 commented 7 years ago
  1. It shouldn't matter.. Glide does the same thing as you, but automatically, and it's much more tested than any implementation you can do. If you think Glide has a resizing bug on lower end devices, file a bug report, but I don't think that's true. When you're using a library as good and well tested as this, it's best to rely on the library as much as you can. 2.1. It was always available. But v4 now even has a circleCrop, so you don't have to use any 3rd party libraries if you need circle ImageViews.. 2.2. I'm not 100% sure Glide works like this, didn't read this part of the code (though I might), but essentially this is how it should work: Imagine you're downloading a 1000x1000 image, and your view is 300x500. So it should be both cropped and resized. In case you're NOT using Glide's crop, Glide would create 500x500 Bitmap in memory, and then give it to ImageView to do native crop. That's a waste of memory. But when you use Glide's crop, Glide automatically knows ImageView size, and can crop+rezise it directly to 300x500, which saves RAM on the Java heap. But once again, I'm not 100% sure it works this way, I'll probably read a bit of code to see exactly what it does..
  2. Using the context of the ImageView is the right thing to do, and Glide properly handles the case when the whole Context gets destroyed, but there's a different problem. Your Context almost always outlives most of the Bitmaps.. So when you're in a Fragment or Activity, and sliding ViewPager, your Context is alive all of the time, but most of the images should be cleared as soon as they're off the screen. That's why you need to use clear manually. Glide handles it automatically on RecyclerView due to ViewHolders and View reuse, but I think it doesn't on Pager Adapter, as there's no View reuse.
chihung93 commented 7 years ago

Thanks for patiently explaining to me. @gajicm93

1 + 2.1. Previously, I read a few Glide topics, if I have 1000x1000 images, and override (500,500), it will resize to 500x500 image and not resize anymore. I think that.

2.1 I can't find RequestOptions in version 3.7 in code and Doc.I only saw in version 4.0 Can you give me a link to it in version 3.7? Because version 4.0 it is not really stable to use.

gajicm93 commented 7 years ago
  1. That's right, if you override, but you shouldn't override, unless you have a very special reason to do it.
  2. 4.0 is stable enough and you should upgrade, but for 3.7, it should be just Glide.with(context) .load(“/user/profile/photo/path”) .asBitmap() .toBytes() .centerCrop()...
chihung93 commented 7 years ago

Thank you so much @gajicm93

  1. I think I'll try Glide.clear(ImageView) in destroyItem.
  2. Why should use .toBytes() with asBitmap() ? This is a reason I still don't want to upgrade.

    1996

    1972

    ....

gajicm93 commented 7 years ago

Honestly not sure what toBytes and asBitmap do, I just copied it from Glide documentation.. You probably don't need it..

chihung93 commented 7 years ago

Thank you so much @gajicm93 again .

chihung93 commented 7 years ago

@gajicm93 Hello , I read Glide's documentation, I just want to make sure Glide will download the image and save the cache and then scale it ? or save it after scaling?. Because I think it will save the cache first, and will scale later, So when I use the centerCrop attribute of native or glide is the same.

From Doc 3.x

Center crop

Center crop first scales your image down maintaining the original aspect ratio so that one dimension of the image is exactly equal to your target dimensions and the other dimension of the image is greater than your target dimensions. Center crop then crops out the center portion of the image.

Center crop performs the same transformation as Android's ScaleType.CENTER_CROP

zhanghai commented 7 years ago

I'm also confirming this bug - I moved from Volley to OkHttp3 and from Glide 3 to 4 and found this issue. It happens after I have viewed a number of large images (around 10). The stacktrace is nearly same. I'm also using PageAdapter.

I didn't encounter this issue while I was using Glide 3 with Volley integration which has been more than 1 year, so I think this is possibly some regression inside Glide 4.0.

I'm using 4.0.0-SNAPSHOT due to the compiler bug of duplicate as().

I have tried Glide.with(ImageView).clear(ImageView), but the problem persists.

EDIT:

After I fixed a memory leak in my Glide extension, the continuous viewable amount of large images went up to some 20~30 in my stress test, which is perhaps alright.

But still, I think Glide should notified users of the OutOfMemoryError instead of getting neither onLoadFailed nor onResourceReady called in this case.

My stacktrace:

06-15 22:23:03.221 29128-29167/me.zhanghai.android.douya E/GlideExecutor: Request threw uncaught throwable
                                                                          java.lang.OutOfMemoryError: Failed to allocate a 48771084 byte allocation with 5816720 free bytes and 5MB until OOM
                                                                              at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
                                                                              at android.graphics.Bitmap.nativeCreate(Native Method)
                                                                              at android.graphics.Bitmap.createBitmap(Bitmap.java:832)
                                                                              at android.graphics.Bitmap.createBitmap(Bitmap.java:809)
                                                                              at android.graphics.Bitmap.createBitmap(Bitmap.java:776)
                                                                              at com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool.get(LruBitmapPool.java:129)
                                                                              at com.bumptech.glide.load.resource.bitmap.TransformationUtils.rotateImageExif(TransformationUtils.java:281)
                                                                              at com.bumptech.glide.load.resource.bitmap.Downsampler.decodeFromWrappedStreams(Downsampler.java:257)
                                                                              at com.bumptech.glide.load.resource.bitmap.Downsampler.decode(Downsampler.java:176)
                                                                              at com.bumptech.glide.load.resource.bitmap.Downsampler.decode(Downsampler.java:134)
                                                                              at com.bumptech.glide.load.resource.bitmap.ByteBufferBitmapDecoder.decode(ByteBufferBitmapDecoder.java:31)
                                                                              at com.bumptech.glide.load.resource.bitmap.ByteBufferBitmapDecoder.decode(ByteBufferBitmapDecoder.java:15)
                                                                              at com.bumptech.glide.load.engine.DecodePath.decodeResourceWithList(DecodePath.java:67)
                                                                              at com.bumptech.glide.load.engine.DecodePath.decodeResource(DecodePath.java:52)
                                                                              at com.bumptech.glide.load.engine.DecodePath.decode(DecodePath.java:43)
                                                                              at com.bumptech.glide.load.engine.LoadPath.loadWithExceptionList(LoadPath.java:56)
                                                                              at com.bumptech.glide.load.engine.LoadPath.load(LoadPath.java:42)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.runLoadPath(DecodeJob.java:454)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.decodeFromFetcher(DecodeJob.java:447)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.decodeFromData(DecodeJob.java:433)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.decodeFromRetrievedData(DecodeJob.java:387)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.onDataFetcherReady(DecodeJob.java:356)
                                                                              at com.bumptech.glide.load.engine.DataCacheGenerator.onDataReady(DataCacheGenerator.java:91)
                                                                              at com.bumptech.glide.load.model.ByteBufferFileLoader$ByteBufferFetcher.loadData(ByteBufferFileLoader.java:68)
                                                                              at com.bumptech.glide.load.engine.DataCacheGenerator.startNext(DataCacheGenerator.java:71)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:279)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:246)
                                                                              at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:222)
                                                                              at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
                                                                              at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
                                                                              at java.lang.Thread.run(Thread.java:818)
                                                                              at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:347)
stale[bot] commented 6 years ago

This issue has been automatically marked as stale because it has not had activity in the last seven days. It will be closed if no further activity occurs within the next seven days. Thank you for your contributions.

sjudd commented 6 years ago

We should be catching OOMs as of 74fcad1340da13ac302994873e076731ab96b8e6 which should be in 4.3.0.

stale[bot] commented 6 years ago

This issue has been automatically marked as stale because it has not had activity in the last seven days. It will be closed if no further activity occurs within the next seven days. Thank you for your contributions.