我有一个ListView,每行上有两个图像按钮。当用户单击列表行时,它将启动一个新活动。我不得不建立我自己的标签,因为相机布局的问题。为结果启动的活动是一个映射。如果我点击我的按钮来启动图像预览(从SD卡加载图像),应用程序从活动返回到ListView活动到结果处理程序,重新启动我的新活动,这只不过是一个图像小部件。

ListView上的图像预览是用光标和ListAdapter完成的。这使得它非常简单,但我不确定我如何可以把一个调整大小的图像(即较小的位大小而不是像素作为图像按钮的src)。我只是调整了从手机摄像头拍下来的图片的大小。

问题是,当它试图返回并重新启动第二个活动时,我得到一个OutOfMemoryError。

是否有一种方法,我可以轻松地构建列表适配器行,在那里我可以调整大小的飞行(位)?

这将是可取的,因为我还需要对每行中的小部件/元素的属性做出一些更改,因为我无法选择一行与触摸屏,因为焦点问题。(我会用滚轮。)

我知道我可以做带外调整并保存我的图像,但这不是我真正想做的,但一些示例代码会很好。

当我在ListView上禁用图像时,它又可以正常工作了。

供你参考:我是这样做的:

String[] from = new String[] { DBHelper.KEY_BUSINESSNAME, DBHelper.KEY_ADDRESS,
    DBHelper.KEY_CITY, DBHelper.KEY_GPSLONG, DBHelper.KEY_GPSLAT,
    DBHelper.KEY_IMAGEFILENAME  + ""};
int[] to = new int[] { R.id.businessname, R.id.address, R.id.city, R.id.gpslong,
    R.id.gpslat, R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);

其中R.id.imagefilename是一个ButtonImage。

这是我的LogCat:

