最近在对编写完后的FileManager做优化,发觉其中异步加载图片的那块代码还需要在重构一下。
首先我先说明一下,该FileManager中显示文件图标的控件为GridView,并且最大可视区域为20个图标,就是因为要同时显示20个才给我惹了大麻烦。
简单地说是由于测试部在对FileManager的稳定性进行非常暴力的测试发生的问题,他们极其迅速地多次上下来回滑动GridView,创建过多AsyncTask导致了CPU无法负荷而发生ANR。这个问题也是由于之前我对android线程的了解还不够深入所引发的。AsyncTask本质是属于线程池,多次new所消耗的资源远远超过了Thread,这就是为什么AsyncTask比较适合简短、少次的异步操作。下面是官方解释:
AsyncTask is designed to be a helper class around
and
and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent
pacakge such as
,
and
. 并且那是每次触发getView时如果cache中没有图片的bitmap话,就会new AsyncTask来执行获取缩略图的操作,这样的后果就是那段代码变成了垃圾。因此我就从网上寻找可行的优化方案,找到了一个比较靠谱的,这回监听了Scroll状态,new的子线程是少了,可是我仔细想了想,感觉他的代码还是可以进一步优化(七楼就是我提的两个改进方案)。我的终极优化方案就是应用启动时就专门创建一个子线程来根据ScrollListener的状态执行解析缩略图的操作。
终于黄天不服有心人,还真让我在MIUI的FileExplore源码和android的Contacts源码中找到了。
SyncThumbnailExtractor是异步提取缩略图的主类,最主要的就是继承于HandlerThread的ExtractorThread,通过mExtractorHandler = new Handler(getLooper(), this)创建唯一的线程,产成消息队列,这样它会无限循环地处理send进来的Message,执行提取缩略图的操作。好了,不废话,直接代码。
public class SyncThumbnailExtractor implements Callback{ private static final String LOADER_THREAD_NAME = "FileIconLoader"; /** * Type of message sent by the UI thread to itself to indicate that some * thumbnails need to be extracted. */ private static final int MESSAGE_REQUEST_EXTRACTING = 1; /** * Type of message sent by the loader thread to indicate that some thumbnails * have been extracted. */ private static final int MESSAGE_THUMBNAIL_EXTRACTED = 2; private boolean mPaused; private boolean mDecodingRequested = false; final Handler mMainHandler = new Handler(this); ExtractorThread mExtractorThread; private Context mContext ; private final ConcurrentHashMapmPendingRequests = new ConcurrentHashMap (); private final static ConcurrentHashMap mImageCache = new ConcurrentHashMap (); private static abstract class ImageHolder { public static final int NEEDED = 0; public static final int EXTRACTING = 1; public static final int EXTRACTED = 2; int state; public static ImageHolder create(String mime) { if(mime == null) return null; if(mime.contains(ThumbnailUtils.APK)){ return new DrawableHolder(); } else if(MediaFile.isImageByMimeType(mime) || MediaFile.isVideoByMimeType(mime)){ return new BitmapHolder(); } return null; }; public abstract boolean setImageView(ImageView v); public abstract boolean isNull(); public abstract void setImage(Object image); } private static class BitmapHolder extends ImageHolder { SoftReference bitmapRef; @Override public boolean setImageView(ImageView v) { if (bitmapRef.get() == null) return false; v.setImageBitmap(bitmapRef.get()); return true; } @Override public boolean isNull() { return bitmapRef == null; } @Override public void setImage(Object image) { bitmapRef = image == null ? null : new SoftReference ((Bitmap) image); } } private static class DrawableHolder extends ImageHolder { SoftReference drawableRef; @Override public boolean setImageView(ImageView v) { if (drawableRef.get() == null) return false; v.setImageDrawable(drawableRef.get()); return true; } @Override public boolean isNull() { return drawableRef == null; } @Override public void setImage(Object image) { drawableRef = image == null ? null : new SoftReference ((Drawable) image); } } private static class FileInfo{ public FileInfo(String path,String mime){ this.path = path; this.mime = mime; } public String path; public String mime; } public SyncThumbnailExtractor(Context context) { mContext = context; } public void clear(){ mPaused = false; mImageCache.clear(); mPendingRequests.clear(); } //当前Activity调用OnDestory时,将ExtractorThread退出,并清空缓存 public void stop(){ pause(); if (mExtractorThread != null) { mExtractorThread.quit(); mExtractorThread = null; } clear(); } public void resume(){ mPaused = false; if (!mPendingRequests.isEmpty()) { requestExtracting(); } } public void pause(){ mPaused = true; } /** * Load thumbnail into the supplied image view. If the thumbnail is already cached, * it is displayed immediately. Otherwise a request is sent to load the * thumbnail from the database. * * @param id, database id */ public boolean decodeThumbnail(ImageView view, String path,String mime) { boolean extracted = loadCache(view, path, mime); if (extracted) { mPendingRequests.remove(view); } else { mPendingRequests.put(view, new FileInfo(path,mime)); if (!mPaused) { // Send a request to start loading thumbnails requestExtracting(); } } return extracted; } //set default icon by MimeType for unextracted mefile private void setImageByMimeType(ImageView image,String mime){ if( mime.contains(ThumbnailUtils.APK)){ image.setImageResource(R.drawable.apk); } else if (mime.contains(ThumbnailUtils.VIDEO)) { image.setImageResource(R.drawable.video); } else if (mime.contains(ThumbnailUtils.IMAGE)) { image.setImageResource(R.drawable.image); } } /** * Checks if the thumbnail is present in cache. If so, sets the thumbnail on the * view, otherwise sets the state of the thumbnail to * { BitmapHolder#NEEDED} */ private boolean loadCache(ImageView view, String path, String mime) { ImageHolder holder = mImageCache.get(path); if (holder == null) { holder = ImageHolder.create(mime); if (holder == null) return false; mImageCache.put(path, holder); } else if (holder.state == ImageHolder.EXTRACTED) { if (holder.isNull()) { setImageByMimeType(view, mime); return true; } // failing to set imageview means that the soft reference was // released by the GC, we need to reload the thumbnail. if (holder.setImageView(view)) { return true; } holder.setImage(null); } setImageByMimeType(view, mime); holder.state = ImageHolder.NEEDED; return false; } /** * Sends a message to this thread itself to start loading images. If the * current view contains multiple image views, all of those image views will * get a chance to request their respective thumbnails before any of those * requests are executed. This allows us to load images in bulk. */ private void requestExtracting() { if (!mDecodingRequested) { mDecodingRequested = true; mMainHandler.sendEmptyMessage(MESSAGE_REQUEST_EXTRACTING); } } /** * @Description: handle MESSAGE_REQUEST_EXTRACTING message to create ExtractorThread and start * to extract thumbnail in mPendingRequests's file * @param msg * */ @Override public boolean handleMessage(Message msg) { switch(msg.what){ case MESSAGE_REQUEST_EXTRACTING: mDecodingRequested = false; if (mExtractorThread == null) { mExtractorThread = new ExtractorThread(); mExtractorThread.start(); } mExtractorThread.requestLoading(); return true; case MESSAGE_THUMBNAIL_EXTRACTED: if (!mPaused) { processExtractThumbnails(); } return true; } return false; } /** * Goes over pending loading requests and displays extracted thumbnails. If some of * the thumbnails still haven't been extracted, sends another request for image * loading. */ private void processExtractThumbnails() { Iterator iterator = mPendingRequests.keySet().iterator(); while (iterator.hasNext()) { ImageView view = iterator.next(); FileInfo info = mPendingRequests.get(view); boolean extracted = loadCache(view, info.path, info.mime); if (extracted) { iterator.remove(); } } if (!mPendingRequests.isEmpty()) { requestExtracting(); } } private class ExtractorThread extends HandlerThread implements Callback{ private Handler mExtractorHandler; /** * @Description: * @param name */ public ExtractorThread() { super(LOADER_THREAD_NAME); } /** * Sends a message to this thread to extract requested thumbnails. */ public void requestLoading() { if (mExtractorHandler == null) { mExtractorHandler = new Handler(getLooper(), this); } mExtractorHandler.sendEmptyMessage(0); } /** * @Description: extract thumbnail * @param msg * */ @Override public boolean handleMessage(Message msg) { Iterator iterator = mPendingRequests.values().iterator(); while (iterator.hasNext()) { FileInfo info = iterator.next(); ImageHolder holder = mImageCache.get(info.path); if (holder != null && holder.state == ImageHolder.NEEDED) { // Assuming atomic behavior holder.state = ImageHolder.EXTRACTING; if(info.mime == null){ holder.setImage(FileUtil.sInvalidBmp); } else { if(info.mime.contains(ThumbnailUtils.APK)){ Drawable icon = ThumbnailUtils.getApkIcon(mContext, info.path); holder.setImage(icon); } else if(MediaFile.isVideoByMimeType(info.mime)){ holder.setImage(ThumbnailUtils.getVideoThumb(info.path)); } else if(MediaFile.isImageByMimeType(info.mime)){ holder.setImage(ThumbnailUtils.getScaleImageThumb(mContext, info.path)); } } holder.state = BitmapHolder.EXTRACTED; mImageCache.put(info.path, holder); } } mMainHandler.sendEmptyMessage(MESSAGE_THUMBNAIL_EXTRACTED); return true; } } }
然后在Adapter的构造函数中创建该类:
public FileGridQueneAdapter(Context context, GridView gridView) { mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mContext = context; syncThumbExtractor = new SyncThumbnailExtractor(context); mGridView = gridView; mGridView.setOnScrollListener(this);}
getView中为需要缩略图的文件调用decodeThumbnail方法:
String mime = FileUtil.getMime(path);if(ThumbnailUtils.isNeedSyncDecodeByMime(mime)){ syncThumbExtractor.decodeThumbnail(icon, path, mime);} else { icon.setImageBitmap(ThumbnailUtils.getThumbnail(mContext, path));}
在Adapter中添加SyncThumbnailExtractor四个操作,供Activity以及ScrollListener使用:
public void clear(){ syncThumbExtractor.clear(); } public void pause(){ syncThumbExtractor.pause(); } public void stop(){ syncThumbExtractor.stop(); } public void resume(){ syncThumbExtractor.resume(); }
最后给GridView或者ListView添加ScrollListener:
@Override public void onScrollStateChanged(AbsListView view, int scrollState) { if(scrollState == OnScrollListener.SCROLL_STATE_FLING){ pause(); } else { resume(); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { }