Android高效地从输入流读取数据

161

我正在制作一个针对Android应用程序的HTTP get请求,请求来自网站。

我使用DefaultHttpClient并使用HttpGet发出请求。我得到实体响应,并从中获取一个InputStream对象以获取页面的html。

然后,我循环浏览回复,如下所示:

BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));
String x = "";
x = r.readLine();
String total = "";

while(x!= null){
total += x;
x = r.readLine();
}

不过这个速度太慢了。

这是低效的吗?我没有加载一个大的网页 - www.cokezone.co.uk,所以文件大小并不大。有更好的方法来解决这个问题吗?

谢谢,

Andy


除非您实际上正在解析这些行,否则逐行阅读并没有太多意义。我宁愿通过固定大小的缓冲区逐个字符地读取:https://gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9 - Mike76
12个回答

363

你代码中的问题在于它创建了许多重、大的 String 对象,复制它们的内容并对它们执行操作。相反,你应该使用 StringBuilder 以避免在每次添加时创建新的 String 对象,并避免复制字符数组。针对你的情况,实现方式应该像这样:

BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder total = new StringBuilder();
for (String line; (line = r.readLine()) != null; ) {
    total.append(line).append('\n');
}
现在您可以直接使用total而不需要将其转换为String,但是如果您需要将结果作为String使用,只需添加以下代码:

String result = total.toString();

我尝试更好地解释一下...
  • a += b(或 a = a + b)其中ab是字符串,将 both a and b 的内容复制到一个新对象中(请注意,您还要复制包含累积字符串的 a),并且您每次迭代都会进行这些复制。
  • a.append(b),其中aStringBuilder,直接将b的内容附加到a中,因此您不会在每次迭代时复制已累积的字符串。

26
为了获取附加分,提供一个初始容量以避免StringBuilder填满时重新分配:`StringBuilder total = new StringBuilder(inputStream.available());` - dokkaebi
11
这难道不会删掉换行符吗? - Nathan Schwermann
5
不要忘记像这样用try/catch包装while循环:try { while ((line = r.readLine()) != null) { total.append(line); } } catch (IOException e) { Log.i(tag, "inputStreamToString函数中的readline出现问题"); } - botbot
4
记录并忽略异常并没有比直接忽略异常好多少... - Matti Virkkunen
52
安卓没有内置流转字符串的功能,这真是令人惊讶。在全球范围内,每个网页和应用程序都必须重新实现“读取行”循环,这太荒谬了。这个模式早在70年代的豌豆绿时代就该被淘汰了。 - Edward Brey
显示剩余11条评论

35

66
Android API 中不包括 IOUtils。 - Charles Ma
2
没错,这就是为什么我提到了拥有该功能的外部库。我将该库添加到我的Android项目中,这使得从流中读取变得非常容易。 - Makotosan
请问我可以在哪里下载这个,你是如何将它导入到你的安卓项目中的? - safari
@Makotosan:你能指导一下如何使用这个库吗?我有点困惑。 - KMI
3
如果你需要下载它,我就不能称之为“内置”了;不过我刚刚下了它,打算试一试。 - B. Clay Shannon-B. Crow Raven
显示剩余2条评论

17

使用Guava的另一种可能性:

依赖项:compile 'com.google.guava:guava:11.0.2'

import com.google.common.io.ByteStreams;
...

String total = new String(ByteStreams.toByteArray(inputStream ));

12

我认为这已经足够高效了... 从InputStream中获取一个String,我会调用以下方法:

public static String getStringFromInputStream(InputStream stream) throws IOException
{
    int n = 0;
    char[] buffer = new char[1024 * 4];
    InputStreamReader reader = new InputStreamReader(stream, "UTF8");
    StringWriter writer = new StringWriter();
    while (-1 != (n = reader.read(buffer))) writer.write(buffer, 0, n);
    return writer.toString();
}

我总是使用UTF-8编码。当然,在InputStream之外,你也可以将charset设置为参数。


7

这个怎么样?看起来可以提供更好的性能。

byte[] bytes = new byte[1000];

StringBuilder x = new StringBuilder();

int numRead = 0;
while ((numRead = is.read(bytes)) >= 0) {
    x.append(new String(bytes, 0, numRead));
}

编辑:实际上,这种情况涵盖了steelbytes和Maurice Perry的两个方面。


问题是 - 我在开始读取之前不知道要读取的东西的大小 - 所以可能需要一些形式的数组扩展。除非您可以通过 http 查询 InputStream 或 URL,以查找正在检索的内容的大小,以优化字节数组的大小。我必须高效,因为它在移动设备上运行,这是主要问题!但是感谢那个想法 - 我今晚会尝试一下,并告诉您在性能方面它处理得如何! - RenegadeAndy
我认为输入流的大小并不那么重要。以上的代码每次读取1000个字节,但是你可以增加/减少该大小。在我的测试中,使用1000/10000个字节并没有太大的区别。不过这只是一个简单的Java应用程序。在移动设备上可能更加重要。 - Adrian
5
你可能会得到一个Unicode实体,它被分成两个连续的读取。最好读取直到某种边界字符,比如\n,这正是BufferedReader所做的。 - Jacob Nordfalk

