Я хотел бы разработать систему для загрузки малых и больших файлов, которая будет очень надежной, то есть устойчивой к ошибкам сети и способной возобновить загрузку, как только это позволят условия сети, и полностью прозрачным для пользователя способом.
Однако это противоречит определенным ограничениям, налагаемым кроссплатформенным подходом. Я не уверен, что фоновая выборка является наиболее подходящим решением для загрузки тяжелого мультимедийного контента, и я не знаю, фиксируются ли сетевые ошибки в фоновой выборке общим обработчиком ошибок. Может быть, я изучу это.
Я разработал решение, которое имеет плюсы и минусы, и оно позволяет обойти проблему.
Плюсы: всегда позволяет вам завершить загрузку, даже если очень тяжелый (например, 100 МБ), даже если соединение нестабильно (сетевые ошибки) и даже если приложение временно работает в фоновом режиме
Минусы: поскольку моя идея основана на разделении загрузки на мелкие части, этот подход вызывает множество запросов GET, которые немного замедляют загрузку и вызывают больше c трафика, чем обычно требуется.
Предварительное условие 1: при обработке ошибок глобальной сети должен быть c .retry()
автомат, как в этом коде : Различие guish между ошибками на стороне сервера и проблемами подключения
Предварительное условие 2: для реализации getFileSizeWithoutDownload(String url)
и Wrapper
см .: { ссылка }
Пояснение: код не требует пояснений. Обычно он загружает 512 Кбайт за раз, а затем объединяет их с выводом. Если происходит сетевая ошибка (и если на iOS приложение переходит в фоновый режим), все, что уже было загружено, не теряется (теряется максимум только последний фрагмент размером 512 Кбайт). По мере загрузки каждого фрагмента ConnectionRequest вызывает себя, изменяя заголовок для частичной загрузки. Обратный вызов filesavedCallback
вызывается только после завершения всей загрузки.
Код:
public static void downloadToStorage(String url, OnComplete<Integer> percentageCallback, OnComplete<String> filesavedCallback) throws IOException {
final String output = getNewStorageFilename(); // get a new random available Storage file name
final long fileSize = getFileSizeWithoutDownload(url); // total expected download size
final int splittingSize = 512 * 1024; // 512 kbyte, size of each small download
Wrapper<Integer> downloadedTotalBytes = new Wrapper<>(0);
OutputStream out = Storage.getInstance().createOutputStream(output); // leave it open to append partial downloads
Wrapper<Integer> completedPartialDownload = new Wrapper<>(0);
ConnectionRequest cr = new GZConnectionRequest();
cr.setUrl(url);
cr.setPost(false);
if (fileSize > splittingSize) {
// Which byte should the download start from?
cr.addRequestHeader("Range", "bytes=0-" + splittingSize);
cr.setDestinationStorage("split-" + output);
} else {
Util.cleanup(out);
cr.setDestinationStorage(output);
}
cr.addResponseListener(a -> {
CN.callSerially(() -> {
try {
// We append the just saved partial download to the output, if it exists
if (Storage.getInstance().exists("split-" + output)) {
InputStream in = Storage.getInstance().createInputStream("split-" + output);
Util.copyNoClose(in, out, 8192);
Util.cleanup(in);
Storage.getInstance().deleteStorageFile("split-" + output);
completedPartialDownload.set(completedPartialDownload.get() + 1);
}
// Is the download finished?
if (fileSize <= 0 || completedPartialDownload.get() * splittingSize >= fileSize || downloadedTotalBytes.get() >= fileSize) {
// yes, download finished
Util.cleanup(out);
filesavedCallback.completed(output);
} else {
// no, it's not finished, we repeat the request after updating the "Range" header
cr.addRequestHeader("Range", "bytes=" + downloadedTotalBytes.get() + "-" + (downloadedTotalBytes.get() + splittingSize));
NetworkManager.getInstance().addToQueue(cr);
}
} catch (IOException ex) {
Log.p("Error in appending splitted file to output file", Log.ERROR);
Log.e(ex);
Server.sendLogAsync();
}
});
});
NetworkManager.getInstance().addToQueue(cr);
NetworkManager.getInstance().addProgressListener((NetworkEvent evt) -> {
if (cr == evt.getConnectionRequest() && fileSize > 0) {
downloadedTotalBytes.set(completedPartialDownload.get() * splittingSize + evt.getSentReceived());
// the following casting to long is necessary when the file is bigger than 21MB, otherwise the result of the calculation is wrong
percentageCallback.completed((int) ((long) downloadedTotalBytes.get() * 100 / fileSize));
}
});
}
Я пробовал это решение в симуляторе на Android и iOS в различные сетевые условия, при загрузке 100 МБ и периодическом перемещении приложения в фоновом режиме (или разрешении go автоматически). Во всех случаях приложение успешно завершает загрузку. Однако различия между Android и iOS остаются, когда приложение работает в фоновом режиме.
Надеюсь, это полезно. Если кто-то хочет еще больше улучшить этот код, он может добавить еще один ответ :)