Lectura de Android desde un flujo de entrada de manera eficiente

152

Estoy haciendo una solicitud de obtención de HTTP a un sitio web para una aplicación de Android que estoy haciendo.

Estoy usando un DefaultHttpClient y estoy usando HttpGet para emitir la solicitud. Obtengo la respuesta de la entidad y de esto obtengo un objeto InputStream para obtener el html de la página.

Luego paso por la respuesta haciendo lo siguiente:

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

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

Sin embargo, esto es terriblemente lento.

¿Es esto ineficiente? No estoy cargando una página web grande: www.cokezone.co.uk, por lo que el tamaño del archivo no es grande. ¿Hay una mejor manera de hacer esto?

Gracias

Andy

RenegadoAndy
fuente
A menos que realmente esté analizando las líneas, no tiene mucho sentido leer línea por línea. Prefiero leer char by char a través de buffers de tamaño fijo: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Respuestas:

355

El problema en su código es que está creando muchos Stringobjetos pesados , copiando su contenido y realizando operaciones en ellos. En su lugar, debe usar StringBuilderpara evitar crear nuevos Stringobjetos en cada apéndice y para evitar copiar las matrices de caracteres. La implementación para su caso sería algo como esto:

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

Ahora puede usarlo totalsin convertirlo String, pero si necesita el resultado como a String, simplemente agregue:

Resultado de cadena = total.toString ();

Trataré de explicarlo mejor ...

  • a += b(o a = a + b), donde ay bson cadenas, copia el contenido de ambos a y b en un nuevo objeto (tenga en cuenta que también está copiando a, que contiene el acumulado String ), y está haciendo esas copias en cada iteración.
  • a.append(b), donde aes a StringBuilder, agrega bcontenido directamente a, de modo que no copie la cadena acumulada en cada iteración.
Jaime Soriano
fuente
23
Para obtener puntos de bonificación, proporcione una capacidad inicial para evitar reasignaciones a medida que el StringBuilder se llena: StringBuilder total = new StringBuilder(inputStream.available());
dokkaebi
10
¿Esto no corta nuevos caracteres de línea?
Nathan Schwermann
55
no olvide envolver el while en try / catch así: try {while ((line = r.readLine ())! = null) {total.append (line); }} catch (IOException e) {Log.i (etiqueta, "problema con readline en la función inputStreamToString"); }
botbot
44
@botbot: Iniciar sesión e ignorar una excepción no es mucho mejor que simplemente ignorar la excepción ...
Matti Virkkunen
50
Es sorprendente que Android no tenga una conversión integrada de secuencia a cadena. Tener cada fragmento de código en la web y la aplicación en el planeta para volver a implementar un readlinebucle es ridículo. Ese patrón debería haber muerto con verde guisante en los años 70.
Edward Brey
35

¿Has probado el método incorporado para convertir una secuencia en una cadena? Es parte de la biblioteca Apache Commons (org.apache.commons.io.IOUtils).

Entonces su código sería esta línea:

String total = IOUtils.toString(inputStream);

La documentación para ello se puede encontrar aquí: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

La biblioteca Apache Commons IO se puede descargar desde aquí: http://commons.apache.org/io/download_io.cgi

Makotosan
fuente
Me doy cuenta de que esta es una respuesta tardía, pero justo ahora me topé con esto a través de una búsqueda en Google.
Makotosan
61
La API de Android no incluye IOUtils
Charles Ma
2
Correcto, por eso mencioné la biblioteca externa que lo tiene. Agregué la biblioteca a mi proyecto de Android y facilitó la lectura de las transmisiones.
Makotosan el
¿Dónde puedo descargar esto y cómo importó eso en su proyecto de Android?
safari
3
Si tiene que descargarlo, no lo llamaría "integrado"; sin embargo, lo acabo de descargar y lo probaré.
B. Clay Shannon
15

Otra posibilidad con guayaba:

dependencia: compile 'com.google.guava:guava:11.0.2'

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

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

Creo que esto es lo suficientemente eficiente ... Para obtener una cadena de un InputStream, llamaría al siguiente 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();
}

Siempre uso UTF-8. Podría, por supuesto, establecer charset como argumento, además de InputStream.

Budimir Grom
fuente
6

¿Qué hay de esto? Parece dar un mejor rendimiento.

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

Editar: en realidad, este tipo abarca tanto steelbytes como Maurice Perry

