Lettura Android da un flusso di input in modo efficiente


152

Sto inviando una richiesta HTTP a un sito Web per un'applicazione Android che sto realizzando.

Sto usando un DefaultHttpClient e sto usando HttpGet per inviare la richiesta. Ottengo la risposta dell'entità e da ciò ottengo un oggetto InputStream per ottenere l'html della pagina.

Quindi scorrere la risposta nel modo seguente:

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

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

Tuttavia, questo è terribilmente lento.

È inefficiente? Non sto caricando una grande pagina web - www.cokezone.co.uk, quindi la dimensione del file non è grande. C'è un modo migliore per farlo?

Grazie

Andy


A meno che tu non stia effettivamente analizzando le righe, non ha molto senso leggere riga per riga. Preferirei leggere i caratteri in base al carattere tramite buffer di dimensioni fisse: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Risposte:


355

Il problema nel tuo codice è che sta creando molti Stringoggetti pesanti , copiando i loro contenuti ed eseguendo operazioni su di essi. Invece, è necessario utilizzare StringBuilderper evitare la creazione di nuovi Stringoggetti su ciascuna append e per evitare di copiare gli array di caratteri. L'implementazione per il tuo caso sarebbe qualcosa del genere:

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

Ora puoi usare totalsenza convertirlo in String, ma se hai bisogno del risultato come un String, aggiungi semplicemente:

Risultato stringa = total.toString ();

Proverò a spiegarlo meglio ...

  • a += b(o a = a + b), dove ae bsono stringhe, copia il contenuto di entrambi a e b in un nuovo oggetto (nota che stai anche copiando a, che contiene l' accumulato String ), e stai facendo quelle copie su ogni iterazione.
  • a.append(b), dove aè a StringBuilder, aggiunge direttamente i bcontenuti a, quindi non copi la stringa accumulata ad ogni iterazione.

23
Per i punti bonus, fornisci una capacità iniziale per evitare riallocazioni quando lo StringBuilder si riempie: StringBuilder total = new StringBuilder(inputStream.available());
dokkaebi

10
Questo non ritaglia i nuovi caratteri di linea?
Nathan Schwermann,

5
non dimenticare di concludere un po 'in try / catch in questo modo: try {while ((line = r.readLine ())! = null) {total.append (line); }} catch (IOException e) {Log.i (tag, "problema con readline nella funzione inputStreamToString"); }
botbot

4
@botbot: registrare e ignorare un'eccezione non è molto meglio che ignorare l'eccezione ...
Matti Virkkunen,

50
È sorprendente che Android non abbia una conversione stream-to-string integrata. Avere ogni frammento di codice sul web e le app sul pianeta reimplementare un readlineciclo è ridicolo. Quel modello avrebbe dovuto morire con il verde pisello negli anni '70.
Edward Brey,

35

Hai provato il metodo integrato per convertire uno stream in una stringa? Fa parte della libreria Apache Commons (org.apache.commons.io.IOUtils).

Quindi il tuo codice sarebbe questa riga:

String total = IOUtils.toString(inputStream);

La documentazione per questo può essere trovata qui: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

La libreria IO di Apache Commons può essere scaricata da qui: http://commons.apache.org/io/download_io.cgi


Mi rendo conto che questa è una risposta tardiva, ma proprio ora è capitato di imbattersi in questo tramite una ricerca di Google.
Makotosan,

61
L'API Android non include IOUtils
Charles Ma

2
Giusto, ecco perché ho citato la biblioteca esterna che ce l'ha. Ho aggiunto la libreria al mio progetto Android e mi ha facilitato la lettura dagli stream.
Makotosan,

dove posso scaricare questo e come lo hai importato nel tuo progetto Android?
safari

3
Se dovessi scaricarlo, non lo definirei "integrato"; tuttavia, l'ho appena scaricato e lo proverò.
B. Clay Shannon

15

Un'altra possibilità con Guava:

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

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

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

9

Credo che questo sia abbastanza efficiente ... Per ottenere una stringa da un InputStream, chiamerei il seguente metodo:

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

Uso sempre UTF-8. Naturalmente, potresti impostare charset come argomento, oltre a InputStream.


6

Che dire di questo. Sembra dare prestazioni migliori.

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

Modifica: In realtà questo tipo di comprende sia steelbytes che Maurice Perry


Il problema è - non conosco le dimensioni della cosa che sto leggendo prima di iniziare - quindi potrebbe essere necessaria anche una qualche forma di array in crescita. A meno che non sia possibile eseguire una query su un InputStream o un URL tramite http per scoprire quanto è importante recuperare una dimensione dell'array di byte. Devo essere efficiente su un dispositivo mobile che è il problema principale! Tuttavia, grazie per quell'idea - Stasera ci proverai e ti farò sapere come gestisce in termini di guadagno delle prestazioni!
RenegadeAndy

Non credo che la dimensione del flusso in entrata sia così importante. Il codice precedente legge 1000 byte alla volta ma è possibile aumentare / ridurre quella dimensione. Con i miei test non ha fatto molta differenza, ho usato 1000/10000 byte. Questa era solo una semplice app Java. Potrebbe essere più importante su un dispositivo mobile.
Adrian,

4
Potresti finire con un'entità Unicode che viene suddivisa in due letture successive. Meglio leggere fino a una sorta di carattere al contorno, come \ n, che è esattamente ciò che fa BufferedReader.
Jacob Nordfalk,

4

Forse un po 'più veloce della risposta di Jaime Soriano, e senza i problemi di codifica multi-byte della risposta di Adrian, suggerisco:

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

Puoi spiegare perché sarebbe più veloce?
Akhil Dad,

Non esegue la scansione dell'input per i caratteri di nuova riga, ma legge solo blocchi di 1024 byte. Non sto sostenendo che questo farà alcuna differenza pratica.
Heiner

eventuali commenti su @Ronald rispondono? Sta facendo lo stesso, ma per un pezzo più grande uguale alla dimensione inputStream. Inoltre, quanto è diverso se eseguo la scansione di array di caratteri anziché di array di byte come risposta Nikola? In realtà volevo solo sapere quale approccio è il migliore in quale caso? Anche readLine rimuove \ n e \ r ma ho visto persino il codice dell'app google io che stanno usando readline
Akhil Dad

3

Forse preferisci leggere "una riga alla volta" e unire le stringhe, provare "leggi tutte le opzioni disponibili" in modo da evitare la scansione per la fine della riga e anche per evitare i join di stringhe.

cioè, InputStream.available()eInputStream.read(byte[] b), int offset, int length)


