In my app there is RecyclerView
with tons of images in it.Images are loaded as user scrolls RecyclerView with this code:
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,url); else loader.execute(url);
Unfortunately sometimes when user scrolls fast this error occurs:
Task android.os.AsyncTask$3@73f1d84 rejected from java.util.concurrent.ThreadPoolExecutor@8f5f96d[Running, pool size = 9, active threads = 9, queued tasks = 128, completed tasks = 279]
Is there way to detect if poolExecutor is full and skip image loading?
Whole Image class:
public class Image extends ImageView { private AsyncTask<String,Integer,Bitmap> loader; public Image(Context context) { super(context); this.setScaleType(ScaleType.FIT_XY); } public Image(Context context, AttributeSet attrs) { super(context, attrs); this.setScaleType(ScaleType.FIT_XY); } public void loadURL(String url) { if(loader!=null) loader.cancel(true); loader=new AsyncTask<String, Integer, Bitmap>() { @Override protected Bitmap doInBackground(String... params) { URL url = null; byte[] bytes = null; HttpURLConnection connection=null; try { url = new URL(params[0]); connection=(HttpURLConnection) url.openConnection(); connection.setRequestProperty("Connection", "close"); connection.setRequestMethod("GET"); connection.setUseCaches(true); InputStream is = null; is=connection.getInputStream(); bytes = IOUtils.toByteArray(is); } catch (MalformedURLException e) { e.printStackTrace(); } catch (ProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } if (connection!=null) connection.disconnect(); Bitmap res=null; if(!isCancelled() && bytes!=null) res=BitmapFactory.decodeByteArray(bytes,0,bytes.length); return res; } @Override protected void onPostExecute(Bitmap res) { if(res!=null) { setImageBitmap(res); _animate(); } } }; if (this.getDrawable()!=null) { Bitmap bmp=((BitmapDrawable) this.getDrawable()).getBitmap(); this.setAnimation(null); if (bmp!=null) { bmp.recycle(); //Log.d("image","recycled"); } this.setImageBitmap(null); } /* ThreadPoolExecutor e =(ThreadPoolExecutor) Executors.newFixedThreadPool(9); Log.d("pool size",e.getActiveCount()+"/"+e.getMaximumPoolSize()); if (e.getActiveCount() == e.getMaximumPoolSize()) { } */ //start loading if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url); else loader.execute(url); } private void _animate() { ValueAnimator bgAnim= ValueAnimator.ofObject(new IntEvaluator(),0,255); bgAnim.setDuration(500); bgAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Image.this.getDrawable().setAlpha((int) (animation.getAnimatedValue())); } }); bgAnim.start(); }
}
7 Answers
Answers 1
I answer that before (here, here, here and here and probably others) and I answer it again for you: Do not try to re-invent the wheel!
Image loading/caching is a very complex task in Android and a lot of good very developers already did that. Threading is just one of the issues, but I can see from your code you have a memory leak, there's no caching so you'll re-download images again if scroll back to it, HttpURLConnection
is a crappy network layer.
So, the way to solve this (IMHO) it's just to re-use work done by other developers. Good examples of libraries you should consider for that are:
- Picasso - https://github.com/square/picasso
- Glide - https://github.com/bumptech/glide
- Fresco - https://github.com/facebook/fresco
Picasso is my favorite, so to use it you need to simply call:
Picasso.with(context).load(url).into(imgView);
and it all be handled for you.
Answers 2
You can check if active threads count is equal to thread pool maximum size then your thread pool is full by using this
ThreadPoolExecutor e =(ThreadPoolExecutor)Executors.newFixedThreadPool(totalnofthreads); if (e.getActiveCount() == e.getMaximumPoolSize()) { }
Answers 3
To detect if user scrolls fast you can use onFlingListener()
recyclerView.setOnFlingListener(new RecyclerView.OnFlingListener() { @Override public boolean onFling(int velocityX, int velocityY) { isFlinging = checkFlinging(velocityY); if (isFlinging) { //Stop image loading here } return false; } }); private boolean checkFlinging(int velocityY) { return (velocityY < 0 && velocityY < -RECYCLER_VIEW_FLING_VELOCITY) || (velocityY > 0 && velocityY > RECYCLER_VIEW_FLING_VELOCITY); }
Let me explain a bit, velocityY
because I use recyclerView with vertical scrolling (for horizontal scrolling just change this parameter to velocityX
), RECYCLER_VIEW_FLING_VELOCITY - your fling velocity, for me RECYCLER_VIEW_FLING_VELOCITY = 7000
.
Answers 4
I just relized I can wrap loading code with try/catch:
try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url); else loader.execute(url); } catch (RejectedExecutionException e){ e.printStackTrace(); }
Looks like this would be optional solution.
Answers 5
Instead of using AsyncTask.THREAD_POOL_EXECUTOR
you can use your own Executor instance with a RejectedExecutionHandler
, for example:
private final Executor mExecutor = new ThreadPoolExecutor(0, 8, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(16), new ThreadPoolExecutor.DiscardPolicy());
This will create an Executor that runs at most 8 threads, keeps 16 tasks in the queue and drops any tasks above those limits (DiscardPolicy
is a predefined RejectedExecutionHandler
that does exactly that). You can then pass it to executeOnExecutor()
.
Alternatively, you might want the images to eventually load. In that case you can use an Executor with an unbounded (i.e. limitless) queue. It will never throw a RejectedExecutionException. Executors utility class has some nice factory method for creating those:
private final Executor mExecutor = Executors.newFixedThreadPool(8);
Answers 6
when ImageView detached from window , cancel the corresponding task of loading url :
public class Image extends ImageView { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if(loader!=null) loader.cancel(true); } }
Answers 7
First of all, why are you still using AsyncTask? the ThreadPool exception is coming because while your scrolling fast the adapter is trying to set an image to a position which is no longer available usually to stop this issue you would disable recycling but that would only make your list slow when handling a large set of data. So i advise you use volley for image loading its easy to implement and handles caching easiily.
<com.android.volley.toolbox.NetworkImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/mainImage" android:scaleType="centerCrop" android:adjustViewBounds="true" android:maxHeight="270dp" />
Use above in place of your imageview and create the volleySingleton class to handle all network requests
public class VolleySingleton { private static VolleySingleton sInstance = null; private RequestQueue mRequestQueue; private ImageLoader imageLoader; private VolleySingleton(){ mRequestQueue = Volley.newRequestQueue(Application.getAppContext()); imageLoader = new ImageLoader(mRequestQueue, new ImageLoader.ImageCache() { private final LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(200); @Override public Bitmap getBitmap(String url) { return cache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { cache.put(url, bitmap); } }); } public static VolleySingleton getsInstance(){ if(sInstance == null){ sInstance = new VolleySingleton(); } return sInstance; } public RequestQueue getmRequestQueue(){ return mRequestQueue; } public ImageLoader getImageLoader() { return imageLoader; } }
Get an instance of your singleton class then add it to the imageView and your good to go
imageLoader = VolleySingleton.getsInstance().getImageLoader(); networkImageVeiw.setImageUrl(imageUrl, imageLoader);
Create a class extending android.app.Application so you can get the context in the volleySingleton class
public class Application extends android.app.Application { private static Application sInstance; public static Application getsInstance() { return sInstance; } public static Context getAppContext() { return sInstance.getApplicationContext(); } public static AppEventsLogger logger(){ return logger; } @Override public void onCreate() { super.onCreate(); sInstance = this; } }
Don't forget to go to your manifest.xml and add the name attribute to the application tag to be the name of your application class you just extended
<application android:name="com.example.ApplicationClass"
0 comments:
Post a Comment