内部存储器中充满了图片,可能是由于Bitmap.compress(format, int, stream)引起的。

4
我的应用程序是一个 WiFi 聊天应用,您可以通过文本消息和拍照图片在两个 Android 设备之间进行通信并发送它们。这些图片将被存储到 SD 卡中。
在发送几张图片后,我曾经遇到过一个 OutOfMemoryError 的问题,但我通过发送方式解决了这个问题。
options.inPurgeable = true;

并且

options.inInputShareable = true;

针对BitmapFactory.decodeByteArray方法,可以使像素“可释放”,以便新的图像可以使用内存。因此,错误不再存在。

但是,内部存储器仍然充满了图像,并出现“存储空间不足:手机存储空间即将用尽”的警告。应用程序不再崩溃,但是应用程序完成后手机上没有更多的内存。我必须在“设置”>“应用程序”>“管理应用程序”中手动清除应用程序的数据。

我尝试回收位图,甚至尝试显式地清空应用程序的缓存,但似乎并没有达到我的预期。

该函数通过TCP套接字接收图片,将其写入SD卡,并启动我的自定义活动PictureView:

public void receivePicture(String fileName) {
    try {
        int fileSize = inStream.readInt();
        Log.d("","fileSize:"+fileSize);
        byte[] tempArray = new byte[200];
        byte[] pictureByteArray = new byte[fileSize];

        path = Prefs.getPath(this) + "/" + fileName;
        File pictureFile = new File(path);
        try {
          if( !pictureFile.exists() ) {
                pictureFile.getParentFile().mkdirs();
                pictureFile.createNewFile();
            }
        } catch (IOException e) { Log.d("", "Recievepic - Kunde inte skapa fil.", e); }

        int lastRead = 0, totalRead = 0;
        while(lastRead != -1) {
            if(totalRead >= fileSize - 200) {
                lastRead = inStream.read(tempArray, 0, fileSize - totalRead);
                System.arraycopy(tempArray, 0, pictureByteArray, totalRead, lastRead);
                totalRead += lastRead;
                break;
            }
            lastRead = inStream.read(tempArray);
            System.arraycopy(tempArray, 0, pictureByteArray, totalRead, lastRead);
            totalRead += lastRead;
        }

        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(pictureFile));
        bos.write(pictureByteArray, 0, totalRead);
        bos.flush();
        bos.close();

        bos = null;
        tempArray = null;
        pictureByteArray = null;

        setSentence("<"+fileName+">", READER);
        Log.d("","path:"+path);
        try {
            startActivity(new Intent(this, PictureView.class).putExtra("path", path));
        } catch(Exception e) { e.printStackTrace(); }
    }
    catch(IOException e) { Log.d("","IOException:"+e); }
    catch(Exception e) { Log.d("","Exception:"+e); }
}

这里是PictureView。它从SD卡上的文件创建一个byte[],将数组解码为Bitmap,压缩Bitmap并将其写回SD卡。最后,在Progress.onDismiss中,将图片设置为全屏imageView的图像:
public class PictureView extends Activity {

private String fileName;
private ProgressDialog progress;

public ImageView view;

@Override
public void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    Log.d("","onCreate() PictureView");

    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
            WindowManager.LayoutParams.FLAG_FULLSCREEN);

    view = new ImageView(this);
    setContentView(view);

    progress = ProgressDialog.show(this, "", "Laddar bild...");
    progress.setOnDismissListener(new OnDismissListener() {
        public void onDismiss(DialogInterface dialog) {
            File file_ = getFileStreamPath(fileName);
            Log.d("","SETIMAGE");
            Uri uri = Uri.parse(file_.toString());
            view.setImageURI(uri);
        }
    });

    new Thread() { public void run() {
        String path = getIntent().getStringExtra("path");
        Log.d("","path:"+path);
        File pictureFile = new File(path);

        if(!pictureFile.exists())
            finish();

        fileName = path.substring(path.lastIndexOf('/') + 1);
        Log.d("","fileName:"+fileName);

        byte[] pictureArray = new byte[(int)pictureFile.length()];

        try {
            DataInputStream dis = new DataInputStream( new BufferedInputStream(
                    new FileInputStream(pictureFile)) );
            for(int i=0; i < pictureArray.length; i++)
                pictureArray[i] = dis.readByte();
        } catch(Exception e) { Log.d("",""+e); e.printStackTrace(); }

        /**
         * Passing these options to decodeByteArray makes the pixels deallocatable
         * if the memory runs out.
         */
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPurgeable = true;
        options.inInputShareable = true;

        Bitmap pictureBM =
                BitmapFactory.decodeByteArray(pictureArray, 0, pictureArray.length, options);

        OutputStream out = null;
        try {
            out = openFileOutput(fileName, MODE_PRIVATE);

                    /**
                      * COMPRESS !!!!!
                    **/
            pictureBM.compress(CompressFormat.PNG, 100, out);

            pictureBM = null;
            progress.dismiss(); }
        catch (IOException e) { Log.e("test", "Failed to write bitmap", e); }
        finally {
            if (out != null)
                try { out.close(); out = null; }
                catch (IOException e) { }
        } }
    }.start();
}

    @Override
    protected void onStop() {
            super.onStop();
            Log.d("","ONSTOP()");
            Drawable oldDrawable = view.getDrawable();
            if( oldDrawable != null) {
                    ((BitmapDrawable)oldDrawable).getBitmap().recycle();
                    oldDrawable = null;
                    Log.d("","recycle");
            }

            Editor editor = 
                    this.getSharedPreferences("clear_cache", Context.MODE_PRIVATE).edit();
            editor.clear();
            editor.commit();
    }
}

