Đọc Android từ luồng đầu vào hiệu quả


152

Tôi đang thực hiện một yêu cầu nhận HTTP đến một trang web cho một ứng dụng Android mà tôi đang thực hiện.

Tôi đang sử dụng DefaultHttpClient và sử dụng HttpGet để đưa ra yêu cầu. Tôi nhận được phản hồi thực thể và từ đó có được một đối tượng InputStream để lấy html của trang.

Sau đó tôi quay vòng trả lời như sau:

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

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

Tuy nhiên, điều này là chậm khủng khiếp.

Đây có phải là không hiệu quả? Tôi không tải một trang web lớn - www.cokezone.co.uk vì vậy kích thước tệp không lớn. Có cách nào tốt hơn để làm điều này?

Cảm ơn

Andy


Trừ khi bạn thực sự phân tích cú pháp các dòng, nó không có ý nghĩa gì khi đọc từng dòng một. Tôi thà đọc char bằng char thông qua bộ đệm kích thước cố định: gist.github.com/fkirc/a231c817d582e114e791b77bb33e30e9
Mike76

Câu trả lời:


355

Vấn đề trong mã của bạn là nó tạo ra nhiều Stringvật thể nặng , sao chép nội dung của chúng và thực hiện các thao tác trên chúng. Thay vào đó, bạn nên sử dụng StringBuilderđể tránh tạo các Stringđối tượng mới trên mỗi phụ lục và để tránh sao chép các mảng char. Việc thực hiện cho trường hợp của bạn sẽ giống như thế này:

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

Bây giờ bạn có thể sử dụng totalmà không cần chuyển đổi nó thành String, nhưng nếu bạn cần kết quả là một String, chỉ cần thêm:

Chuỗi kết quả = Total.toString ();

Tôi sẽ cố gắng giải thích nó tốt hơn ...

  • a += b(hoặc a = a + b), ở đâu ablà Chuỗi, sao chép nội dung của cả hai a b sang một đối tượng mới (lưu ý rằng bạn cũng đang sao chép a, có chứa phần tích lũy String ) và bạn đang thực hiện các bản sao đó trên mỗi lần lặp.
  • a.append(b), ở đâu aStringBuilder, trực tiếp nối thêm bnội dung vào a, vì vậy bạn không sao chép chuỗi tích lũy ở mỗi lần lặp.

23
Đối với các điểm thưởng, hãy cung cấp khả năng ban đầu để tránh việc tái phân bổ khi StringBuilder lấp đầy: StringBuilder total = new StringBuilder(inputStream.available());
dokkaebi

10
Điều này không cắt bỏ các ký tự dòng mới?
Nathan Schwermann

5
đừng quên gói lại trong khi thử / bắt như thế này: thử {while ((line = r.readLine ())! = null) {total.append (line); }} Catch (IOException e) {Log.i (tag, "vấn đề với đường đọc trong hàm inputStreamToString"); }
botbot

4
@botbot: Ghi nhật ký và bỏ qua một ngoại lệ không tốt hơn nhiều so với việc bỏ qua ngoại lệ ...
Matti Virkkunen

50
Thật đáng ngạc nhiên khi Android không có chuyển đổi chuỗi thành chuỗi tích hợp. Có mỗi đoạn mã trên web và ứng dụng trên hành tinh này thực hiện lại một readlinevòng lặp là vô lý. Mô hình đó đáng lẽ đã chết với hạt đậu xanh vào những năm 70.
Edward Brey

35

Bạn đã thử phương thức dựng sẵn để chuyển đổi một luồng thành một chuỗi chưa? Đây là một phần của thư viện Apache Commons (org.apache.commons.io.IOUtils).

Sau đó, mã của bạn sẽ là một dòng này:

String total = IOUtils.toString(inputStream);

Tài liệu cho nó có thể được tìm thấy ở đây: http://commons.apache.org/io/api-1.4/org/apache/commons/io/IOUtils.html#toString%28java.io.InputStream%29

Có thể tải xuống thư viện IO Commons IO từ đây: http://commons.apache.org/io/doad_io.cgi


