Android Membaca dari aliran input secara efisien

152

Saya membuat permintaan dapatkan HTTP ke situs web untuk aplikasi android yang saya buat.

Saya menggunakan DefaultHttpClient dan menggunakan HttpGet untuk mengeluarkan permintaan. Saya mendapatkan respons entitas dan dari sini mendapatkan objek InputStream untuk mendapatkan html halaman.

Saya kemudian melakukan balas dengan melakukan sebagai berikut:

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

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

Namun ini sangat lambat.

Apakah ini tidak efisien? Saya tidak memuat halaman web besar - www.cokezone.co.uk sehingga ukuran file tidak besar. Apakah ada cara yang lebih baik untuk melakukan ini?

Terima kasih

Andy

RenegadeAndy
sumber
Kecuali jika Anda benar-benar menguraikan baris, itu tidak masuk akal untuk membaca baris demi baris. Saya lebih suka membaca char by char melalui buffer ukuran tetap: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Jawaban:

355

Masalah dalam kode Anda adalah membuat banyak Stringobjek berat , menyalin kontennya dan melakukan operasi padanya. Sebagai gantinya, Anda harus menggunakan StringBuilderuntuk menghindari membuat Stringobjek baru di setiap append dan untuk menghindari menyalin array char. Implementasi untuk kasus Anda akan menjadi seperti ini:

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

Anda sekarang dapat menggunakan totaltanpa mengubahnya menjadi String, tetapi jika Anda membutuhkan hasilnya sebagai String, cukup tambahkan:

Hasil string = total.toString ();

Saya akan mencoba menjelaskannya dengan lebih baik ...

  • a += b(atau a = a + b), di mana adan bsekarang, menyalin konten keduanya a dan b ke objek baru (perhatikan bahwa Anda juga menyalin a, yang berisi akumulasi String ), dan Anda melakukan salinan itu pada setiap iterasi.
  • a.append(b), di mana aa StringBuilder, langsung menambahkan bkonten a, sehingga Anda tidak menyalin string yang terakumulasi di setiap iterasi.
Jaime Soriano
sumber
23
Untuk poin bonus, berikan kapasitas awal untuk menghindari realokasi karena StringBuilder mengisi: StringBuilder total = new StringBuilder(inputStream.available());
dokkaebi
10
Tidakkah ini memotong karakter baris baru?
Nathan Schwermann
5
jangan lupa untuk membungkus while dalam try / catch seperti ini: coba {while ((line = r.readLine ())! = null) {total.append (line); }} catch (IOException e) {Log.i (tag, "masalah dengan readline di fungsi inputStreamToString"); }
botbot
4
@botbot: Masuk dan mengabaikan pengecualian tidak jauh lebih baik daripada hanya mengabaikan pengecualian ...
Matti Virkkunen
50
Sungguh menakjubkan bahwa Android tidak memiliki konversi stream-to-string bawaan. Memiliki setiap cuplikan kode di web dan aplikasi di planet ini menerapkan kembali readlineloop adalah konyol. Pola itu seharusnya mati dengan kacang hijau di tahun 70-an.
Edward Brey
35

Sudahkah Anda mencoba metode bawaan untuk mengonversi aliran ke string? Itu bagian dari perpustakaan Apache Commons (org.apache.commons.io.IOUtils).

Maka kode Anda adalah baris ini:

String total = IOUtils.toString(inputStream);

Dokumentasi untuk itu dapat ditemukan di sini: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

Perpustakaan IO Apache Commons dapat diunduh dari sini: http://commons.apache.org/io/download_io.cgi

Makotosan
sumber
Saya menyadari ini adalah respons yang terlambat, tetapi baru saja kebetulan menemukan ini melalui pencarian Google.
Makotosan
61
API android tidak termasuk IOUtils
Charles Ma
2
Benar, itulah sebabnya saya menyebutkan perpustakaan eksternal yang memilikinya. Saya menambahkan perpustakaan ke proyek Android saya dan membuatnya mudah dibaca dari stream.
Makotosan
di mana saya bisa mengunduh ini, dan bagaimana Anda mengimpor itu ke proyek Android Anda?
safari
3
Jika Anda harus mengunduhnya, saya tidak akan menyebutnya "bawaan"; namun, saya baru saja mengunduhnya, dan akan mencobanya.
B. Clay Shannon
15

Kemungkinan lain dengan Jambu Biji:

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

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

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

Saya percaya ini cukup efisien ... Untuk mendapatkan sebuah String dari InputStream, saya akan memanggil metode berikut:

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

Saya selalu menggunakan UTF-8. Anda tentu saja dapat menetapkan rangkaian karakter sebagai argumen, selain InputStream.

Budimir Grom
sumber
6

Bagaimana dengan ini. Tampaknya memberikan kinerja yang lebih baik.

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

Sunting: Sebenarnya ini mencakup steelbytes dan Maurice Perry