01-25 05:05:49.877: ERROR/dalvikvm-heap(3896): 6291456-byte external allocation too large for this process.
01-25 05:05:49.877: ERROR/(3896): VM wont let us allocate 6291456 bytes
01-25 05:05:49.877: ERROR/AndroidRuntime(3896): Uncaught handler: thread main exiting due to uncaught exception
01-25 05:05:49.917: ERROR/AndroidRuntime(3896): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:149)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:174)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.drawable.Drawable.createFromPath(Drawable.java:729)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.resolveUri(ImageView.java:484)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.setImageURI(ImageView.java:281)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.setViewImage(SimpleCursorAdapter.java:183)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.bindView(SimpleCursorAdapter.java:129)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.CursorAdapter.getView(CursorAdapter.java:150)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.obtainView(AbsListView.java:1057)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.makeAndAddView(ListView.java:1616)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.fillSpecific(ListView.java:1177)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.layoutChildren(ListView.java:1454)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.onLayout(AbsListView.java:937)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1108)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:922)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:999)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:920)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.performTraversals(ViewRoot.java:771)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1103)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Handler.dispatchMessage(Handler.java:88)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Looper.loop(Looper.java:123)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.app.ActivityThread.main(ActivityThread.java:3742)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invokeNative(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invoke(Method.java:515)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at dalvik.system.NativeStart.main(Native Method)
01-25 05:10:01.127: ERROR/AndroidRuntime(3943): ERROR: thread attach failed 

我也有一个新的错误时显示的图像:

22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed

当前回答

在将位图设置为imageview后,像这样回收它:

bitmap.recycle();
bitmap=null;

其他回答

几分钟前我遇到了这个问题。我通过更好地管理我的listview适配器来解决这个问题。我认为这是我使用的数百个50x50px图像的问题,结果是我在每次显示一行时都试图膨胀我的自定义视图。通过简单地测试看看行是否被膨胀,我消除了这个错误,我使用了数百个位图。这实际上是用于Spinner的,但基本适配器对ListView是一样的。这个简单的修复还极大地提高了适配器的性能。

@Override
public View getView(final int position, View convertView, final ViewGroup parent) {

    if(convertView == null){
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.spinner_row, null);
    }
...

我认为避免OutOfMemoryError的最好方法是面对它并理解它。

我做了一个应用程序,故意引起OutOfMemoryError,并监控内存使用情况。

在对这个App做了大量的实验之后,我得到了以下结论:

在Honey Comb之前,我先谈谈SDK版本。

位图存储在原生堆中,但它会自动被垃圾收集,调用recycle()是不必要的。 如果{VM堆大小}+{已分配的本机堆内存}>={设备的VM堆大小限制},并且您正在尝试创建位图,则会抛出OOM。 注意:计算的是虚拟机堆大小,而不是虚拟机已分配内存。 VM堆大小在增长后永远不会缩小,即使分配的VM内存减少了。 所以你必须保持VM内存的峰值尽可能低,以保持VM堆大小不会变得太大,以节省可用的内存位图。 手动调用system .gc()是没有意义的,系统会在尝试增加堆大小之前先调用它。 原生堆大小也永远不会缩小,但它不计入OOM,所以不用担心。

然后,让我们谈谈SDK从蜂巢开始。

位图存储在VM堆中,OOM不计算本机内存。 OOM的条件要简单得多:{虚拟机堆大小}>={设备的虚拟机堆大小限制}。 所以你有更多的可用内存来创建位图与相同的堆大小限制,OOM不太可能被抛出。

以下是我对垃圾收集和内存泄漏的一些观察。

你可以自己在App中看到它。如果一个Activity执行了一个AsyncTask,而这个AsyncTask在Activity被销毁后仍然在运行,那么这个Activity在AsyncTask完成之前不会被垃圾收集。

这是因为AsyncTask是一个匿名内部类的实例,它持有一个Activity的引用。

如果任务在后台线程的IO操作中被阻塞,调用AsyncTask.cancel(true)将不会停止执行。

回调也是匿名的内部类,所以如果项目中的静态实例持有它们并且不释放它们,内存就会泄漏。

如果你计划一个重复或延迟的任务,例如定时器,你不调用取消()和清除()在onPause(),内存将被泄漏。

最近我看到了很多关于OOM异常和缓存的问题。开发人员指南中有一篇关于这方面的非常好的文章,但有些人往往无法以合适的方式实现它。

因此,我编写了一个示例应用程序,演示了Android环境中的缓存。这个实现还没有得到一个OOM。

在答案的末尾可以找到源代码的链接。

要求:

Android API 2.1或更高版本(我根本无法在API 1.6中为应用程序获得可用内存-这是API 1.6中唯一不能工作的代码段) Android支持包

特点:

如果方向改变,使用单例保存缓存 将分配的应用程序内存的八分之一用于缓存(如果需要可以修改) 大的位图缩放(您可以定义您想要允许的最大像素) 控制在下载位图之前是否有可用的互联网连接 确保每行只实例化一个任务 如果你扔掉ListView,它就不会下载中间的位图

这不包括:

磁盘缓存。这应该很容易实现-只要指向一个不同的任务,从磁盘获取位图

示例代码:

正在下载的图像是来自Flickr的图像(75x75)。然而,不管你想处理什么图像url,如果它超过最大值,应用程序就会缩小它。在这个应用程序中,url只是一个字符串数组。

LruCache有一个处理位图的好方法。但是,在这个应用程序中,我将一个LruCache实例放在我创建的另一个缓存类中,以便使应用程序更加可行。

Cache.java的关键内容(loadBitmap()方法是最重要的):

public Cache(int size, int maxWidth, int maxHeight) {
    // Into the constructor you add the maximum pixels
    // that you want to allow in order to not scale images.
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;

    mBitmapCache = new LruCache<String, Bitmap>(size) {
        protected int sizeOf(String key, Bitmap b) {
            // Assuming that one pixel contains four bytes.
            return b.getHeight() * b.getWidth() * 4;
        }
    };

    mCurrentTasks = new ArrayList<String>();    
}

/**
 * Gets a bitmap from cache. 
 * If it is not in cache, this method will:
 * 
 * 1: check if the bitmap url is currently being processed in the
 * BitmapLoaderTask and cancel if it is already in a task (a control to see
 * if it's inside the currentTasks list).
 * 
 * 2: check if an internet connection is available and continue if so.
 * 
 * 3: download the bitmap, scale the bitmap if necessary and put it into
 * the memory cache.
 * 
 * 4: Remove the bitmap url from the currentTasks list.
 * 
 * 5: Notify the ListAdapter.
 * 
 * @param mainActivity - Reference to activity object, in order to
 * call notifyDataSetChanged() on the ListAdapter.
 * @param imageKey - The bitmap url (will be the key).
 * @param imageView - The ImageView that should get an
 * available bitmap or a placeholder image.
 * @param isScrolling - If set to true, we skip executing more tasks since
 * the user probably has flinged away the view.
 */
public void loadBitmap(MainActivity mainActivity, 
        String imageKey, ImageView imageView,
        boolean isScrolling) {
    final Bitmap bitmap = getBitmapFromCache(imageKey); 

    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.ic_launcher);
        if (!isScrolling && !mCurrentTasks.contains(imageKey) && 
                mainActivity.internetIsAvailable()) {
            BitmapLoaderTask task = new BitmapLoaderTask(imageKey,
                    mainActivity.getAdapter());
            task.execute();
        }
    } 
}

您不需要在Cache.java文件中编辑任何内容,除非您想实现磁盘缓存。

java的关键部分:

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (view.getId() == android.R.id.list) {
        // Set scrolling to true only if the user has flinged the       
        // ListView away, hence we skip downloading a series
        // of unnecessary bitmaps that the user probably
        // just want to skip anyways. If we scroll slowly it
        // will still download bitmaps - that means
        // that the application won't wait for the user
        // to lift its finger off the screen in order to
        // download.
        if (scrollState == SCROLL_STATE_FLING) {
            mIsScrolling = true;
        } else {
            mIsScrolling = false;
            mListAdapter.notifyDataSetChanged();
        }
    } 
}

