For various Android applications, I need large ListViews, i.e. such views with 100-300 entries.
All entries must be loaded in bulk when the application is started, as some sorting and processing is necessary and the application cannot know which items to display first, otherwise.
So far, I've been loading the images for all items in bulk as well, which are then saved in an ArrayList<CustomType> together with the rest of the data for each entry.
But of course, this is not a good practice, as you're very likely to have an OutOfMemoryException then: The references to all images in the ArrayList prevent the garbage collector from working.
So the best solution is, obviously, to load only the text data in bulk whereas the images are then loaded as needed, right? The Google Play application does this, for example: You can see that images are loaded as you scroll to them, i.e. they are probably loaded in the adapter's getView() method. But with Google Play, this is a different problem, anyway, as the images must be loaded from the Internet, which is not the case for me. My problem is not that loading the images takes too long, but storing them requires too much memory.
So what should I do with the images? Load in getView(), when they are really needed? Would make scrolling sluggish. So calling an AsyncTask then? Or just a normal Thread? Parametrize it?
I could save the images that are already loaded into a HashMap<String,Bitmap>, so that they don't need to be loaded again in getView(). But if this is done, you have the memory problem again: The HashMap stores references to all images, so in the end, you could have the OutOfMemoryException again.
I know that there are already lots of questions here that discuss "Lazy loading" of images. But they mainly cover the problem of slow loading, not too much memory consumption.
Edit: I've now decided to start AsyncTasks in getView() which load the image into the ListView in the background. But this causes my application to run into an RejectedExecutionException. What should I do now?
I took the approach of loading the images with an AsyncTask and attaching the task to the view in the adapter's getView function to keep track of which task is loading in which view. I use this in an app of mine and there's no scroll lag and all images are loaded in the proper position with no exceptions being thrown. Also, because the task does no work if it's canceled, you can perform a fling on your list and it should lag up at all.
The task:
public class DecodeTask extends AsyncTask<String, Void, Bitmap> {
private static int MaxTextureSize = 2048; /* True for most devices. */
public ImageView v;
public DecodeTask(ImageView iv) {
v = iv;
}
protected Bitmap doInBackground(String... params) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inPurgeable = true;
opt.inPreferQualityOverSpeed = false;
opt.inSampleSize = 0;
Bitmap bitmap = null;
if(isCancelled()) {
return bitmap;
}
opt.inJustDecodeBounds = true;
do {
opt.inSampleSize++;
BitmapFactory.decodeFile(params[0], opt);
} while(opt.outHeight > MaxTextureSize || opt.outWidth > MaxTextureSize)
opt.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(params[0], opt);
return bitmap;
}
#Override
protected void onPostExecute(Bitmap result) {
if(v != null) {
v.setImageBitmap(result);
}
}
}
The adapter stores an ArrayList that contains the file paths of all the images that need loaded. The getView function looks like this:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView iv = null;
if(convertView == null) {
convertView = getLayoutInflater().inflate(R.id.your_view, null); /* Inflate your view here */
iv = convertView.findViewById(R.id.your_image_view);
} else {
iv = convertView.findViewById(R.id.your_image_view);
DecodeTask task = (DecodeTask)iv.getTag(R.id.your_image_view);
if(task != null) {
task.cancel(true);
}
}
iv.setImageBitmap(null);
DecodeTask task = new DecodeTask(iv);
task.execute(getItem(position) /* File path to image */);
iv.setTag(R.id.your_image_view, task);
return convertView;
}
NOTE: Just a caveat here, this might still give you memory problems on versions 1.5 - 2.3 since they use a thread pool for AsyncTask. 3.0+ go back to the serial model by default for executing AsyncTasks which keeps it to one task running at a time, thus using less memory at any given time. So long as your images aren't too big though, you should be fine.
UPDATE: While this solution will still work, there have been great additions to the open source community for solving this problem in a cleaner way. Libraries like Glide or Picasso both handle loading items in a list quite well and I'd recommend you look into one of those solutions if possible.
1) To solve your memory problem with HashMap<String, Bitmap>: Can you use WeakHashMap so that images are recycled when needed? The same should work for the ArrayList you mentioned in the beginning, if your CustomType has weak references to images.
2) What about NOT loading images while user scrolls through the list, but instead when user stops scrolling, load images at that moment. Yes, the list will not look fancy without images, but it will be very efficient during scroll, and while user scrolls he does not see details anyway. Techinally it should work like this:
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// Don't care.
}
#Override
public void onScrollStateChanged(final AbsListView view, int scrollState) {
// Load images here. ListView can tell you what rows are visible, you load images for these rows and update corresponding View-s.
}
})
3) I have an app that loads about 300 images during app start and keeps them for app lifetime. So that's an issue for me too, but so far I have seen very few OOM reports and I suspect they happened because of a different leak. I try to save some memory by using RGB_565 profile for ListView images (there's no significant difference in quality for this purpose) and I use 96x96 max image size, that should be enough for standard list item height.
Do not store all of the images in a list because it's too heavy. You should start an AsyncTask in getView, get,decode you image in InBackground and draw an image on the imageView in PostExecute. To maintain performance of your list you could also use the convertView parameter from getView method, but it starts to be complicated with AsyncTask because your view can be recycled before AsyncTask finishes and you should handle this extra...
You could use LruCache but this only make sense when images are downloaded from internet. When they are stored localy there is no point in using it.
check this out: https://github.com/DHuckaby/Prime
I would use this for images in a ListView vs trying to solve it yourself.. I actually use it for all remote images in my apps.. or at least read the source code.. image management is a pain.. lean on a mature library to get you going.
The link that you provided is good for understanding what is convertView, asyncTask etc.I dont think doing View v = super.getView(position, convertView, parent); would work. If you want to recycle views you should do if(convertView != null){ myView = convertView} else{ inflate(myView)};
About AsyncTask that's right its different in different APIS but when you use execute() for old API and executeOnExecutor on the new one - I think everything is fine. You should pass URI and ImageView to your AsyncTask. Here you could have problem with convertView that it appears in a new row with AsyncTask working on it's image. You can hold for example hashmap of ImageView-AsyncTask and cancel these which are not valid any more.
PS.Sorry for creating a new comment but it was too long for inline answer :)
private class CallService extends AsyncTask<String, Integer, String>
{
protected String doInBackground(String... u)
{
fetchReasons();
return null;
}
protected void onPreExecute()
{
//Define the loader here.
}
public void onProgressUpdate(Integer... args)
{
}
protected void onPostExecute(String result)
{
//remove loader
//Add data to your view.
}
}
public void fetchReasons()
{
//Call Your Web Service and save the records in the arrayList
}
Call new CallService().execute(); in onCreate() method
A small piece of advice would be to disable loading of images when a fling occurs.
dirty HACK to make everything faster:
In your Adapter's getItemViewType(int position), return the position:
#Override
public long getItemViewType(int position) {
return position;
}
#Override
public long getViewTypeCount(int position) {
return getCount();
}
#Override
public View getView(int position, View convertView, ViewGroup arg2) {
if (convertView == null) {
//inflate your convertView and set everything
}
//do not do anything just return the convertView
return convertView;
}
ListView will decide the amount of images to cache.
Related
I am querying the SQLite database and putting the data into a List View. One of the database rows contains an image Url field (which can also be a Uri).
The images are loaded as they should but as soon as I scroll the list all the images start flickering, some are changing places or displaying in the different places.
I already understood that this behavior is happening because the List View is reusing rows on scroll, but I have no idea how to fix this behavior. Also I cannot use external libraries like Picasso in this project.
Here is my adapter code:
public class FilmsListCustomAdapter extends CursorAdapter {
private LayoutInflater cursorInflater;
public FilmsListCustomAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
cursorInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public void bindView(View view, Context context, Cursor cursor) {
TextView filmTitle = (TextView) view.findViewById(R.id.filmListTitle);
TextView filmScore = (TextView) view.findViewById(R.id.filmListScore);
ImageView filmImage = (ImageView)view.findViewById(R.id.filmListPoster);
ImageView filmSeen = (ImageView)view.findViewById(R.id.filmListSeen);
String title = cursor.getString( cursor.getColumnIndex("title") );
String score = cursor.getString(cursor.getColumnIndex("score"));
String url = cursor.getString( cursor.getColumnIndex("url") );
int seen = cursor.getInt( cursor.getColumnIndex("seen") );
if(Patterns.WEB_URL.matcher(url).matches()){
LoadImage loadImage = new LoadImage(context,filmImage);
loadImage.execute(url);
}
else{
Bitmap bmp = BitmapFactory.decodeFile(url);
CamImage camImage = new CamImage(context,Uri.parse(url));
Bitmap rotetedIm = camImage.rotateCamImage(bmp,url);
if(rotetedIm!=null){filmImage.setImageBitmap(Bitmap.createScaledBitmap(rotetedIm, 850, rotetedIm.getHeight(), false));}
else{filmImage.setImageResource(R.drawable.no_poster);}
}
GlobalMethods methods = new GlobalMethods(context);
filmTitle.setTypeface(methods.getWalkFont());
filmTitle.setText(title);
filmScore.setText(score);
if(seen==1){filmSeen.setImageResource(R.drawable.eye);}
else{filmSeen.setImageResource(0);}
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
return cursorInflater.inflate(R.layout.film_row, viewGroup, false);
}
}
What is probably happening is this:
You get a web URL for an image and queue an AsyncTask to download it for an ImageView
You scroll and the ImageView is recycled
This time the ImageView gets a local URL, so you do that immediately
The previously queued AsyncTask completes and loads a now unrelated image over the image you just put in
The key to clearing this up is to make sure to cancel the task once the ImageView is recycled. One way you can do this is to put a reference to the AsyncTask in a tag in the ImageView. When you get a recycled ImageView, you check the tag and see if there is a task in progress and cancel it before you start a new task.
You should check out this article on Android Developers Blog, it will explain a little more about the problem and how to fix it:
Multithreading For Performance | Android Developers Blog
I think this article was written back when AsyncTasks were changed to run in parallel threads, since they talk about tasks completing out of order. They've since reverted to serial execution so I don't think that part applies anymore, but the concept is similar since your immediate loading of the local image acts like a task executing out of order.
Two other things I would consider:
In getView, always call imageView.setImageBitmap(null) first to clear out any leftover image in the recycled ImageView. I like to init ImageViews witth a very neutral gray bitmap that represents an "Image Loading" state.
Use an AsyncTask to decode the local files as well as retrieve the web files. I'll bet your list scrolling will seem a lot smoother when you do this.
I implemented ListView to display some items from the SQLite Database. Each of which contains an Image and some Data.
I want my ListView to work faster even if it contains thousand rows of data. So i tried to implement some optimizations that i have noticed. Here is the basic structure of my CustomCursorAdapter:
Class CustomCursorAdapter extends CursorAdapter
{
Cursor cursor;
public CustomCursorAdapter(..., Cursor _cursor)
{
cursor = _cursor;
}
public void bindView(View _view, Context _context, Cursor _cursor)
{
if( view.getTage() == null)
{
//create and initialize a new holder and set it to view tag.
holder = new Holder();
...
...
holder.imageView = (ImageView) view.findViewById(R.id.image);
holder.imageView.setImageDrawable(defaultDrawable);
view.setTag(holder);
}
String mediaID = _cursor.getString("media_id");
//create an asyncTask to load the image from database
new MediaLoader(context, holder.imageView).execute(mediaID);
}
private class MediaLoader extends AsyncTask<String, Void, Media>
{
private Context context;
private final WeakReference <ImageView> imageViewReference;
public MediaLoader(Context _context, ImageView _imageView)
{
context = _context;
imageViewReference = new WeakReference<ImageView>(_imageView);
}
protected Media doInBackground(String... args)
{
String _mediaID = args[0];
return MediaDataManager.getInstance(_context).getMediaObjectForListAdapter(_mediaID);
}
protected void onPostExecute(final Media _media)
{
super.onPostExecute(_media);
if( imageViewReference != null )
{
ImageView _imageView = imageViewReference.get();
if(_imageView != null)
{
if( _media != null && _media.getImage() != null )
{
_imageView.setImageBitmap(_media.getImage());
}
else
{
_imageView.setImageDrawable(defaultDrawable);
}
}
}
}
}//Media Loader ends.
}//Custom Cursor Adapter Ends.
Using this approach loading time of image seemed ok to me.
But in some android devices (low configuration ones), i am experiencing image flickering. For some reason during scrolling or even loading i noticed images keeps changing throughout the list. But the final image that remains in every row is always the correct one.
Edit:
Loading of images one by one is not a problem for me. But showing some irrelevant images before showing the correct one is my only concern.
I couldn't find any helpful resource by searching. Any kind of help is very much appreciated.
Let's imagine that you have 100 rows in your Cursor. And, let's suppose that this is a really short ListView, where only 2 rows are visible. And, let's suppose that after you load up the adapter in the ListView, the user flings through the whole list.
What your code will do is:
Fork 100 AsyncTask instances, where on Android 3.2+ they will only execute one at a time (unless your targetSdkVersion is fairly low)
Download 100 images
Put each of those 100 images into the rows as they come in
All of this, for a case where you only need 2 images, the ones at the end.
This is why you really should consider using an existing library for this sort of thing, like Picasso, where you could plug in logic to pull values out of... well, wherever your images are actually stored. These sorts of libraries already handle these sorts of situations.
If you insist upon implementing this yourself, you will need to add in the smarts to realize that if the user scrolled the list and we are recycling a row, that we no longer need previous tasks that are tied to that row. Cancel those and queue up a task to download what you need. Also, consider using executeOnExecutor() on API Level 11+, so some of these will run in parallel.
I am using a SimpleCursorTreeAdapter to display data in database. I am using loaders to manage all the cursors. Everything works fine. But there are some images in each of the child views. This causes a noticeable stutter when scrolling. So I want to use an asynctask to decode the images in background. Something like (pseudocode):
#Override
protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
super.bindChildView(view, context, cursor, isLastChild);
String imgFile = cursor.getString(MyDatabaseHelper.FILE_INDEX);
new asyncLoadImage().execute(imgFile);
}
private class asyncLoadImage extends AsyncTask<String, Void, Bitmap> {
#Override
protected Bitmap doInBackground(String... arg0) {
String imgFile = arg0[0];
return Utils.getBitMap(imgFile);//psuedocode
}
#Override
protected void onPostExecute(Bitmap bm) {
ImageView imageView = new ImageView(mCtx);
imageView.setTag(mID);
imageView.setImageBitmap(bm);
//ok got the imageview. where do I append it ??
}
}
By the time the imageview is ready in onPostExecute() function, the view provided in bindChildView might have been recycled and pointing to some different child element. How do I determine where to append the imageview?
First, don't append. You need the ImageView there anyway. Have the ImageView use a placeholder image, that you replace with the final image when it is ready.
Second, consider using an existing library that knows about Adapter recycling, such as Picasso. Admittedly, I don't know if Picasso has support for ExpandableListAdapter, as I rarely use ExpandableListView.
If you determine that there is no suitable library, you'll need some logic in your getChildView() and/or getGroupView() methods that can deal with recycling and your background work. One fairly simple approach is to tuck the URL of the image needed by the ImageView in the tag of the ImageView via setTag(). Your onPostExecute() can then compare the URL it just downloaded with the URL from the ImageView tag, and if they do not match, do not update the ImageView. This means that you will download some images unnecessarily, so a more sophisticated approach will arrange to cancel the AsyncTask that is downloading the image. And I am sure that there are other approaches as well.
I'm using a similar approach to the answer of this question. The only real difference is that instead of a SoftReference<Bitmap based cache, I'm saving the images to /data/data/my.app/files since they're not expected to change too often. My adapter's getView() function:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
//data from your adapter
MyItem entry = getItem(position);
//we want to reuse already constructed row views...
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.appitem, null);
}
convertView.setTag(entry);
TextView Name = (TextView)convertView.findViewById(R.id.Name);
TextView Version = (TextView)convertView.findViewById(R.id.Version);
final ImageView Icon = (ImageView)convertView.findViewById(R.id.Icon);
Name.setText(entry.getName());
Version.setText(entry.getVersion());
Icon.setImageResource(R.drawable.default_icon); // the problem line
try {
final String _id = entry.getID();
imageLoader.loadImage(_id, "<my url>", new ImageThreadLoader.ImageLoadedListener() {
public void imageLoaded(Bitmap imageBitmap) {
Icon.setImageBitmap(imageBitmap);
notifyDataSetChanged();
}
});
} catch (Throwable t) {
Log.e("","", t); // nothing is hitting this log
}
return convertView;
}
The marked "problem line" above where I set the icon to a default icon, if I remove that line then things mostly work ok (when Views are reused it'll display the old image shortly before showing the new one). If that line is present then the image never changes to anything else. The anonymous ImageLoadedListener is still run on the UI thread and setting a breakpoint there reveals everything seems to be happening normally. I also know that the ImageThreadLoader is working correctly. The files appear where they should and look just fine (and they load fine when the problem line above is removed).
Why would setting the image ahead of time result in not being able to update it later?
a hunch:
remove the notifyDataSetChanged().
Why? the call adapter.notifyDataSetChanged() will provoqc a FULL refresh of the list (ie: of each view). And so, a new call for each item to getView(position). In this call you change again the image to default_icon. This append JUST AFTER setting the good one!
so the sequence is: set default_icon for one image, load one image from disk, invalidate -> set default for ALL, load one image from disk, ....
EDIT: clarify explanation, remove assumption about thread limit.
I am currently trying to make the scrolling of my GridView smoother by moving some work from the UI-thread into other threads, but I get strange behavior: sometimes one image is drawn to more then one view. I am using Xamarin (C#).
Below you can find the simplyfied version of my current code. LoadShowImageAsync is called from within the GetView() in my gridview Adapter. If I call LoadShowImageDoWork() singlethreaded everything is fine but when I call it via the ThreadPool it shows the strange behaviour.
In the debuglogs I see that the method is left after decoding the image and the RunOnUiThread stuff is executed combined for several images later. Then some other Images gets decoded and then RunOnUiThread stuff is done combined for them. I guess that somehow the bitmap content gets mixed up here.
Has anyone an idea what I can do to make it work multithreaded?
public class LoadImageAsyncDatas {
public int ImageId { get; private set;}
public ImageView ImageView { get; private set;}
public LoadImageAsyncDatas (ImageView imageView,int imageId) {
ImageId = imageId;
ImageView = imageView;
}
}
public class LoadImageAsync {
private object locker;
Activity activity;
public LoadImageAsync (Activity activity) {
this.activity = activity;
locker = new object ();
}
public void LoadShowImageAsync (ImageView imageView,int imageId) {
object stateInfo = new LoadImageAsyncDatas (imageView, imageId);
// call LoadShowImageDoWork() either multithreaded (strange behaviour) or single threaded (works)
ThreadPool.QueueUserWorkItem (new WaitCallback (LoadShowImageDoWork), stateInfo);
// LoadShowImageDoWork (stateInfo);
}
private void LoadShowImageDoWork (object stateInfo) {
lock (locker) { // lock is only for debugging
LoadImageAsyncDatas imageData = stateInfo as LoadImageAsyncDatas;
byte[] imageBytes = LoadFileByImageId(imageData.ImageId);
Bitmap bitmap = BitmapFactory.DecodeByteArray (imageBytes, 0, imageBytes.Length, myBitmapOptions);
activity.RunOnUiThread(() => {
imageData.ImageView.SetImageBitmap(bitmap);
});
}
}
}
Thanks!
Jens
If you want each bitmap in an Android list control to be unique then you must manage unique bitmaps for each visible "cell" on the display. If you try to re-use a bitmap that is currently being used to display another cell, you'll change both of them. For example, if you use a single bitmap to handle all of the GetView() requests of the list, each time you update the bitmap, all of the cells in the list which were set to that bitmap object will display the new bitmap. In my applications I manage this situation by setting a unique ID value (the list index) into the ImageView object. When a GetView() request is sent, I can then tell if the bitmap has been re-used and now needs to display new information. It also allows me to know when I need to create a new bitmap versus re-use an existing one.
Use these two functions to manage the IDs of your bitmaps set into your ImageViews:
ImageView.setTag()
ImageView.getTag()