当用户按下返回键时,图片不应该再从应用程序中获取。只存储在SD卡上。
onStop()中,我回收了旧位图甚至尝试清空应用程序的数据。但仍会出现“内存不足”警告。如何确保不再需要图片时它们不会再分配内存?
编辑:问题似乎是压缩方法。如果将压缩后面的所有内容注释掉,问题仍然存在。如果删除压缩,则问题消失。压缩似乎会分配永远不会释放的内存,每个图像为2-3 MB。
2个回答

1

好的,问题已经解决了。问题在于,我将一个OutputStream传递给了compress,这是到应用程序内部存储器中的私有文件的流。那就是我后来设置为图像的东西。这个文件从未被分配。

我没有意识到我有两个文件:一个在SD卡上,另一个在内部存储器中,都有相同的名称。

现在,我只是将SD卡文件设置为ImageView的图像。我从未将文件读取到内部存储器作为byte[],因此从未将数组解码为位图,因此也从未将位图压缩到内部存储器中。

这是新的PictureView:

public class PictureView extends Activity {

    public ImageView view;
    private String path;

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        Log.d("","onCreate() PictureView");

        path = getIntent().getStringExtra("path");

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        view = new ImageView(this);
        setContentView(view);

        Uri uri = Uri.parse( new File(path).toString() );
        view.setImageURI(uri);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            Log.d("","Back key pressed");

            Drawable oldDrawable = view.getDrawable();
                if( oldDrawable != null) {
                    ((BitmapDrawable)oldDrawable).getBitmap().recycle();
                    oldDrawable = null;
                    Log.d("","recycle");
                }
                view = null;
        }
        return super.onKeyDown(keyCode, event);
    }
}

将外部文件作为ImageView的图像是不好的实践吗?我应该先将其加载到内存中吗?


你确定这样解决了吗?当你将imageView设置为URI时,仍会以“完整尺寸”加载到内存中。你试过采用抽样方法吗? - Snake

0

如果您希望当用户按下返回键时,特定的图像确实从内存中清除,您可以覆盖返回按钮并在那里进行图像清理调用。我在我的某些应用程序中这样做,似乎效果很好。也许可以像这样:

@Override

protected void onBackPressed() {
         super.onBackPressed();

         view.drawable = null;
         jumpBackToPreviousActivity();
       } 

我相信有一些视图方法可以清除其他缓存等。你可以回收位图,但这并不能保证它会立即被释放,只有在垃圾回收机制到达时才能释放...但我相信你可能已经知道了这个情况 :)

编辑:你也可以在 onPause 方法中做同样的事情。这个方法是有保证会被调用的。根据 Android 文档,其他两种方法可能永远不会被调用。 http://developer.android.com/reference/android/app/Activity.html


2
我知道,但是垃圾回收器根本不会清除它,即使应用程序停止运行。我不明白onBackPressed()会有什么区别。onStop()被调用并执行代码,但没有任何效果。至少看起来是这样的。 - JonatanEkstedt
onStop()方法可能不会被调用,而将设置放在backpressed方法中可以保证用户主动导致应用程序强制清理视图。有时依赖于onStop()和onDestroy()方法可能不可靠,因为先调用了onPause()方法,然后当特定的Android版本决定设备需要回收内存时才会调用其他两个方法。 - James andresakis
我曾经遇到过一个类似的问题,那是我制作的一个应用程序,允许用户在触摸屏上创建绘画。我尝试在onstop和ondestroy中清除绘画,但是直到我创建了一个清理方法,将所有视图和位图都设置为空值,并在用户离开绘画活动时调用它,我才能完全清除视图和画布。 - James andresakis
2
不,这没有任何区别。问题似乎出在“compress”方法上。如果在“compress”之后的所有内容都被注释掉,问题仍然存在。如果我删除“compress”,问题就消失了。“Compress”似乎分配了从未释放的内存。 - JonatanEkstedt
我在你的代码中没有看到你实际清理视图本身的任何地方。尝试在onstop或onpause中清除视图本身,而不仅仅是清除可绘制对象。 - James andresakis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接