Hi, Android developers!
I think each of us is faced with downloading images by URL. The easiest way to solve this problem is to use a ready-made third-party library. As a rule, Universal Image Loader (UIL), Picasso turns out to be one of such ready-made solutions. When I ask the developer why he chose this or that library, I usually get different answers. For example, "Picasso / UIL has no problems with memory leaks," or "Square does only the right things," or just "Yes, I use UIL, it works — and well."
So, I wondered: which of these 2 libraries makes the best use of memory? I am using UIL and have a problem with OutOfMemory on older devices. Perhaps Picasso is a medicine?
So the idea of this benchmark appeared.
The purpose of testing: to determine which of the libraries (UIL or Picasso) uses the minimum memory device.
Test cases:
- Download small images (240x240)
- Download large images (> 400px according to any of the dimensions)
- Download large images and convert their size to the dimensions of ImageView
- Download small images and display them as a round picture
- Download large images and display them in RGB565 configuration
')
Test procedure:
As a list, use a 2-column GridView. The adapter is configured separately for each test case. In the adapter, we return a list of previously prepared URLs, thus creating the same testing conditions.
With a period of 1 second, the list automatically makes one pass down, and then up in 4-step increments. Each step measures the memory used by the application.
We measure the used memory in 3 stages for each test case:
- first run - with a clean application cache;
- second launch: without closing the application after the first pass;
- the third launch - after re-opening the application without clearing the cache.
At the end of the test case, I additionally recorded the size of the cache, which is also important for older devices.
Benchmark sources can be found here.
github.com/artemmanaenko/ImageLoadersTest . The project is compiled under Gradle.
So, below are the results for each test case. The Y axis is the memory used by the application in MB. X axis - the time of the test case.
Loading small images
Cache size: Picasso = 1.39 MB, UIL = 1.17 MB
Download large images
Cache size: Picasso = 3.67 MB, UIL = 5.44 MB
Download large images with up to image size conversion
Cache size: Picasso = 3.67 MB, UIL = 5.44 MB
Loading small images and cropping them to a round image
Cache size: Picasso = 1.39 MB, UIL = 1.17 MB
Downloading large images and displaying them in the RGB565 configuration
The results of experiments with large pictures impressed me, and I decided that it was worth trying to configure the UIL configuration. In order not to heavily load memory with cache - I tried to disable the cache in UIL from RAM. And, as an option, set the cache size of the image - no more than half the screen.
Based on the experiment, I made the following conclusions:
- If your lists work with small images (comparable to the size of ImageView) - the choice of the library is not important for you. Picasso creates a slightly larger cache on the disk, while using less RAM by about the same size.
- Picasso showed tremendous results in memory management, working with large images. UIL apparently keeps the original image in memory. Picasso stores already converted image size. Therefore, the cache on the disk at Picasso is much smaller.
- UIL can work with the same efficiency as Picasso, if it is additionally configured. For example, limit the size of the cache in memory. Or, as in one of the tests, manually limit the size of cached photos. The second method may be unsuitable for use, because it sets the global configuration for ImageLoader.
- Working with round avatars is "cheaper" through Picasso. But, again, due to the fact that I manually called recycle () on the original Bitmap. The same action can be performed in UIL by setting the redefined BitmapDisplayer.
- Picasso is extremely easy to use and already “off the box” works with memory efficiently. This is how initialization and loading for libraries looks like:
Picassopublic class PicassoSquareFitAdapter extends BaseBenchmarkAdapter { public PicassoSquareFitAdapter(Context context, IUrlListContainer urlListContainer) { super(context, urlListContainer); } @Override protected void loadImage(ImageView imageView, String url) { Picasso.with(context).load(url).fit().into(imageView); } }
UIL public class UILSquareFitAdapter extends BaseBenchmarkAdapter { private DisplayImageOptions options; public UILSquareFitAdapter(Context context, IUrlListContainer urlListContainer) { super(context, urlListContainer); ImageLoaderConfiguration config = ImageLoaderConfiguration.createDefault(context); ImageLoader.getInstance().init(config); options = new DisplayImageOptions.Builder() .imageScaleType(ImageScaleType.EXACTLY) .resetViewBeforeLoading(true) .cacheInMemory(true) .cacheOnDisc(true) .build(); } @Override protected void loadImage(ImageView imageView, String url) { ImageLoader.getInstance().displayImage(url, imageView, options); } }
- Picasso has a minus: image transformations and casting to RGB565 need to be done in self-written classes.
RoundTransformation public class RoundTransformation implements Transformation { @Override public Bitmap transform(Bitmap source) { int size = Math.min(source.getWidth(), source.getHeight()); int x = (source.getWidth() - size) / 2; int y = (source.getHeight() - size) / 2; Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size); if (squaredBitmap != source) { source.recycle(); } Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig()); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(); BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); paint.setShader(shader); paint.setAntiAlias(true); float radius = size / 2f; canvas.drawCircle(radius, radius, radius, paint); squaredBitmap.recycle(); return bitmap; } @Override public String key() { return "circle"; } }
Config565Transformation public class Config565Transformation implements Transformation { @Override public Bitmap transform(Bitmap source) { Bitmap resultBitmap = Bitmap.createBitmap( source.getWidth(), source.getHeight(), Bitmap.Config.RGB_565 ); Canvas canvas = new Canvas(resultBitmap); Paint paint = new Paint(); paint.setFilterBitmap(true); canvas.drawBitmap(source, 0, 0, paint); source.recycle(); return resultBitmap; } @Override public String key() { return "Config565Transformation"; } }
For myself, I concluded: projects must be transferred to Picasso. In my case, this will solve the problem with memory overhead. I hope this post will be useful to you too!