Передача прочитанной ФД посылочной: когда закрывать?

Чтобы передать большие объемы данных через связующее, мы создаем канал, затем передаем конец канала для чтения по связующему как ParcelFileDescriptor и запускаем поток для записи данных в конец канала для записи. В основном это выглядит так:

  public void writeToParcel(Parcel out, int flags) {
    ParcelFileDescriptor[] fds;
    try {
      fds = ParcelFileDescriptor.createPipe();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    out.writeParcelable(fds[0], 0);
    byte[] bytes = ...; // Marshall object data to bytes
    write(bytes, fds[1]); // Starts a thread to write the data
  }

Принимающая сторона считывает данные со стороны чтения канала. Это выглядит так:

ParcelFileDescriptor readFd = in.readFileDescriptor();

FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(readFd);
ByteArrayOutputStream out = new ByteArrayOutputStream();

byte[] b = new byte[16 * 1024];
int n;

try {
  while ((n = fis.read(b)) != -1) {
    out.write(b, 0, n);
  }
} catch (IOException e) {
  throw new RuntimeException(e);
} finally {
  try {
    Log.i(TAG, "Closing read file descriptor..."); // I see this
    fis.close();
    Log.i(TAG, "Closed read file descriptor"); // And I see this
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Это работает, но когда включен строгий режим, мы вылетаем с этим:

 01-03 14:26:48.099 E/StrictMode(25346): A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks. 
 01-03 14:26:48.099 E/StrictMode(25346): java.lang.Throwable: Explicit termination method 'close' not called 
 01-03 14:26:48.099 E/StrictMode(25346):    at dalvik.system.CloseGuard.open(CloseGuard.java:223) 
 01-03 14:26:48.099 E/StrictMode(25346):    at android.os.ParcelFileDescriptor.<init>(ParcelFileDescriptor.java:192) 
 01-03 14:26:48.099 E/StrictMode(25346):    at android.os.ParcelFileDescriptor.<init>(ParcelFileDescriptor.java:181) 
 01-03 14:26:48.099 E/StrictMode(25346):    at android.os.ParcelFileDescriptor.createPipe(ParcelFileDescriptor.java:425) 
 01-03 14:26:48.099 E/StrictMode(25346):    at com.clover.sdk.FdParcelable.writeToParcel(FdParcelable.java:118) 

Строка 118 — это создание канала (ParcelFileDescriptor.createPipe()).

Таким образом, кажется, что отправителю необходимо закрыть конец чтения, а также конец записи. Моя проблема в том, что я не знаю, когда я смогу закрыть конец чтения, так как я не знаю, когда читатель закончит чтение.

Что мне не хватает?


person Jeffrey Blattman    schedule 03.01.2019    source источник


Ответы (1)


  1. Как только вы закончите писать, закройте выходной поток.

  2. Потребитель всегда несет ответственность за закрытие своего входного потока, как только он закончит чтение. Это не ваша ответственность (если только вы не выступаете в роли потребителя).

То, что вы описываете, аналогично тому, как я открываю FileOutputStream (я использую API) и ожидаю, что среда выполнения закроет его для меня только потому, что я не закрыл его явно, когда закончил его использовать.

Поэтому отправьте FD в Parcel, и тот, кто его использует, несет ответственность за его закрытие. Они могли бы использовать что-то вроде этого:

val fd = parcel.readFileDescriptor()
val input = ParcelFileDescriptor.AutoCloseInputStream(fd)
// Use input. When #close() is called it will also close the FD.

См.: https://developer.android.com/reference/android/os/ParcelFileDescriptor.AutoCloseInputStream

Вы можете скрыть эту реализацию в своей клиентской библиотеке SDK. В любом случае хорошо задокументируйте это для потребителей.

person Eugen Pechanec    schedule 03.01.2019
comment
Спасибо. Да, я делаю это. Я все еще получаю исключение строгого режима на стороне службы. Я обновлю свой ответ, чтобы показать код на стороне получателя. - person Jeffrey Blattman; 04.01.2019
comment
Кроме того, я смотрю на внедрение ParcelFileDescriptor. Closeguard срабатывает в финализаторе этого класса. Он не делает никаких проверок относительно закрытия другой стороны. Это просто проверка, не вызвал ли кто-то явным образом close() для этого экземпляра ParcelFileDescriptor. - person Jeffrey Blattman; 04.01.2019
comment
Таким образом, ссылка на стороне сервера является сборщиком мусора, в то время как фактический дескриптор файла все еще используется и действителен на стороне клиента. Вам нужно будет сохранить сильную ссылку на FD, переданный в Parcel. И мы вернулись к проблеме незнания того, когда чтение закончено. Вам нужно будет периодически проверять на стороне сервера, действителен ли базовый FD. И это просто недостаточно хорошее решение, по крайней мере, в моей книге. - person Eugen Pechanec; 04.01.2019
comment
Должно быть безопасно удалять ссылки FD, когда служба не привязана... на самом деле, в этот момент вы все равно должны закрыть все FD. Поэтому сохраняйте набор доставленных FD, закрывайте и удаляйте их все в onUnbind и, возможно, фильтруйте набор по getFileDescriptor().valid() в каждом onBind. - person Eugen Pechanec; 04.01.2019
comment
Я сделал тест, в котором я просто закрыл чтение FD на стороне отправителя после того, как запись в запись FD возвращается. Кажется, это работает и не вызывает нарушения строгого режима. Вот некоторая информация об отладке: pastebin.com/hbRm0D4m. Вы можете видеть, что закрытие на стороне получателя (после завершения чтения) происходит в ту же миллисекунду, что и закрытие на стороне отправителя прочитанного FD. Я тестировал с размером данных от ~ 1k до ~ 100k. Однако я беспокоюсь, что это работает из-за определенного времени. Нужно больше думать. - person Jeffrey Blattman; 04.01.2019
comment
Одна из теорий заключается в том, что отправка записи не возвращается до тех пор, пока получатель не закончит чтение. В этом случае можно закрыть FD чтения на стороне отправки в этот момент. - person Jeffrey Blattman; 04.01.2019
comment
Вот что я нашел: согласно примеру канала c процесс следует закрыть ненужные FD. Это означает, что ваш сервис должен закрыть пишущий FD после записи данных, закрыть читающий FD после записи его в посылку и доставки этой посылки другому процессу, а ваш клиент должен закрыть читающий FD, который он получил после чтения . Вы можете проверить дальше? - person Eugen Pechanec; 04.01.2019
comment
Липкая часть после доставки в другой процесс. Я действительно не знаю, когда получатель получает его. Я думаю, что мой тест работает, потому что я не закрываю его, пока отправитель не закончит писать, и это, похоже, связано с чтением получателя, что, очевидно, означает, что получатель получил FD. Если вы хотите поместить то, что вы узнали, в ответ, я приму это. - person Jeffrey Blattman; 04.01.2019
comment
@JeffreyBlattman Я действительно не знаю, когда получатель его получает - если вы используете блокировку вызовов Binder (по умолчанию), получатель всегда будет получать FD к моменту возврата вызова. Вы можете использовать Parcelable.PARCELABLE_WRITE_RETURN_VALUE, чтобы сделать его еще более явным. - person user1643723; 04.01.2019