Lecture d'Android à partir d'un flux d'entrée efficacement


152

Je fais une demande HTTP get à un site Web pour une application Android que je crée.

J'utilise un DefaultHttpClient et j'utilise HttpGet pour émettre la demande. J'obtiens la réponse de l'entité et j'obtiens un objet InputStream pour obtenir le html de la page.

Je passe ensuite en revue la réponse en procédant comme suit:

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

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

Cependant, cela est horriblement lent.

Est-ce inefficace? Je ne charge pas une grande page Web - www.cokezone.co.uk donc la taille du fichier n'est pas grande. Y a-t-il une meilleure manière de faire cela?

Merci

Andy


À moins que vous n'analysiez réellement les lignes, il n'a pas beaucoup de sens de lire ligne par ligne. Je préfère lire char par char via des tampons de taille fixe: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Réponses:


355

Le problème dans votre code est qu'il crée beaucoup d' Stringobjets lourds , copie leur contenu et effectue des opérations sur eux. Au lieu de cela, vous devez utiliser StringBuilderpour éviter de créer de nouveaux Stringobjets à chaque ajout et pour éviter de copier les tableaux de caractères. La mise en œuvre pour votre cas serait quelque chose comme ceci:

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

Vous pouvez maintenant l'utiliser totalsans le convertir en String, mais si vous avez besoin du résultat en tant que String, ajoutez simplement:

Résultat de la chaîne = total.toString ();

J'essaierai de mieux l'expliquer ...

  • a += b(ou a = a + b), où aet bsont des chaînes, copie le contenu des deux a et b vers un nouvel objet (notez que vous copiez également a, qui contient l' accumulation String ), et vous effectuez ces copies à chaque itération.
  • a.append(b), où aest a StringBuilder, ajoute directement le bcontenu à a, afin de ne pas copier la chaîne accumulée à chaque itération.

23
Pour les points bonus, fournissez une capacité initiale pour éviter les réallocations lorsque le StringBuilder se remplit: StringBuilder total = new StringBuilder(inputStream.available());
dokkaebi

10
Cela ne supprime-t-il pas les caractères de nouvelle ligne?
Nathan Schwermann

5
n'oubliez pas de terminer le while dans try / catch comme ceci: try {while ((line = r.readLine ())! = null) {total.append (line); }} catch (IOException e) {Log.i (tag, "problème avec readline dans la fonction inputStreamToString"); }
botbot

4
@botbot: Enregistrer et ignorer une exception n'est pas beaucoup mieux que d'ignorer simplement l'exception ...
Matti Virkkunen

50
Il est étonnant qu'Android n'ait pas de conversion de flux en chaîne intégrée. Il readlineest ridicule d' avoir tous les extraits de code sur le Web et les applications de la planète réimplémentant une boucle. Ce motif aurait dû mourir avec du vert pois dans les années 70.
Edward Brey

35

Avez-vous essayé la méthode intégrée pour convertir un flux en chaîne? Il fait partie de la bibliothèque Apache Commons (org.apache.commons.io.IOUtils).

Ensuite, votre code serait cette ligne:

String total = IOUtils.toString(inputStream);

La documentation pour cela peut être trouvée ici: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

La bibliothèque Apache Commons IO peut être téléchargée à partir d'ici: http://commons.apache.org/io/download_io.cgi


Je me rends compte que c'est une réponse tardive, mais je viens de tomber sur cela via une recherche Google.
Makotosan

61
L'API Android n'inclut pas IOUtils
Charles Ma

2
Bien, c'est pourquoi j'ai mentionné la bibliothèque externe qui l'a. J'ai ajouté la bibliothèque à mon projet Android et cela a facilité la lecture à partir des flux.
Makotosan

où puis-je télécharger ceci et comment l'avez-vous importé dans votre projet Android?
safari

3
Si vous devez le télécharger, je ne l'appellerais pas "intégré"; néanmoins, je viens de le télécharger et je vais l'essayer.
B. Clay Shannon

15

Une autre possibilité avec Guava:

dépendance: compile 'com.google.guava:guava:11.0.2'

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

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

9

Je pense que c'est assez efficace ... Pour obtenir une chaîne à partir d'un InputStream, j'appellerais la méthode suivante:

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

J'utilise toujours UTF-8. Vous pouvez, bien sûr, définir charset comme argument, en plus d'InputStream.


6

Et ça. Semble donner de meilleures performances.

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: En fait, ce type englobe à la fois steelbytes et Maurice Perry


Le problème est - je ne connais pas la taille de la chose que je lis avant de commencer - donc pourrait avoir besoin d'une forme de tableau de plus en plus. À moins que vous ne puissiez interroger un InputStream ou une URL via http pour savoir à quel point la récupération est importante pour optimiser la taille du tableau d'octets. Je dois être efficace car c'est sur un appareil mobile qui est le principal problème! Cependant merci pour cette idée - Je vais essayer ce soir et vous faire savoir comment il gère en termes de gain de performance!
RenegadeAndy

Je ne pense pas que la taille du flux entrant soit si importante. Le code ci-dessus lit 1000 octets à la fois, mais vous pouvez augmenter / diminuer cette taille. Avec mes tests, cela n'a pas fait autant de différence que la météo, j'ai utilisé 1000/10000 octets. Ce n'était cependant qu'une simple application Java. Cela peut être plus important sur un appareil mobile.
Adrian

4
Vous pourriez vous retrouver avec une entité Unicode découpée en deux lectures suivantes. Mieux vaut lire jusqu'à une sorte de caractère de limite, comme \ n, qui est exactement ce que fait BufferedReader.
Jacob Nordfalk

4

Peut-être un peu plus rapide que la réponse de Jaime Soriano, et sans les problèmes d'encodage multi-octets de la réponse d'Adrian, je suggère:

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

Pouvez-vous expliquer pourquoi ce serait plus rapide?
Akhil Dad le

Il ne scanne pas l'entrée pour les caractères de nouvelle ligne, mais lit simplement des morceaux de 1024 octets. Je ne dis pas que cela fera une différence pratique.
heiner le

des commentaires sur la réponse @Ronald? Il fait la même chose mais pour un morceau plus grand égal à la taille de inputStream. Aussi, à quel point il est différent si je scanne un tableau de caractères plutôt qu'un tableau d'octets comme réponse Nikola? En fait, je voulais juste savoir quelle approche est la meilleure dans quel cas? ReadLine supprime également \ n et \ r mais j'ai même vu le code d'application google io qu'ils utilisent readline
Akhil Dad

3

Peut-être plutôt que de lire «une ligne à la fois» et de joindre les chaînes, essayez de «lire tout disponible» afin d'éviter de rechercher la fin de ligne et également d'éviter les jointures de chaînes.

ie, InputStream.available()etInputStream.read(byte[] b), int offset, int length)


