image download: memory leak using LazyList
I've got memory leak using LazyList. I use one instance of ImageLoader in whole app, I create it in Application.onCreate(), because I need image downloading in several activities: list activity, one activity with gallery widget and full-screen gallery activitiy(all of them use same cache) I modified image loader so it uses SoftReference-based HashMap. Here's code for SoftHashMap:
public class SoftHashMap extends AbstractMap {
private final Map hash=new HashMap();
private final int HARD_SIZE;
private final LinkedList hardCache=new LinkedList();
private final ReferenceQueue queue=new ReferenceQueue();
public SoftHashMap(){
this(100);
}
public SoftHashMap(int hardSize){
HARD_SIZE=hardSize;
}
public Object get(Object key){
Object result=null;
SoftReference soft_ref=(SoftReference)hash.get(key);
if(soft_ref!=null){
result=soft_ref.get();
if(result==null){
hash.remove(key);
}else{
hardCache.addFirst(result);
if(hardCache.size()>HARD_SIZE){
hardCache.removeLast();
}
}
}
return result;
}
private static class SoftValue extends SoftReference{
private final Object key;
public SoftValue(Object k, Object key, ReferenceQueue q) {
super(k, q);
this.key=key;
}
}
private void processQueue(){
SoftValue sv;
while((sv=(SoftValue)queue.poll())!=null){
hash.remove(sv.key);
}
}
public Object put(Object key, Object value){
processQueue();
return hash.put(key, new SoftValue(value, key, queue));
}
public void clear(){
hardCache.clear();
processQueue();
hash.clear();
}
public int size(){
processQueue();
return hash.size();
}
public Set entrySet() {
throw new UnsupportedOperationException();
}
}
ImageLoader class:
public class ImageLoader {
private SoftHashMap cache=new SoftHashMap(15);
private File cacheDir;
final int stub_id=R.drawable.stub;
private int mWidth, mHeight;
public ImageLoader(Context context, int h, int w){
mWidth=w;
mHeight=h;
photoLoaderThread.setPriority(Thread.NORM_PRIORITY);
if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"CacheDir");
else
cacheDir=context.getCacheDir();
if(!cacheDir.exists())
cacheDir.mkdirs();
}
public void DisplayImage(String url, Activity activity, ImageView imageView)
{
Log.d("IMAGE LOADER", "getNativeHeapSize()-"+String.valueOf(Debug.getNativeHeapSize()/1024)+" kb");
Log.d("IMAGE LOADER", "getNativeHeapAllocatedSize()-"+String.valueOf(Debug.getNativeHeapAllocatedSize()/1024)+" kb");
Log.d("IMAGE LOADER", "getNativeHeapFreeSize()-"+String.valueOf(Debug.getNativeHeapFreeSize()/1024)+" kb");
if(cache.get(url)!=null){
imageView.setImageBitmap((Bitmap)cache.get(url));
}
else
{
queuePhoto(url, activity, imageView);
imageView.setImageResource(stub_id);
}
}
private void queuePhoto(String url, Activity activity, ImageView imageView)
{
//This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them.
photosQueue.Clean(imageView);
PhotoToLoad p=new PhotoToLoad(url, imageView);
synchronized(photosQueue.photosToLoad){
photosQueue.photosToLoad.push(p);
photosQueue.photosToLoad.notifyAll();
}
//start thread if it's not started yet
if(photoLoaderThread.getState()==Thread.State.NEW)
photoLoaderThread.start();
}
private Bitmap getBitmap(String url)
{
//I identify images by hashcode. Not a perfect solution, good for the demo.
String filename=String.valueOf(url.hashCode());
File f=new File(cacheDir, filename);
//from SD cache
Bitmap b = decodeFile(f);
if(b!=null)
return b;
//from web
try {
Bitmap bitmap=null;
InputStream is=new URL(url).openStream();
OutputStream os = new FileOutputStream(f);
Utils.CopyStream(is, os);
os.close();
bitmap = decodeFile(f);
return bitmap;
} catch (Exception ex){
ex.printStackTrace();
return null;
}
}
//decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f){
Bitmap b=null;
try {
//decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
FileInputStream fis=new FileInputStream(f);
BitmapFactory.decodeStream(fis,null,o);
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//Find the correct scale value. It should be the power of 2.
//final int REQUIRED_SIZE=mWidth;
int width_tmp=o.outWidth, height_tmp=o.outHeight;
int scale=1;
while(true){
if(width_tmp/2<=mWidth || height_tmp/2<=mHeight)
break;
width_tmp/=2;
height_tmp/=2;
scale*=2;
}
//decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize=scale;
//o2.inPurgeable=true;
fis=new FileInputStream(f);
b=Bitmap开发者_如何学编程Factory.decodeStream(fis, null, o2);
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return b;
} catch (FileNotFoundException e) {}
return null;
}
class PhotoToLoad{
public String url;
public ImageView imageView;
public PhotoToLoad(String u, ImageView i){
url=u;
imageView=i;
}
}
PhotosQueue photosQueue=new PhotosQueue();
public void stopThread()
{
photoLoaderThread.interrupt();
}
class PhotosQueue{
private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>();
public void Clean(ImageView image)
{
for(int j=0 ;j<photosToLoad.size();){
if(photosToLoad.get(j).imageView==image)
photosToLoad.remove(j);
else
++j;
}
}
}
class PhotosLoader extends Thread{
public void run(){
try {
while(true)
{
//thread waits until there are any images to load in the queue
if(photosQueue.photosToLoad.size()==0)
synchronized(photosQueue.photosToLoad){
photosQueue.photosToLoad.wait();
}
if(photosQueue.photosToLoad.size()!=0)
{
PhotoToLoad photoToLoad;
synchronized(photosQueue.photosToLoad){
photoToLoad=photosQueue.photosToLoad.pop();
}
Bitmap bmp=getBitmap(photoToLoad.url);
cache.put(photoToLoad.url, bmp);
Object tag=photoToLoad.imageView.getTag();
if(tag!=null && ((String)tag).equals(photoToLoad.url)){
BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
Activity a=(Activity)photoToLoad.imageView.getContext();
a.runOnUiThread(bd);
}
}
if(Thread.interrupted())
break;
}
} catch (InterruptedException e) {
//allow thread to exit
}
}
}
PhotosLoader photoLoaderThread=new PhotosLoader();
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
ImageView imageView;
public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
public void run()
{
if(bitmap!=null)
imageView.setImageBitmap(bitmap);
else
imageView.setImageResource(stub_id);
}
}
public void clearCache() {
//clear memory cache
cache.clear();
//clear SD cache
File[] files=cacheDir.listFiles();
for(File f:files)
f.delete();
}
}
And my Application class, not the best way to do it, though:
public class MyApplication extends Application {
ImageLoader mImageLoader;
@Override
public void onCreate(){
int h =((WindowManager)getApplicationContext().getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getHeight();
int w =((WindowManager)getApplicationContext().getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getWidth();
mImageLoader=new ImageLoader(getApplicationContext(), h, w);
super.onCreate();
public ImageLoader getImageLoader(){
return mImageLoader;
}
@Override
public void onLowMemory(){
mImageLoader.clearCache();
Log.d("MY APP", "ON LOW MEMORY");
super.onLowMemory();
}
}
And the worst part: after some time I receive OOM exception when ImageLoader tries to decode another bitmap. I'll appreciate any your help. Thanks.
EDIT I've got rid of hard cache, but i still get this OOM exception. It seems to me that I'm doing smth funny. I don't even know what extra information should I provide... The images which i download from server are pretty big, though. And app fails to allocate appr. 1.5 mb, that's what I see in LogCat. But I just can't figure out why doesn't vm clear my SoftHashMap if there is need for memory...
onLowMemory
will be of no help to you as it is not generated when your app is running out of memory, it is called when the Android system would like memory for a different application or itself before it kills off processes.- I don't see the need for hard cache - this is preventing drawables from being recycled. Just leave the drawables in the soft cache - the GC won't collect the memory while the drawables have references that are not soft so you don't need to worry about a drawable currently set in an ImageView being recycled.
Also how many images are you displaying on screen at once? How large are they?
Here's an amazing article on analyzing memory leaks. It can definitely help you. http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html.
Are you absolutely sure your SoftHashMap implementation works fine? Looks rather complicated. You can use debugger to ensure SoftHashMap never holds more than 15 bitmaps. MAT can also help you to identify how many bitmaps are there in memory.
You can also comment cache.put(photoToLoad.url, bmp) call. This way you'll disable in-memory caching to identify if it's a cause of a problem or not.Yes it may be an Activity leak. You can identify that. If you just scroll around in the same activity and get OOM it means that something else is leaking not activity. If you stop/start activity several times and get OOM it means activity is leaking. Also you can definitely say is activity leaking or not if you take a look at MAT histogram.
As you use inSampleSize the image size doesn't matter. It should work fine even with 5mpx images.
You could try to replace your SoftHashMap implementation with just HashMap<String, SoftReference<Bitmap>>. Read about SoftReference. It is good for very simple implementation of in-memory cache. It holds objects in memory if there's enough memory. If there's too little memory SoftReference releases objects.
I can also recommend you to use LinkedHashMap for in-memory cache. It has a special constuctor to iterate items in the order in which its entries were last accessed. So when you have more than 15 items in cache you can remove least-recently accessed items. As documentation says:
This kind of map is well-suited to building LRU caches.
You know my implementation was designed with small images in mind, something like 50*50. If you have larger images you should think how much memory they consume. If they take too much you could just cache them to SD card but not to memory. The performance may be slower but OOM would not be a problem any more.
Not related to OOM. I can see you call clearCache() in onLowMemory(). Not good because clearCache() also removes cache from SD. You should only clear in-memory cache not SD cache.
I seen that below line is commented in your code, First of All uncomment that line plz.
//o2.inPurgeable=true;
In the NonPurgeable case, an encoded bitstream is decoded to a different Bitmap over and over again until out-of-memory occurs. In Purgeable case, The memory allocated by one image will be shared by any new image whenever required and later any time if the older image reference is activiated in that case the OS will manage the memory reference it self by using a space of another image and vice verce and this way it always avoid the out of memory error.
If even you have purgeable case and still facing that memory leak then now use below try catch blog to trace the error and let me know the Stack Trace Detail.
try{
//your code to download or decode image
}catch(Error e){
//Print the stack trace
}
It looks like you're creating bitmaps, but never calling Bitmap.recycle().
EDIT: To elaborate, Bitmap.recycle() frees memory currently being used to store the Bitmap. Since Bitmaps created by the BitmapFactory are immutable, (you can check this with Bitmap.isMutable() on your freshly created bitmaps), simply removing a reference to them from the hashtable and waiting on the garbage collector isn't enough to free up the memory.
Call Bitmap Recycle whenever you're done with a specific bitmap (such as in the "clean" method of your photoQueue, or clearCache()).
精彩评论