Leitura Android de um fluxo de entrada com eficiência

152

Estou fazendo uma solicitação de obtenção HTTP para um site para um aplicativo Android que estou fazendo.

Estou usando um DefaultHttpClient e usando HttpGet para emitir a solicitação. Recebo a resposta da entidade e, a partir disso, obtenho um objeto InputStream para obter o html da página.

Em seguida, alterno a resposta da seguinte maneira:

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

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

No entanto, isso é terrivelmente lento.

Isso é ineficiente? Não estou carregando uma grande página da web - www.cokezone.co.uk, portanto o tamanho do arquivo não é grande. Existe uma maneira melhor de fazer isso?

obrigado

Andy

RenegadeAndy
fonte
A menos que você esteja realmente analisando as linhas, não faz muito sentido ler linha por linha. I preferem ler caractere por caractere através dos tampões de tamanho fixo: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Respostas:

355

O problema no seu código é que ele está criando muitos Stringobjetos pesados , copiando seu conteúdo e executando operações neles. Em vez disso, você deve StringBuilderevitar criar novos Stringobjetos em cada anexo e copiar as matrizes de caracteres. A implementação para o seu caso seria algo como isto:

BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder total = new StringBuilder();
for (String line; (line = r.readLine()) != null; ) {
    total.append(line).append('\n');
}

Agora você pode usá- totallo sem convertê-lo em String, mas se precisar do resultado como a String, basta adicionar:

Resultado da string = total.toString ();

Vou tentar explicar melhor ...

  • a += b(ou a = a + b), onde ae bsão Strings, copia o conteúdo de ambos a e b para um novo objeto (observe que você também está copiando a, que contém o acumulado String ) e está fazendo essas cópias em cada iteração.
  • a.append(b), onde aé a StringBuilder, anexa diretamente o bconteúdo a, para que você não copie a sequência acumulada a cada iteração.
Jaime Soriano
fonte
23
Para obter pontos de bônus, forneça uma capacidade inicial para evitar realocações à medida que o StringBuilder for preenchido: StringBuilder total = new StringBuilder(inputStream.available());
dokkaebi
10
Isso não elimina novos caracteres de linha?
Nathan Schwermann
5
não se esqueça de encerrar o tempo em try / catch desta maneira: try {while ((line = r.readLine ())! = null) {total.append (line); }} catch (IOException e) {Log.i (tag, "problema com a linha de leitura na função inputStreamToString"); }
botbot
4
@botbot: Logging e ignorando uma exceção não é muito melhor do que simplesmente ignorar a exceção ...
Matti Virkkunen
50
É incrível que o Android não tenha uma conversão integrada de fluxo para string. Ter cada snippet de código na Web e aplicativo no planeta reimplementar um readlineloop é ridículo. Esse padrão deveria ter morrido com o verde de ervilha nos anos 70.
Edward Brey
35

Você já tentou o método interno para converter um fluxo em uma string? Faz parte da biblioteca do Apache Commons (org.apache.commons.io.IOUtils).

Então seu código seria esta linha:

String total = IOUtils.toString(inputStream);

A documentação para isso pode ser encontrada aqui: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

A biblioteca do Apache Commons IO pode ser baixada aqui: http://commons.apache.org/io/download_io.cgi

Makotosan
fonte
Sei que esta é uma resposta tardia, mas agora tropeçamos nessa via uma pesquisa no Google.
Makotosan
61
A API do Android não inclui IOUtils
Charles Ma
2
Certo, razão pela qual mencionei a biblioteca externa que a possui. Adicionei a biblioteca ao meu projeto Android e facilitou a leitura dos fluxos.
Makotosan
onde posso fazer o download e como você importou isso no seu projeto android?
Safari
3
Se você tiver que fazer o download, eu não chamaria isso de "embutido"; no entanto, acabei de fazer o download e vou tentar.
B. Clay Shannon
15

Outra possibilidade com o Goiaba:

dependência: compile 'com.google.guava:guava:11.0.2'

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

String total = new String(ByteStreams.toByteArray(inputStream ));
Andrey
fonte
9

Acredito que isso seja eficiente o suficiente ... Para obter uma String de um InputStream, eu chamaria o seguinte método:

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();
}

Eu sempre uso UTF-8. Você poderia, é claro, definir charset como argumento, além do InputStream.

Budimir Grom
fonte
6

Que tal isso? Parece dar um melhor desempenho.

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));
}

Edit: Na verdade, esse tipo de abrange tanto steelbytes quanto Maurice Perry

