Efektywne odczytywanie Androida ze strumienia wejściowego


152

Wysyłam żądanie HTTP get do witryny internetowej dla tworzonej przeze mnie aplikacji na Androida.

Używam DefaultHttpClient i używam HttpGet do wystawiania żądania. Otrzymuję odpowiedź jednostki iz tego otrzymuję obiekt InputStream w celu pobrania kodu HTML strony.

Następnie przechodzę przez odpowiedź w następujący sposób:

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

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

Jednak jest to horrendalnie powolne.

Czy to jest nieefektywne? Nie ładuję dużej strony internetowej - www.cokezone.co.uk, więc rozmiar pliku nie jest duży. Czy jest lepszy sposób na zrobienie tego?

Dzięki

Andy


O ile faktycznie nie analizujesz wierszy, nie ma sensu czytać wiersz po wierszu. Wolałbym raczej czytać char przez char przez bufory o stałej wielkości: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Odpowiedzi:


355

Problem w twoim kodzie polega na tym, że tworzy on wiele ciężkich Stringobiektów, kopiuje ich zawartość i wykonuje na nich operacje. Zamiast tego należy używać, StringBuilderaby uniknąć tworzenia nowych Stringobiektów przy każdym dołączaniu i aby uniknąć kopiowania tablic char. Implementacja dla twojego przypadku wyglądałaby mniej więcej tak:

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

Możesz teraz używać totalbez konwertowania go na String, ale jeśli potrzebujesz wyniku jako a String, po prostu dodaj:

Wynik w postaci ciągu = total.toString ();

Postaram się to lepiej wyjaśnić ...

  • a += b(lub a = a + b), gdzie ai bsą ciągami, kopiuje zawartość obu a i b do nowego obiektu (zwróć uwagę, że kopiujesz również a, który zawiera nagromadzone String ) i robisz te kopie w każdej iteracji.
  • a.append(b), gdzie ajest a StringBuilder, bezpośrednio dołącza bzawartość do a, więc nie kopiujesz skumulowanego ciągu w każdej iteracji.

23
Aby uzyskać dodatkowe punkty, zapewnij początkową pojemność, aby uniknąć ponownych przydziałów, gdy StringBuilder StringBuilder total = new StringBuilder(inputStream.available());
zapełni

10
Czy to nie usuwa nowych znaków linii?
Nathan Schwermann

5
nie zapomnij zakończyć podczas try / catch w ten sposób: try {while ((line = r.readLine ())! = null) {total.append (line); }} catch (IOException e) {Log.i (tag, "problem z readline w funkcji inputStreamToString"); }
botbot

4
@botbot: Rejestrowanie i ignorowanie wyjątku nie jest dużo lepsze niż po prostu ignorowanie wyjątku ...
Matti Virkkunen

50
To niesamowite, że Android nie ma wbudowanej konwersji strumienia na ciąg. Ponowne zaimplementowanie readlinepętli każdego fragmentu kodu w sieci i aplikacji na świecie jest absurdalne. Ten wzór powinien był umrzeć wraz z zielenią grochu w latach 70.
Edward Brey,

35

Czy wypróbowałeś wbudowaną metodę konwersji strumienia na ciąg? Jest częścią biblioteki Apache Commons (org.apache.commons.io.IOUtils).

Wtedy twój kod byłby tym jednym wierszem:

String total = IOUtils.toString(inputStream);

Dokumentację do tego można znaleźć tutaj: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

Bibliotekę Apache Commons IO można pobrać tutaj: http://commons.apache.org/io/download_io.cgi


Zdaję sobie sprawę, że to spóźniona odpowiedź, ale właśnie teraz natknąłem się na to za pomocą wyszukiwarki Google.
Makotosan

61
Android API nie obejmuje IOUtils
Charles Ma

2
Racja, dlatego wspomniałem o zewnętrznej bibliotece, która go posiada. Dodałem bibliotekę do mojego projektu na Androida i ułatwiłem czytanie ze strumieni.
Makotosan

gdzie mogę to pobrać i jak zaimportowałeś to do swojego projektu na Androida?
safari

3
Jeśli musisz go pobrać, nie nazwałbym tego „wbudowanym”; niemniej jednak właśnie go pobrałem i spróbuję.
B. Clay Shannon

15

Inna możliwość z guawą:

zależność: compile 'com.google.guava:guava:11.0.2'

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

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

9

Uważam, że jest to wystarczająco wydajne ... Aby uzyskać String z InputStream, wywołałbym następującą metodę:

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

Zawsze używam UTF-8. Możesz oczywiście ustawić charset jako argument, oprócz InputStream.


6

A co z tym. Wydaje się, że zapewnia lepszą wydajność.

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

Edycja: Właściwie ten rodzaj obejmuje zarówno steelbytes, jak i Maurice Perry's


Problem polega na tym, że nie znam rozmiaru rzeczy, którą czytam, zanim zacznę - więc może również potrzebować jakiejś formy powiększania tablicy. Poza tym możesz wysłać zapytanie do InputStream lub adresu URL przez http, aby dowiedzieć się, jak duża rzecz, którą odzyskuje, ma na celu zoptymalizowanie rozmiaru tablicy bajtów. Muszę być skuteczny na urządzeniu mobilnym, co jest głównym problemem! Jednak dzięki za ten pomysł - dziś wieczorem dam ci szansę i pokażę, jak radzi sobie pod względem wzrostu wydajności!
RenegadeAndy