// Inside ListAdapter...
@Override
public View getView(final int position, View convertView, ViewGroup parent) {           
    View row = convertView;
    final ViewHolder holder;

    if (row == null) {
        LayoutInflater inflater = getLayoutInflater();
        row = inflater.inflate(R.layout.main_listview_row, parent, false);  
        holder = new ViewHolder(row);
        row.setTag(holder);
    } else {
        holder = (ViewHolder) row.getTag();
    }   

    final Row rowObject = getItem(position);

    // Look at the loadBitmap() method description...
    holder.mTextView.setText(rowObject.mText);      
    mCache.loadBitmap(MainActivity.this,
            rowObject.mBitmapUrl, holder.mImageView,
            mIsScrolling);  

    return row;
}

getView()经常被调用。如果我们没有实现检查,以确保我们不会在每行启动无限数量的线程,那么在那里下载图像通常不是一个好主意。Cache.java检查rowObject。mBitmapUrl已经在一个任务,如果它是,它不会开始另一个任务。因此,我们很可能不会超过AsyncTask池的工作队列限制。

下载:

您可以从https://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip下载源代码。


最后一句话:

我已经测试了这个几个星期了,我还没有得到一个OOM异常。我已经在模拟器、我的Nexus One和Nexus s上测试了这个功能。我测试了包含高清图像的图像url。唯一的瓶颈是下载需要更多的时间。

只有一种可能的情况下,我可以想象OOM将会出现,那就是如果我们下载许多非常大的图像,在它们被缩放并放入缓存之前,将同时占用更多的内存并导致OOM。但无论如何,这都不是一个理想的情况,而且很可能不可能以更可行的方式解决。

在评论中报告错误!: -)

我花了一整天的时间来测试这些解决方案,唯一对我有用的是上面的获取图像和手动调用GC的方法,我知道这是不必要的,但当我把应用程序置于繁重的负载测试下时,这是唯一有效的方法。我的应用程序有一个缩略图列表在列表视图(让说活动a),当你点击其中一个图像,它带你到另一个活动(让说活动B),显示该项目的主图像。当我在两个活动之间来回切换时,我最终会得到OOM错误,应用程序将强制关闭。

当我走到列表视图一半的时候它就崩溃了。

现在,当我在活动B中实现以下内容时,我可以毫无问题地遍历整个列表视图,并继续下去……而且速度很快。

@Override
public void onDestroy()
{   
    Cleanup();
    super.onDestroy();
}

private void Cleanup()
{    
    bitmap.recycle();
    System.gc();
    Runtime.getRuntime().gc();  
}

我使用解码文件描述符,它为我工作:

 FileInputStream  fileInputStream = null;
        try {
            fileInputStream  = new FileInputStream(file);
             FileDescriptor fd = fileInputStream.getFD();
            Bitmap imageBitmap = decodeSampledBitmapFromDescriptor(fd , 612,
                    816);
            imageView.setImageBitmap(imageBitmap);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

从文件描述符解码采样位图的代码:

 /**
     * Decode and sample down a bitmap from a file input stream to the requested width and height.
     *
     * @param fileDescriptor The file descriptor to read from
     * @param reqWidth       The requested width of the resulting bitmap
     * @param reqHeight      The requested height of the resulting bitmap
     * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
     * that are equal to or greater than the requested width and height
     */
    public static Bitmap decodeSampledBitmapFromDescriptor(
            FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

    /**
     * Calculate an inSampleSize for use in a {@link android.graphics.BitmapFactory.Options} object when decoding
     * bitmaps using the decode* methods from {@link android.graphics.BitmapFactory}. This implementation calculates
     * the closest inSampleSize that will result in the final decoded bitmap having a width and
     * height equal to or larger than the requested width and height. This implementation does not
     * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
     * results in a larger bitmap which isn't as useful for caching purposes.
     *
     * @param options   An options object with out* params already populated (run through a decode*
     *                  method with inJustDecodeBounds==true
     * @param reqWidth  The requested width of the resulting bitmap
     * @param reqHeight The requested height of the resulting bitmap
     * @return The value to be used for inSampleSize
     */
    public static int calculateInSampleSize(BitmapFactory.Options options,
                                            int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            // Calculate ratios of height and width to requested height and width
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);

            // Choose the smallest ratio as inSampleSize value, this will guarantee a final image
            // with both dimensions larger than or equal to the requested height and width.
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;

            // This offers some additional logic in case the image has a strange
            // aspect ratio. For example, a panorama may have a much larger
            // width than height. In these cases the total pixels might still
            // end up being too large to fit comfortably in memory, so we should
            // be more aggressive with sample down the image (=larger inSampleSize).

            final float totalPixels = width * height;

            // Anything more than 2x the requested pixels we'll sample down further
            final float totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
                inSampleSize++;
            }
        }
        return inSampleSize;
    }