Adrian
fonte
O problema é - eu não sei o tamanho da coisa que estou lendo antes de começar - então pode precisar de algum tipo de matriz crescendo também. A menos que você possa consultar um InputStream ou URL por meio de http para descobrir o tamanho da coisa que estou recuperando para otimizar o tamanho da matriz de bytes. Eu tenho que ser eficiente como em um dispositivo móvel, que é o principal problema! No entanto, obrigado por essa idéia - hoje você terá uma chance e informará como ela lida com o ganho de desempenho!
RenegadeAndy
Não acho que o tamanho do fluxo recebido seja tão importante. O código acima lê 1000 bytes de cada vez, mas você pode aumentar / diminuir esse tamanho. Com meus testes, não fez muita diferença no clima, usei 1000/10000 bytes. Esse foi apenas um aplicativo Java simples. Pode ser mais importante em um dispositivo móvel.
Adrian
4
Você pode acabar com uma entidade Unicode que é dividida em duas leituras subsequentes. Melhor ler até algum tipo de caractere de limite, como \ n, que é exatamente o que o BufferedReader faz.
Jacob Nordfalk
4

Possivelmente um pouco mais rápido que a resposta de Jaime Soriano, e sem os problemas de codificação de vários bytes da resposta de Adrian, sugiro:

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();
}
Heiner
fonte
Você pode explicar por que seria mais rápido?
Akhil Dad 02/02
Ele não verifica a entrada em busca de caracteres de nova linha, mas apenas lê pedaços de 1024 bytes. Não estou argumentando que isso fará alguma diferença prática.
21716
algum comentário sobre a resposta @Ronald? Ele está fazendo o mesmo, mas para um pedaço maior igual ao tamanho do inputStream. Além disso, como é diferente se eu digitalizar uma matriz de caracteres em vez de uma matriz de bytes como a Nikola responde? Na verdade, eu só queria saber qual é a melhor abordagem, em qual caso? Também remove readLine \ n e \ r mas eu vi até mesmo o Google io código do aplicativo eles estão usando readline
Dad Akhil
3

Talvez, em vez disso, leia 'uma linha de cada vez' e junte as strings, tente 'read all available' para evitar a varredura do fim da linha e também para evitar junções de strings.

ou seja, InputStream.available()eInputStream.read(byte[] b), int offset, int length)

SteelBytes
fonte
Hmm. então seria assim: int offset = 5000; Byte [] bArr = novo Byte [100]; Byte [] total = Byte [5000]; while (InputStream.available) {offset = InputStream.read (bArr, offset, 100); for (int i = 0; i <deslocamento; i ++) {total [i] = bArr [i]; } bArr = novo Byte [100]; } Isso é realmente mais eficiente - ou eu escrevi mal! Por favor, dê um exemplo!
precisa
2
não não não não, quero dizer simplesmente {byte total [] = novo [instrm.available ()]; instrm.read (total, 0, total.length); } e se você precisar dele como uma String, use {String asString = String (total, 0, total.length, "utf-8"); // assume utf8 :-)}
SteelBytes 25/03
2

Ler uma linha de texto de cada vez e anexar a referida linha a uma cadeia de caracteres individualmente consome muito tempo ao extrair cada linha e a sobrecarga de tantas invocações de métodos.

Consegui obter um melhor desempenho alocando uma matriz de bytes de tamanho decente para armazenar os dados do fluxo e que é substituída iterativamente por uma matriz maior quando necessário, e tentando ler o máximo que a matriz pudesse suportar.

Por alguma razão, o Android falhou repetidamente no download do arquivo inteiro quando o código usou o InputStream retornado por HTTPUrlConnection, então tive que recorrer ao uso de um BufferedReader e um mecanismo de tempo limite rolado manualmente para garantir que eu obtivesse o arquivo inteiro ou cancelasse a transferência.

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();
    }
}

EDIT: Acontece que, se você não precisa ter o conteúdo recodificado (ou seja, deseja que o conteúdo seja como está ), não deve usar nenhuma das subclasses de Reader. Basta usar a subclasse Stream adequada.

Substitua o início do método anterior pelas linhas correspondentes a seguir para acelerar mais 2 a 3 vezes .

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];
Huperniketes
fonte
Isso é muito mais rápido que as respostas acima e aceitas. Como você usa "Reader" e "Stream" no Android?
SteveGSD
1

Se o arquivo for longo, você poderá otimizar seu código anexando a um StringBuilder em vez de usar uma concatenação de String para cada linha.

Maurice Perry
fonte
Não demorou muito para ser honesto - é a fonte da página do site www.cokezone.co.uk - portanto, não é tão grande assim. Definitivamente menos de 100kb.
usar o seguinte
Alguém tem alguma outra idéia de como isso poderia ser mais eficiente - ou se isso é mesmo ineficiente !? Se o último é verdadeiro - por que demora tanto? Não acredito que a conexão seja a culpada.
RenegadeAndy
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);
José Araújo
fonte
1

Para converter o InputStream em String, usamos o método BufferedReader.readLine () . Repetimos até o BufferedReader retornar nulo, o que significa que não há mais dados para ler. Cada linha será anexada a um StringBuilder e retornada como 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();
    }
}`

E, finalmente, de qualquer classe em que você deseja converter, chame a função

String dataString = Utils.convertStreamToString(in);

completo

yubaraj poudel
fonte
-1

Estou acostumado a ler dados completos:

// inputStream is one instance InputStream
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
String dataString = new String(data);
Ronald
fonte