Tôi nhận ra đây là một phản hồi muộn, nhưng vừa mới tình cờ phát hiện ra điều này thông qua một tìm kiếm Google.
Makotosan

61
API android không bao gồm IOUtils
Charles Ma

2
Phải, đó là lý do tại sao tôi đề cập đến thư viện bên ngoài có nó. Tôi đã thêm thư viện vào dự án Android của mình và nó giúp bạn dễ dàng đọc từ các luồng.
Makotosan

Tôi có thể tải cái này ở đâu và làm thế nào bạn nhập nó vào dự án Android của bạn?
safari

3
Nếu bạn phải tải xuống, tôi sẽ không gọi nó là "tích hợp"; tuy nhiên, tôi chỉ tải về nó và sẽ cho nó đi.
B. Clay Shannon

15

Một khả năng khác với Guava:

phụ thuộc: compile 'com.google.guava:guava:11.0.2'

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

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

9

Tôi tin rằng điều này đủ hiệu quả ... Để có được Chuỗi từ InputStream, tôi sẽ gọi phương thức sau:

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

Tôi luôn sử dụng UTF-8. Tất nhiên, bạn có thể đặt bộ ký tự làm đối số, bên cạnh InputStream.


6

Cái này thì sao. Có vẻ để cung cấp hiệu suất tốt hơn.

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

Chỉnh sửa: Trên thực tế, loại này bao gồm cả Steelbyte và Maurice Perry


Vấn đề là - tôi không biết kích thước của thứ tôi đang đọc trước khi tôi bắt đầu - vì vậy có thể cần một số dạng phát triển mảng. Ngoài ra, bạn có thể truy vấn InputStream hoặc URL thông qua http để tìm hiểu mức độ lớn mà tôi đang truy xuất để tối ưu hóa kích thước của mảng byte. Tôi phải làm việc hiệu quả như trên thiết bị di động, đây là vấn đề chính! Tuy nhiên, cảm ơn vì ý tưởng đó - Sẽ cho nó một shot tối nay và cho bạn biết nó xử lý như thế nào về hiệu suất đạt được!
RenegadeAndy

Tôi không nghĩ rằng kích thước của luồng đến là quan trọng. Đoạn mã trên đọc 1000 byte mỗi lần nhưng bạn có thể tăng / giảm kích thước đó. Với thử nghiệm của tôi, nó không tạo ra sự khác biệt nhiều về thời tiết, tôi đã sử dụng 1000/10000 byte. Đó chỉ là một ứng dụng Java đơn giản. Nó có thể quan trọng hơn trên một thiết bị di động.
Adrian

4
Bạn có thể kết thúc với một thực thể Unicode được cắt thành hai lần đọc tiếp theo. Tốt hơn là đọc cho đến khi một số loại ký tự ranh giới, như \ n, đó chính xác là những gì BufferedReader làm.
Jacob Nordfalk

4

Có thể nhanh hơn một chút so với câu trả lời của Jaime Soriano và không có vấn đề mã hóa nhiều byte trong câu trả lời của Adrian, tôi đề nghị:

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

Bạn có thể giải thích tại sao nó sẽ nhanh hơn?
Bố Akhil

Nó không quét đầu vào cho các ký tự dòng mới, mà chỉ đọc các đoạn 1024 byte. Tôi không tranh luận điều này sẽ làm cho bất kỳ sự khác biệt thực tế.
heiner 2/2/2016

bất kỳ ý kiến ​​về câu trả lời @Ronald? Anh ta đang làm tương tự nhưng cho một đoạn lớn hơn bằng với kích thước inputStream. Ngoài ra nó khác nhau như thế nào nếu tôi quét mảng char chứ không phải mảng byte như câu trả lời của Nikola? Thật ra tôi chỉ muốn biết cách tiếp cận nào là tốt nhất trong trường hợp nào? Đồng thời readLine xóa \ n và \ r nhưng tôi thậm chí còn thấy mã ứng dụng google io mà họ đang sử dụng readline
Akhil Dad

3