Adrian
sumber
Masalahnya adalah - Saya tidak tahu ukuran hal yang saya baca sebelum saya mulai - jadi mungkin perlu beberapa bentuk array yang tumbuh juga. Kecuali Anda dapat meminta InputStream atau URL melalui http untuk mencari tahu seberapa besar hal yang saya ambil adalah untuk mengoptimalkan ukuran array byte. Saya harus efisien karena pada perangkat seluler yang merupakan masalah utama! Namun terima kasih untuk ide itu - Akan mencobanya malam ini dan beri tahu Anda cara kerjanya dalam hal perolehan kinerja!
RenegadeAndy
Saya tidak berpikir ukuran aliran masuk itu penting. Kode di atas membaca 1000 byte pada suatu waktu tetapi Anda dapat menambah / mengurangi ukuran itu. Dengan pengujian saya itu tidak membuat banyak perbedaan cuaca saya menggunakan 1000/10000 byte. Itu hanya aplikasi Java sederhana. Mungkin lebih penting pada perangkat seluler.
Adrian
4
Anda bisa berakhir dengan entitas Unicode yang dipotong menjadi dua bacaan berikutnya. Lebih baik membaca sampai semacam karakter batas, seperti \ n, yang persis seperti yang BufferedReader lakukan.
Jacob Nordfalk
4

Mungkin agak lebih cepat daripada jawaban Jaime Soriano, dan tanpa masalah pengkodean multi-byte dari jawaban Adrian, saya sarankan:

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
sumber
Bisakah Anda jelaskan mengapa lebih cepat?
Akhil Dad
Itu tidak memindai input untuk karakter baris baru, tetapi hanya membaca potongan 1024 byte. Saya tidak berpendapat ini akan membuat perbedaan praktis.
heiner
ada komentar di atas jawaban @Ronald? Dia melakukan hal yang sama tetapi untuk potongan yang lebih besar sama dengan ukuran inputStream. Juga betapa berbedanya jika saya memindai array char daripada byte array sebagai jawaban Nikola? Sebenarnya saya hanya ingin tahu pendekatan mana yang terbaik dalam hal ini? Juga readLine menghapus \ n dan \ r tetapi saya melihat bahkan kode aplikasi google io yang mereka gunakan readline
Akhil Dad
3

Mungkin lebih baik daripada membaca 'satu baris pada satu waktu' dan bergabung dengan string, coba 'baca semua yang tersedia' untuk menghindari pemindaian untuk akhir baris, dan juga untuk menghindari string bergabung.

yaitu, InputStream.available()danInputStream.read(byte[] b), int offset, int length)

SteelBytes
sumber
Hmm. jadi akan seperti ini: int offset = 5000; Byte [] bArr = Byte baru [100]; Byte [] total = Byte [5000]; while (InputStream.available) {offset = InputStream.read (bArr, offset, 100); untuk (int i = 0; i <offset; i ++) {total [i] = bArr [i]; } bArr = Byte baru [100]; } Apakah itu benar-benar lebih efisien? Tolong beri contoh!
RenegadeAndy
2
tidak, tidak, tidak, tidak, maksud saya cukup {byte total [] = baru [instrm.available ()]; instrm.read (total, 0, total.length); } dan jika Anda membutuhkannya sebagai sebuah String, gunakan {String asString = String (total, 0, total.length, "utf-8"); // anggap utf8 :-)}
SteelBytes
2

Membaca satu baris teks pada satu waktu, dan menambahkan baris kata ke string secara individual memakan waktu baik dalam mengekstraksi setiap baris dan overhead dari begitu banyak doa metode.

Saya bisa mendapatkan kinerja yang lebih baik dengan mengalokasikan byte array berukuran layak untuk menampung data stream, dan yang secara iteratif diganti dengan array yang lebih besar bila diperlukan, dan mencoba membaca sebanyak yang bisa disimpan oleh array.

Untuk beberapa alasan, Android berulang kali gagal mengunduh seluruh file ketika kode menggunakan InputStream yang dikembalikan oleh HTTPUrlConnection, jadi saya harus menggunakan BufferedReader dan mekanisme timeout linting tangan untuk memastikan saya mendapatkan seluruh file atau membatalkan 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();
    }
}

EDIT: Ternyata jika Anda tidak perlu menyandikan ulang konten (yaitu, Anda menginginkan konten SEBAGAIMANA ADANYA ), Anda tidak boleh menggunakan subkelas Pustaka apa pun. Cukup gunakan subkelas Stream yang sesuai.

Ganti awal metode sebelumnya dengan garis yang sesuai di bawah ini untuk mempercepatnya 2 hingga 3 kali lipat .

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];
Huperniket
sumber
Ini jauh lebih cepat daripada jawaban di atas dan diterima. Bagaimana Anda menggunakan "Reader" dan "Stream" di android?
SteveGSD
1

Jika file panjang, Anda dapat mengoptimalkan kode dengan menambahkan ke StringBuilder alih-alih menggunakan penggabungan String untuk setiap baris.

Maurice Perry
sumber
Tidak terlalu lama untuk jujur ​​- ini adalah sumber halaman situs web www.cokezone.co.uk - jadi benar-benar tidak terlalu besar Jelas kurang dari 100kb.
RenegadeAndy
Adakah yang punya ide lain tentang bagaimana ini bisa dibuat lebih efisien - atau jika ini bahkan tidak efisien !? Jika yang terakhir ini benar - mengapa perlu waktu lama? Saya tidak percaya hubungannya dengan kesalahan.
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
sumber
1

Untuk mengonversi InputStream ke String, kami menggunakan metode BufferedReader.readLine () . Kami beralih sampai BufferedReader mengembalikan nol yang berarti tidak ada lagi data untuk dibaca. Setiap baris akan ditambahkan ke StringBuilder dan dikembalikan sebagai 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();
    }
}`

Dan akhirnya dari kelas mana pun di mana Anda ingin mengkonversi panggilan fungsi

String dataString = Utils.convertStreamToString(in);

lengkap

poudel yubaraj
sumber
-1

Saya terbiasa membaca data lengkap:

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