如何获取位图信息并从Internet-InputStream解码位图?

9

背景

假设我有一个从互联网上获取的图像文件的inputStream。

我希望在解码之前获取有关图像文件的信息。

这对于多种用途非常有用,例如缩小文件大小和在显示图像之前预览信息。

问题

我尝试使用BufferedInputStream包装inputStream来标记&重置inputStream,但它没有起作用:

inputStream=new BufferedInputStream(inputStream);
inputStream.mark(Integer.MAX_VALUE);
final BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeStream(inputStream,null,options);
//this works fine. i get the options filled just right.

inputStream.reset();
final Bitmap bitmap=BitmapFactory.decodeStream(inputStream,null,options);
//this returns null

为了从URL中获取输入流,我使用以下代码:
public static InputStream getInputStreamFromInternet(final String urlString)
  {
  try
    {
    final URL url=new URL(urlString);
    final HttpURLConnection urlConnection=(HttpURLConnection)url.openConnection();
    final InputStream in=urlConnection.getInputStream();
    return in;
    }
  catch(final Exception e)
    {
    e.printStackTrace();
    }
  return null;
  }

问题

如何让代码处理标记和重置?

使用资源时它可以完美地工作(实际上,我甚至不需要为此创建一个新的BufferedInputStream),但是在来自互联网的inputStream中却不能正常工作...


编辑:

看起来我的代码很好,但有点...

在一些网站上(比如这个这个),即使重置后仍无法解码图像文件。

如果你解码位图(并使用inSampleSize),它可以正常解码(只是需要很长时间)。

现在的问题是为什么会发生这种情况,以及如何修复它。


嘿,@android开发者,我遇到了同样的错误...你是如何解决的?请分享你的代码...提前致谢。 - buzzingsilently
@DhirenParmar 不,我没有修复它。 - android developer
我在这里提出了一个新的建议:https://code.google.com/p/android/issues/detail?id=231550 - android developer
4个回答

0

(这里有一个与相同问题相关的解决方案,但是当从磁盘读取时。我一开始没有意识到你的问题特别来自于网络流。)

通常标记和重置存在一个问题,即 BitmapFactory.decodeStream() 有时会 重置您的标记。因此,重置以进行实际读取是错误的。