Có thể sau đó đọc 'một dòng tại một thời điểm' và nối các chuỗi, thử 'đọc tất cả có sẵn' để tránh việc quét cho đến cuối dòng và để tránh tham gia chuỗi.

tức là InputStream.available()InputStream.read(byte[] b), int offset, int length)


Hừm. vì vậy nó sẽ như thế này: int offset = 5000; Byte [] bArr = new Byte [100]; Byte [] tổng = Byte [5000]; while (InputStream.av Available) {offset = InputStream.read (bArr, offset, 100); for (int i = 0; i <offset; i ++) {tổng [i] = bArr [i]; } bArr = Byte mới [100]; } Điều đó thực sự hiệu quả hơn - hay tôi đã viết nó tồi tệ! Xin cho một ví dụ!
RenegadeAndy

2
không không không không, ý tôi chỉ đơn giản là {byte tổng [] = new [Bologm.av Available ()]; Barsm.read (tổng, 0, Total.length); } và nếu sau đó bạn cần nó dưới dạng Chuỗi, hãy sử dụng {String asString = String (Total, 0, total.length, "utf-8"); // giả sử utf8 :-)}
SteelBytes

2

Đọc một dòng văn bản tại một thời điểm và nối thêm dòng đã nói vào một chuỗi riêng lẻ tốn thời gian cả trong việc trích xuất từng dòng và chi phí của rất nhiều cách gọi phương thức.

Tôi đã có thể có được hiệu suất tốt hơn bằng cách phân bổ một mảng byte có kích thước phù hợp để giữ dữ liệu luồng và được thay thế bằng một mảng lớn hơn khi cần và cố gắng đọc càng nhiều mảng càng tốt.

Vì một số lý do, Android liên tục không tải xuống được toàn bộ tệp khi mã được sử dụng InputStream được trả về bởi HTTPUrlConnection, vì vậy tôi đã phải sử dụng cả BufferedReader và cơ chế hết thời gian cuộn bằng tay để đảm bảo tôi sẽ lấy toàn bộ tệp hoặc hủy việc chuyển nhượng.

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: Hóa ra nếu bạn không cần phải mã hóa lại nội dung (nghĩa là bạn muốn nội dung NHƯ VẬY ) thì bạn không nên sử dụng bất kỳ lớp con Reader nào. Chỉ cần sử dụng lớp con Stream thích hợp.

Thay thế bắt đầu của phương pháp trước bằng các dòng tương ứng sau để tăng tốc thêm 2 đến 3 lần .

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

Điều này nhanh hơn nhiều so với các câu trả lời ở trên và được chấp nhận. Làm thế nào để bạn sử dụng "Reader" và "Stream" trên Android?
SteveGSD

1

Nếu tệp dài, bạn có thể tối ưu hóa mã của mình bằng cách nối thêm StringBuilder thay vì sử dụng nối chuỗi cho mỗi dòng.


Thành thật không lâu - đó là nguồn trang của trang web www.cokezone.co.uk - vì vậy thực sự không lớn lắm. Chắc chắn ít hơn 100kb.
RenegadeAndy

Có ai có bất kỳ ý tưởng nào khác về cách điều này có thể được thực hiện hiệu quả hơn - hoặc nếu điều này thậm chí không hiệu quả!? Nếu điều sau là đúng - tại sao nó lại mất nhiều thời gian? Tôi không tin rằng kết nối là để đổ lỗi.
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

Để chuyển đổi InputStream thành String, chúng tôi sử dụng phương thức BufferedReader.readLine () . Chúng tôi lặp đi lặp lại cho đến khi BufferedReader trả về null, điều đó có nghĩa là không còn dữ liệu để đọc. Mỗi dòng sẽ được thêm vào StringBuilder và được trả lại dưới dạng 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();
    }
}`

Và cuối cùng từ bất kỳ lớp nào mà bạn muốn chuyển đổi hãy gọi hàm

String dataString = Utils.convertStreamToString(in);

hoàn thành


-1

Tôi đang sử dụng để đọc dữ liệu đầy đủ:

// inputStream is one instance InputStream
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
String dataString = new String(data);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.