Hmm. donc ce serait comme ceci: int offset = 5000; Byte [] bArr = nouvel octet [100]; Octet [] total = Octet [5000]; while (InputStream.available) {offset = InputStream.read (bArr, offset, 100); pour (int i = 0; i <offset; i ++) {total [i] = bArr [i]; } bArr = nouvel octet [100]; } Est-ce vraiment plus efficace - ou est-ce que je l'ai mal écrit! Veuillez donner un exemple!
RenegadeAndy

2
non non non non, je veux dire simplement {byte total [] = new [instrm.available ()]; instrm.read (total, 0, total.length); } et si vous en aviez besoin en tant que chaîne, utilisez {String asString = String (total, 0, total.length, "utf-8"); // assume utf8 :-)}
SteelBytes

2

La lecture d'une ligne de texte à la fois et l'ajout de ladite ligne à une chaîne individuellement prend du temps à la fois pour l'extraction de chaque ligne et la surcharge de tant d'appels de méthode.

J'ai pu obtenir de meilleures performances en allouant un tableau d'octets de taille décente pour contenir les données de flux, et qui est remplacé de manière itérative par un tableau plus grand si nécessaire, et en essayant de lire autant que le tableau peut contenir.

Pour une raison quelconque, Android n'a pas réussi à télécharger le fichier entier à plusieurs reprises lorsque le code utilisait le InputStream retourné par HTTPUrlConnection, j'ai donc dû recourir à la fois à un BufferedReader et à un mécanisme de délai d'expiration manuel pour m'assurer que j'obtiendrais le fichier entier ou annulerais le transfert.

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: Il s'avère que si vous n'avez pas besoin de ré-encoder le contenu (c'est-à-dire que vous voulez le contenu TEL QUEL ), vous ne devez utiliser aucune des sous-classes de Reader. Utilisez simplement la sous-classe Stream appropriée.

Remplacez le début de la méthode précédente par les lignes correspondantes de la suivante pour l'accélérer de 2 à 3 fois supplémentaires .

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

C'est beaucoup plus rapide que les réponses ci-dessus et acceptées. Comment utilisez-vous "Reader" et "Stream" sur Android?
SteveGSD

1

Si le fichier est long, vous pouvez optimiser votre code en l'ajoutant à un StringBuilder au lieu d'utiliser une concaténation de chaîne pour chaque ligne.


Ce n'est pas si long pour être honnête - c'est la source de la page du site www.cokezone.co.uk - donc vraiment pas si grand. Certainement moins de 100 Ko.
RenegadeAndy

Quelqu'un a-t-il d'autres idées sur la manière de rendre cela plus efficace - ou si cela est même inefficace !? Si ce dernier est vrai, pourquoi cela prend-il si longtemps? Je ne crois pas que la connexion est à blâmer.
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

Pour convertir InputStream en String, nous utilisons la méthode BufferedReader.readLine () . Nous itérons jusqu'à ce que BufferedReader renvoie null, ce qui signifie qu'il n'y a plus de données à lire. Chaque ligne sera ajoutée à un StringBuilder et renvoyée sous forme de chaîne.

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

Et enfin à partir de n'importe quelle classe où vous souhaitez convertir, appelez la fonction

String dataString = Utils.convertStreamToString(in);

Achevée


-1

Je suis habitué à lire les données complètes:

// inputStream is one instance InputStream
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
String dataString = new String(data);
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.