Nie sądzę, żeby rozmiar przychodzącego strumienia był aż tak ważny. Powyższy kod odczytuje 1000 bajtów na raz, ale możesz zwiększyć / zmniejszyć ten rozmiar. Podczas moich testów nie zrobiło to tak dużej różnicy, gdy użyłem 1000/10000 bajtów. To była jednak tylko prosta aplikacja Java. Może to być ważniejsze na urządzeniu mobilnym.
Adrian

4
Możesz skończyć z jednostką Unicode, która jest podzielona na dwa kolejne odczyty. Lepiej czytać, dopóki nie pojawi się jakiś znak graniczny, taki jak \ n, co jest dokładnie tym, co robi BufferedReader.
Jacob Nordfalk

4

Prawdopodobnie nieco szybciej niż odpowiedź Jaime Soriano i bez problemów z wielobajtowym kodowaniem odpowiedzi Adriana, proponuję:

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

Czy możesz wyjaśnić, dlaczego byłoby to szybsze?
Akhil Dad

Nie skanuje wejścia w poszukiwaniu znaków nowej linii, ale odczytuje tylko fragmenty 1024 bajtów. Nie twierdzę, że będzie to miało praktyczne znaczenie.
heiner

jakieś komentarze na temat odpowiedzi @Ronald? Robi to samo, ale dla większego fragmentu równego rozmiarowi strumienia danych wejściowych. Jak inaczej jest, gdy skanuję tablicę znaków, a nie tablicę bajtów, jak odpowiada Nikola? Właściwie chciałem tylko wiedzieć, które podejście jest najlepsze w jakim przypadku? ReadLine usuwa także \ n i \ r, ale widziałem nawet kod aplikacji Google io, z którego korzystają readline
Achil Dad

3

Może zamiast tego przeczytaj „jeden wiersz na raz” i połącz łańcuchy, spróbuj „przeczytaj wszystkie dostępne”, aby uniknąć przeszukiwania końca wiersza, a także łączenia łańcuchów.

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


Hmm. więc wyglądałoby to tak: int offset = 5000; Byte [] bArr = new Byte [100]; Bajt [] łącznie = Bajt [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]; } Czy to naprawdę jest bardziej wydajne - czy źle napisałem! Proszę podać przykład!
RenegadeAndy

2
nie nie nie nie, mam na myśli po prostu {bajt łącznie [] = nowy [instrm.available ()]; instrm.read (total, 0, total.length); } i jeśli potrzebowałeś go jako łańcucha, użyj {String asString = String (total, 0, total.length, "utf-8"); // zakładaj utf8 :-)}
SteelBytes

2

Czytanie jednego wiersza tekstu na raz i dołączanie tego wiersza do łańcucha osobno jest czasochłonne zarówno w przypadku wyodrębniania każdego wiersza, jak i narzutu związanego z tak wieloma wywołaniami metod.

Udało mi się uzyskać lepszą wydajność, przydzielając tablicę bajtów przyzwoitej wielkości do przechowywania danych strumienia, która w razie potrzeby jest iteracyjnie zastępowana większą tablicą i próbując odczytać tyle, ile mogła pomieścić tablica.

Z jakiegoś powodu system Android wielokrotnie nie mógł pobrać całego pliku, gdy kod wykorzystywał InputStream zwracany przez HTTPUrlConnection, więc musiałem skorzystać zarówno z BufferedReader, jak i ręcznie zwijanego mechanizmu limitu czasu, aby upewnić się, że otrzymam cały plik lub anuluję transfer.

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

EDYCJA: Okazuje się, że jeśli nie musisz ponownie kodować treści (tj. Chcesz, aby treść była taka, jaka jest ), nie powinieneś używać żadnej z podklas Reader. Po prostu użyj odpowiedniej podklasy Stream.

Zastąp początek poprzedniej metody odpowiednimi wierszami poniższego, aby przyspieszyć ją dodatkowo 2 do 3 razy .

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

Jest to znacznie szybsze niż powyższe i zaakceptowane odpowiedzi. Jak używać „Czytnika” i „Strumienia” w systemie Android?
SteveGSD

1

Jeśli plik jest długi, możesz zoptymalizować kod, dołączając go do StringBuilder, zamiast używać konkatenacji String dla każdego wiersza.


Szczerze mówiąc, to nie tak długo - to źródło strony www.cokezone.co.uk - więc naprawdę nie jest tak duże. Zdecydowanie mniej niż 100kb.
RenegadeAndy

Czy ktoś ma inne pomysły, jak można to uczynić bardziej wydajnymi - lub jeśli jest to nawet nieefektywne !? Jeśli to ostatnie jest prawdą - dlaczego trwa to tak długo? Nie wierzę, że to połączenie jest winne.
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

Aby przekonwertować InputStream na String, używamy metody BufferedReader.readLine () . Iterujemy, aż BufferedReader zwróci wartość null, co oznacza, że ​​nie ma więcej danych do odczytania. Każda linia zostanie dołączona do StringBuilder i zwrócona jako 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();
    }
}`

I na koniec z dowolnej klasy, w której chcesz przekonwertować, wywołaj funkcję

String dataString = Utils.convertStreamToString(in);

kompletny


-1

Czytam pełne dane:

// inputStream is one instance InputStream
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
String dataString = new String(data);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.