4
我建议采用以下方法,可能比Jaime Soriano的答案快一些,并且避免了Adrian的答案中出现的多字节编码问题:
File file = new File("/tmp/myfile");
try {
    FileInputStream stream = new FileInputStream(file);

    int count;
    byte[] buffer = new byte[1024];
    ByteArrayOutputStream byteStream =
        new ByteArrayOutputStream(stream.available());

    while (true) {
        count = stream.read(buffer);
        if (count <= 0)
            break;
        byteStream.write(buffer, 0, count);
    }

    String string = byteStream.toString();
    System.out.format("%d bytes: \"%s\"%n", string.length(), string);
} catch (IOException e) {
    e.printStackTrace();
}

你能解释一下为什么它会更快吗? - Akhil Dad
它不会扫描输入的换行符,而只是读取1024字节的块。我并不争辩这会产生任何实际的差异。 - heiner
有没有对Ronald的回答有什么评论?他正在做相同的事情,但是针对与inputStream大小相等的更大块。如果我扫描char数组而不是byte数组,它会有多大的不同,就像Nikola的回答一样? 实际上,我只想知道哪种方法在哪种情况下最好? 另外,readLine会删除\n和\r,但我看过甚至谷歌io应用程序代码也在使用readline。 - Akhil Dad

3
也许不必逐行读取并拼接字符串,而是尝试“读取所有可用内容”,以避免扫描行末和字符串拼接。即使用 InputStream.available()InputStream.read(byte[] b), int offset, int length)

嗯,应该是这样的:int offset = 5000; Byte[] bArr = new Byte[100]; Byte[] total = Byte[5000]; while(InputStream.available){ offset = InputStream.read(bArr,offset,100); for(int i=0;i<offset;i++){ total[i] = bArr[i]; } bArr = new Byte[100]; }这样真的更有效率吗?还是我写得不好!请给一个例子! - RenegadeAndy
2
不不不不,我的意思是简单地使用 { byte total[] = new [instrm.available()]; instrm.read(total,0,total.length); } 如果你需要将其作为字符串使用,则使用 { String asString = String(total,0,total.length,"utf-8"); // 假设使用 utf8 :-) } - SteelBytes

2

一次只读取一行文本,并逐个将该行附加到字符串中,这样做在提取每一行和执行如此多的方法调用时都非常耗时。

通过分配一个足够大的字节数组来保存流数据并进行迭代替换,尝试尽可能多地读取数组可以容纳的内容,我能够获得更好的性能。

由于某种原因,当代码使用HTTPUrlConnection返回的InputStream时,Android反复无常地下载整个文件失败,因此我不得不采用BufferedReader和手动超时机制来确保我要么获取整个文件,要么取消传输。

private static  final   int         kBufferExpansionSize        = 32 * 1024;
private static  final   int         kBufferInitialSize          = kBufferExpansionSize;
private static  final   int         kMillisecondsFactor         = 1000;
private static  final   int         kNetworkActionPeriod        = 12 * kMillisecondsFactor;

private String loadContentsOfReader(Reader aReader)
{
    BufferedReader  br = null;
    char[]          array = new char[kBufferInitialSize];
    int             bytesRead;
    int             totalLength = 0;
    String          resourceContent = "";
    long            stopTime;
    long            nowTime;

    try
    {
        br = new BufferedReader(aReader);

        nowTime = System.nanoTime();
        stopTime = nowTime + ((long)kNetworkActionPeriod * kMillisecondsFactor * kMillisecondsFactor);
        while(((bytesRead = br.read(array, totalLength, array.length - totalLength)) != -1)
        && (nowTime < stopTime))
        {
            totalLength += bytesRead;
            if(totalLength == array.length)
                array = Arrays.copyOf(array, array.length + kBufferExpansionSize);
            nowTime = System.nanoTime();
        }

        if(bytesRead == -1)
            resourceContent = new String(array, 0, totalLength);
    }
    catch(Exception e)
    {
        e.printStackTrace();
    }

    try
    {
        if(br != null)
            br.close();
    }
    catch(IOException e)
    {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

编辑:事实证明,如果您不需要重新编码内容(即,您希望内容保持原样),则不应使用任何Reader子类。只需使用适当的Stream子类。

将前面方法的开头替换为以下相应行,可以将其加速额外2到3倍

String  loadContentsFromStream(Stream aStream)
{
    BufferedInputStream br = null;
    byte[]              array;
    int                 bytesRead;
    int                 totalLength = 0;
    String              resourceContent;
    long                stopTime;
    long                nowTime;

    resourceContent = "";
    try
    {
        br = new BufferedInputStream(aStream);
        array = new byte[kBufferInitialSize];

这比上面的答案快得多且被接受。你如何在安卓上使用“Reader”和“Stream”? - SteveGSD

1
为了将InputStream转换为String,我们使用BufferedReader.readLine()方法。我们迭代直到BufferedReader返回null,这意味着没有更多的数据可读取。每行都将附加到StringBuilder中,并作为String返回。
 public static String convertStreamToString(InputStream is) {

        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();

        String line = null;
        try {
            while ((line = reader.readLine()) != null) {
                sb.append(line + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }
}`

最后,无论在哪个类中想要进行转换,都需要调用该函数。
String dataString = Utils.convertStreamToString(in);

完整的。

1
    byte[] buffer = new byte[1024];  // buffer store for the stream
    int bytes; // bytes returned from read()

    // Keep listening to the InputStream until an exception occurs
    while (true) {
        try {
            // Read from the InputStream
            bytes = mmInStream.read(buffer);

            String TOKEN_ = new String(buffer, "UTF-8");

            String xx = TOKEN_.substring(0, bytes);

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