Adrian
fuente
El problema es que no sé el tamaño de lo que estoy leyendo antes de comenzar, por lo que también podría necesitar algún tipo de matriz. A menos que pueda consultar un InputStream o URL a través de http para averiguar qué tan grande es la cosa que estoy recuperando para optimizar el tamaño de la matriz de bytes. ¡Tengo que ser eficiente ya que está en un dispositivo móvil, que es el principal problema! Sin embargo, gracias por esa idea: ¡le daré una oportunidad esta noche y le informaré cómo se maneja en términos de ganancia de rendimiento!
RenegadeAndy
No creo que el tamaño de la transmisión entrante sea tan importante. El código anterior lee 1000 bytes a la vez, pero podría aumentar / disminuir ese tamaño. Con mis pruebas no hizo mucha diferencia el clima usé 1000/10000 bytes. Sin embargo, esa era solo una aplicación Java simple. Puede ser más importante en un dispositivo móvil.
Adrian
44
Podría terminar con una entidad Unicode que se corta en dos lecturas posteriores. Es mejor leer hasta algún tipo de carácter de límite, como \ n, que es exactamente lo que hace BufferedReader.
Jacob Nordfalk
4

Posiblemente algo más rápido que la respuesta de Jaime Soriano, y sin los problemas de codificación de varios bytes de la respuesta de Adrian, sugiero:

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
fuente
¿Puedes explicar por qué sería más rápido?
Akhil Dad
No escanea la entrada en busca de caracteres de nueva línea, sino que solo lee fragmentos de 1024 bytes. No estoy discutiendo que esto haga alguna diferencia práctica.
heiner
¿Algún comentario sobre la respuesta de @Ronald? Él está haciendo lo mismo pero por una porción más grande igual al tamaño inputStream. Además, ¿qué tan diferente es si escaneo la matriz de caracteres en lugar de la matriz de bytes como Nikola responde? En realidad, solo quería saber qué enfoque es el mejor en cuyo caso. También readLine elimina \ ny \ r pero vi incluso el código de la aplicación google io que están usando readline
Akhil Dad
3

Tal vez, en lugar de leer 'una línea a la vez' y unir las cadenas, intente 'leer todas las disponibles' para evitar el escaneo en busca de fin de línea y también evitar uniones de cadenas.

es decir, InputStream.available()yInputStream.read(byte[] b), int offset, int length)

SteelBytes
fuente
Hmm entonces sería así: int offset = 5000; Byte [] bArr = nuevo Byte [100]; Byte [] total = Byte [5000]; while (InputStream.available) {offset = InputStream.read (bArr, offset, 100); para (int i = 0; i <offset; i ++) {total [i] = bArr [i]; } bArr = nuevo Byte [100]; } ¿Es eso realmente más eficiente, o lo he escrito mal? ¡Por favor da un ejemplo!
RenegadeAndy
2
no no no no, quiero decir simplemente {byte total [] = new [instrm.available ()]; instrm.read (total, 0, longitud total); } y si luego lo necesitabas como String, usa {String asString = String (total, 0, total.length, "utf-8"); // asumir utf8 :-)}
SteelBytes
2

Leer una línea de texto a la vez y agregar dicha línea a una cadena individualmente lleva mucho tiempo, tanto en la extracción de cada línea como en la sobrecarga de tantas invocaciones de métodos.

Pude obtener un mejor rendimiento al asignar una matriz de bytes de tamaño decente para contener los datos de la secuencia, y que se reemplaza iterativamente por una matriz más grande cuando es necesario, y al tratar de leer tanto como la matriz podría contener.

Por alguna razón, Android repetidamente no pudo descargar el archivo completo cuando el código usó el InputStream devuelto por HTTPUrlConnection, por lo que tuve que recurrir al uso de un BufferedReader y un mecanismo de tiempo de espera manual para asegurarme de obtener el archivo completo o cancelar la transferencia.

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

EDITAR: Resulta que si no necesita tener el contenido codificado de nuevo (es decir, si desea el contenido TAL CUAL ), no debe usar ninguna de las subclases de Reader. Simplemente use la subclase Stream adecuada.

Reemplace el comienzo del método anterior con las líneas correspondientes de lo siguiente para acelerarlo de 2 a 3 veces más .

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
fuente
Esto es mucho más rápido que lo anterior y las respuestas aceptadas. ¿Cómo se usa "Reader" y "Stream" en Android?
SteveGSD
1

Si el archivo es largo, puede optimizar su código agregando un StringBuilder en lugar de usar una concatenación de String para cada línea.

Maurice Perry
fuente
No es mucho tiempo para ser sincero, es la fuente de la página del sitio web www.cokezone.co.uk, así que realmente no es tan grande. Definitivamente menos de 100kb.
RenegadeAndy
¿Alguien tiene alguna otra idea sobre cómo esto podría hacerse más eficiente, o si esto es incluso ineficiente? Si esto último es cierto, ¿por qué lleva TANTO tiempo? No creo que la conexión sea la culpable.
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
fuente
1

Para convertir InputStream a String, utilizamos el método BufferedReader.readLine () . Repetimos hasta que BufferedReader devuelva nulo, lo que significa que no hay más datos para leer. Cada línea se agregará a un StringBuilder y se devolverá 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();
    }
}`

Y finalmente, desde cualquier clase donde quieras convertir llama a la función

String dataString = Utils.convertStreamToString(in);

completar

Yubaraj Poudel
fuente
-1

Estoy acostumbrado a leer datos completos:

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