Hmm. quindi sarebbe così: int offset = 5000; Byte [] bArr = new Byte [100]; Byte [] total = Byte [5000]; while (InputStream.available) {offset = InputStream.read (bArr, offset, 100); per (int i = 0; i <offset; i ++) {total [i] = bArr [i]; } bArr = new Byte [100]; } È davvero più efficiente - o l'ho scritto male! Per favore, fai un esempio!
RenegadeAndy

2
no no no no, intendo semplicemente {byte total [] = new [instrm.available ()]; instrm.read (totale, 0, total.length); } e se ne avevi bisogno come stringa, usa {String asString = String (total, 0, total.length, "utf-8"); // assume utf8 :-)}
SteelBytes il

2

Leggere una riga di testo alla volta e aggiungere singolarmente detta riga a una stringa richiede molto tempo sia nell'estrazione di ciascuna riga sia nell'overhead di così tante invocazioni di metodi.

Sono stato in grado di ottenere prestazioni migliori allocando un array di byte di dimensioni decenti per contenere i dati di flusso e che viene sostituito iterativamente con un array più grande quando necessario e provando a leggere quanto l'array potrebbe contenere.

Per qualche motivo, Android non è riuscito a scaricare ripetutamente l'intero file quando il codice utilizzava InputStream restituito da HTTPUrlConnection, quindi ho dovuto ricorrere all'uso di un BufferedReader e di un meccanismo di timeout a rotazione manuale per assicurarmi che avrei ottenuto l'intero file o annullato il trasferimento.

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: Si scopre che se non è necessario ricodificare il contenuto (ovvero, si desidera il contenuto COSÌ COM'È ) non è necessario utilizzare nessuna delle sottoclassi di Reader. Basta usare la sottoclasse Stream appropriata.

Sostituisci l'inizio del metodo precedente con le righe corrispondenti di seguito per velocizzarlo da 2 a 3 volte in più .

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];

Questo è molto più veloce delle risposte sopra e accettate. Come usi "Reader" e "Stream" su Android?
SteveGSD

1

Se il file è lungo, è possibile ottimizzare il codice aggiungendo a StringBuilder invece di utilizzare una concatenazione di stringhe per ogni riga.


Non è poi tanto tempo per essere onesti - è la fonte della pagina del sito web www.cokezone.co.uk - quindi non è poi così grande. Sicuramente meno di 100kb.
RenegadeAndy

Qualcuno ha altre idee su come questo potrebbe essere reso più efficiente - o se questo è persino inefficiente !? Se quest'ultimo è vero, perché impiega così tanto tempo? Non credo che la colpa sia della connessione.
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);

1

Per convertire InputStream in String utilizziamo il metodo BufferedReader.readLine () . Ripetiamo fino a quando BufferedReader non restituisce null, il che significa che non ci sono più dati da leggere. Ogni riga verrà aggiunta a StringBuilder e restituita come 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 infine da qualsiasi classe in cui desideri convertire, chiama la funzione

String dataString = Utils.convertStreamToString(in);

completare


-1

Sono abituato a leggere i dati completi:

// inputStream is one instance InputStream
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
String dataString = new String(data);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.