但是,BufferedInputStream 还有第二个问题:它可能会导致整个图像在内存中缓冲,而不仅仅是实际读入的位置。根据您的用例,这可能会严重影响性能。(大量分配意味着大量垃圾回收

这里有一个非常好的解决方案: https://dev59.com/z3NA5IYBdhLWcg3wF5uO#18665678

我稍微修改了它以解决标记和重置问题:

public class MarkableFileInputStream extends FilterInputStream
{
    private static final String TAG = MarkableFileInputStream.class.getSimpleName();

    private FileChannel m_fileChannel;
    private long m_mark = -1;

    public MarkableFileInputStream( FileInputStream fis )
    {
        super( fis );
        m_fileChannel = fis.getChannel();
    }

    @Override
    public boolean markSupported()
    {
        return true;
    }

    @Override
    public synchronized void mark( int readlimit )
    {
        try
        {
            m_mark = m_fileChannel.position();
        }
        catch( IOException ex )
        {
            Log.d( TAG, "Mark failed" );
            m_mark = -1;
        }
    }

    @Override
    public synchronized void reset() throws IOException
    {
        // Reset to beginning if mark has not been called or was reset
        // This is a little bit of custom functionality to solve problems
        // specific to Android's Bitmap decoding, and is slightly non-standard behavior
        if( m_mark == -1 )
        {
            m_fileChannel.position( 0 );
        }
        else
        {
            m_fileChannel.position( m_mark );
            m_mark = -1;
        }
    }
}

在读取过程中,这不会分配任何额外的内存,并且即使标记已被清除,也可以重置。


我不熟悉“FilterInputStream”,但是它需要使用“FileInputStream”而不是简单的InputStream,这意味着我需要提供一个存在于设备上的文件,对吗?如果不是,你有检查过它是否有效吗? - android developer
在我的使用案例中,我将网络流保存到磁盘缓存中。然后我的实际图像加载器始终使用FileInputStream从缓存中读取。FilterInputStream只是一个包装器,允许您在读取时转换InputStream。FilterInputStream可以使用任何InputStream,但是MarkableFileInputStream基于它是FileInputStream来使用FileChannel进行标记和重置功能。 - Adam
如果你想要缓存图片,我建议使用DiskLruCache。https://github.com/JakeWharton/DiskLruCache/ 它可以帮助减少冗余的网络流量和加载时间,一旦你第一次获取到数据。 - Adam
问题是如何避免使用磁盘并使用inputStream完成所有操作,因此这不是很好的答案(尽管它可能是正确的)。最终,我决定使用磁盘,因为这样更容易,而且可能更好。你所做的和简单地使用BufferedInputStream有什么区别? - android developer
我把它放在答案里了,但是BufferedInputStream可能会导致最坏情况下整个数据的副本在你读取时驻留在内存中。这就是BufferedInputStream提供其功能的方式,通过将数据抛入中间缓冲区。 - Adam
我明白了,所以这比使用BufferedInputStream更好。但是如果你已经有了文件本身,你可以调用BitmapFactory.decode两次:第一次设置inJustDecodeBounds=true(获取图像信息),第二次设置为false,进行真正的降采样。 - android developer

0
我认为问题在于对大值的mark()调用被mark(1024)的调用覆盖了。正如文档中所述:
在KITKAT之前,如果is.markSupported()返回true,则会调用is.mark(1024)。从KITKAT开始,不再是这种情况。
如果读取的内容超过此值,则可能导致reset()失败。

我没有在KitKat上测试过,但我现在不明白文档在说什么。如果没有调用is.mark(1024),那么是什么?无论如何,我报告的问题仍然不能像我写的那样处理。现在的问题是:你如何正确地做到这一点? - android developer
这是 decodeStream 的文档说明。它表示它在内部调用 is.mark(1024),覆盖了您的调用。正确的方法是:重新实现 decodeStream,而不调用 is.mark(1024)。否则,您需要两次生成输入流。 - hdante
我该怎么做?简单地从源代码复制并不足够,因为其中很多是内部和私有的,我认为在这种情况下我还需要本地代码(C/C++)... - android developer
我不确定。我注意到代码中存在过度封装,这使得它难以或不可能重用(这是Android库和Java项目中的共同主题)。如果我选择这条路线,我将完全复制并粘贴整个BitmapFactory.java文件,删除有问题的部分。虽然我没有尝试这种方法,但我选择了两次生成输入流的方式。 - hdante
我认为最好将文件下载到缓存中,然后从缓存中读取,而不是两次连接服务器。关于封装,你是正确的。我不知道他们为什么要这样“安全”地封装它。这只是位图... - android developer

-1

能否标记/重置流取决于流的实现。这些是可选操作,通常不受支持。您的选择是将流读入缓冲区,然后从该流读取2次,或者只需使网络连接2次。

最简单的方法可能是写入ByteArrayOutputStream中,

ByteArrayOutputStream baos = new ByteArrayOutputStream();
int count;
byte[] b = new byte[...];
while ((count = input.read(b) != -1) [
  baos.write(b, 0, count);
}

现在可以直接使用baos.toByteArray()的结果,或者创建一个ByteArrayInputStream并重复使用它,在每次使用后调用reset()

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

这可能听起来很傻,但其实并没有什么魔法。你要么在内存中缓冲数据,要么从源头读取2次。如果流支持标记/重置,它在实现中也必须做同样的事情。


这取决于用于构造“BufferedInputStream”的流的实现。它是否支持“reset()”? - Jeffrey Blattman
应该是这样的吧?不过,假设我使用你的方法,那么这是否意味着我要下载整个文件的字节? - android developer
不,它不应该。你必须理解的是,流是一个流,而不是缓冲区。它在读取数据后并不会本质上缓冲或缓存数据。这就是流的全部意义。是的,这意味着你必须下载并将文件的所有字节存储在内存中,或者你必须从源头读取两次。这些是你的选择。我很好奇你认为数据会来自哪里。它要么在内存中,要么再次从源头读取。这里没有魔法。 - Jeffrey Blattman
但是我如何回去再次阅读它们(而不是重新下载它们)以及剩余的字节? - android developer
如果你想读取1k字节并对它们进行操作,在读取循环中放置一个计数器,当读取到1k字节时停止并执行任何你想要的操作。然后,如果你决定要继续读取流,请继续读取。或者不要。话虽如此,BitmapFactory上没有让你传递前n个字节并解码有关图像的元数据的方法。你必须给它完整的流,所以你唯一的选择就是我发布的内容。话虽如此,如果你想自己读取/解码压缩图像格式的前N个字节而不使用BitmapFactory,那么你可以这样做。 - Jeffrey Blattman
显示剩余16条评论

-1

这里是一个对我总是有效的简单方法 :)

 private Bitmap downloadBitmap(String url) {
    // initilize the default HTTP client object
    final DefaultHttpClient client = new DefaultHttpClient();

    //forming a HttoGet request
    final HttpGet getRequest = new HttpGet(url);
    try {

        HttpResponse response = client.execute(getRequest);

        //check 200 OK for success
        final int statusCode = response.getStatusLine().getStatusCode();

        if (statusCode != HttpStatus.SC_OK) {
            Log.w("ImageDownloader", "Error " + statusCode +
                    " while retrieving bitmap from " + url);
            return null;

        }

        final HttpEntity entity = response.getEntity();
        if (entity != null) {
            InputStream inputStream = null;
            try {
                // getting contents from the stream
                inputStream = entity.getContent();

                // decoding stream data back into image Bitmap that android understands
                image = BitmapFactory.decodeStream(inputStream);


            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }
                entity.consumeContent();
            }
        }
    } catch (Exception e) {
        // You Could provide a more explicit error message for IOException
        getRequest.abort();
        Log.e("ImageDownloader", "Something went wrong while" +
                " retrieving bitmap from " + url + e.toString());
    }

    return image;
}

获取位图信息(宽度、高度等)的部分在哪里?缩小采样的部分在哪里? - android developer

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