Кроме того, под катом будет ссылка на репозиторий на гитхабе с примером кода. А вот картинок не будет, зато найдётся место для небольшой таблички.
Итак, на текущем проекте у меня возникла необходимость парсить ответ сервиса, состоящий из пачки вложенных друг в друга объектов, внутри которых могли быть объекты, внутри которых… Данные были в формате JSON, кроме того, было использовано gzip-сжатие сервером, всё-таки разница в размере переданных данных была значительна (4 мегабайте против 300 килобайт в сжатом виде – для мобильной связи это не шутка).
Как человеку ленивому, парсить руками каждое поле и объект мне было совсем не с руки… Таким образом, была задействована библиотека Gson, судя по тестом – быстрейший десериализатор из формата JSON. Ну а теперь, приступим, и начнём сразу с кода. Для простоты весь вывод ведём в консоль, что бы не думать о вьюшках и прочем.
Вот так выглядят объекты, которые прилетают нам из сети:
public class HumorItem { public String text; public String url; } public class HumorItems { List<HumorItem> Items; //тут может быть больше списков, и не только списки, для примера упростим. }
А вот так – код, который его скачивает и десериализует.
public class LoadData extends AsyncTask<Void, Void, Void> { String _url=""; public LoadData(String url){ _url=url; } @Override protected Void doInBackground(Void... voids) { try { //скачивание данных HttpClient httpclient = new DefaultHttpClient(); HttpPost httppost = new HttpPost(_url); HttpResponse response = httpclient.execute(httppost); HttpEntity httpEntity=response.getEntity(); InputStream stream = AndroidHttpClient.getUngzippedContent(httpEntity); //для скачивания gzip-нутых данных BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream)); StringBuilder responseBuilder= new StringBuilder(); char[] buff = new char[1024*512]; int read; while((read = bufferedReader.read(buff)) != -1) { responseBuilder.append(buff, 0, read) ; Log.d("скачано " + PrepareSize(responseBuilder.length())); } //парсинг полученных данных HumorItems list= Gson.fromJson(responseBuilder.toString(),HumorItems.class); //тестовый вывод for (HumorItem message:list.Items){ Log.d("Текст: "+message.text); Log.d("Ссылка: "+message.url); Log.d("-------------------"); } Log.d("ВСЕГО СКАЧАНО "+list.Items.size()); } catch (IOException e) { e.printStackTrace(); Log.e("ошибка "+e.getMessage()); } return null; } }
public class Log { public static final String TAG="hhh"; public static void d(String text){ android.util.Log.d(TAG,text); } public static void e(String text){ android.util.Log.e(TAG,text); } } public String PrepareSize(long size){ if (size<1024){ return size+" б."; }else { return size/1024+" кб."; } }
И это решение отлично работало! До поры до времени. Ответ для одной из комбинации параметров весил порядка 8 мегабайт. При тестировании на части телефонов – программа падала, где на пятом скачанном мегабайте, где на третьем.
Гугл подсказал сначала простое решение — выставить largeHeap в фале AndroidManifest.
<application [...] android:largeHeap="true">
Этот параметр позволяет приложению выделить под себя больше оперативной памяти. Вариант конечно ленивый и простой, но телефонами на Android ниже 3й версии не поддерживается. Да и в целом подход какой-то пораженческий – “зачем оптимизировать, если можно купить ещё железа?”
Далее, после нескольких попыток был выбран такой, простой вариант:
- Не наполняем файлом переменную, нет – скачиваем данные непосредственно на флешку (ну или внутреннюю память, что под руку подвернётся).
- Натравливаем Gson на этот файл. Проблема в парсинге и занимаемой файлом памяти не возникает.
Сказано-сделано:
public class LoadBigDataTmpFile extends AsyncTask<Void, Void, Void> { String _url=""; File cache_dir; public LoadBigDataTmpFile(String url){ _url=url; cache_dir = getExternalCacheDir(); } @Override protected Void doInBackground(Void... voids) { try { //скачивание данных HttpClient httpclient = new DefaultHttpClient(); HttpPost httppost = new HttpPost(_url); HttpResponse response = httpclient.execute(httppost); HttpEntity httpEntity=response.getEntity(); InputStream stream = AndroidHttpClient.getUngzippedContent(httpEntity); //нечто новое - открываем временный файл для записи BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream)); File file = new File(cache_dir, "temp_json_new.json"); if (file.exists()){ //если таковой уже есть - удаляем и создаём новый file.delete(); } file.createNewFile(); FileOutputStream fileOutputStream=new FileOutputStream(file,true); BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(fileOutputStream)); char[] buff = new char[1024*1024]; int read; long FullSize=0; while((read = bufferedReader.read(buff)) != -1) { bufferedWriter.write(buff,0,read); //запись в файл FullSize+=read; Log.d("скачано " + PrepareSize(FullSize)); } bufferedWriter.flush(); fileOutputStream.close(); //парсинг из файла Log.d("начали парсинг..."); FileInputStream fileInputStream=new FileInputStream(file); InputStreamReader reader = new InputStreamReader(fileInputStream); HumorItems list= Gson.fromJson(reader,HumorItems.class); Log.d("закончили парсинг."); /тестовый вывод for (HumorItem message:list.Items){ Log.d("Текст: "+message.text); Log.d("Ссылка: "+message.url); Log.d("-------------------"); } Log.d("ВСЕГО СКАЧАНО "+list.Items.size()); } catch (IOException e) { e.printStackTrace(); Log.e("ошибка "+e.getMessage()); } return null; } }
Вот и всего-то. Код проверен в боевых условиях, работает стабильно на ура. Впрочем, можно сделать ещё проще и обойтись без временного файла.
public class LoadBigData extends AsyncTask<Void, Void, Void> { String _url=""; public LoadBigData(String url){ _url=url; } @Override protected Void doInBackground(Void... voids) { try { //скачивание данных HttpClient httpclient = new DefaultHttpClient(); HttpPost httppost = new HttpPost(_url); HttpResponse response = httpclient.execute(httppost); HttpEntity httpEntity=response.getEntity(); InputStream stream = AndroidHttpClient.getUngzippedContent(httpEntity); //открывам потом на чтение данных BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream)); //и сразу направляем его в десериализатор InputStreamReader reader = new InputStreamReader(stream); HumorItems list= Gson.fromJson(reader,HumorItems.class); //тестовый вывод for (HumorItem message:list.Items){ Log.d("Текст: "+message.text); Log.d("Ссылка: "+message.url); Log.d("-------------------"); } Log.d("ВСЕГО СКАЧАНО "+list.Items.size()); } catch (IOException e) { e.printStackTrace(); Log.e("ошибка "+e.getMessage()); } return null; } }
Минус – не удастся контролировать процесс скачивания (прервать его адекватным способом), а так же – неизвестно, сколько уже скачано данных. Красивый прогресс-бар не нарисуешь.
Есть ещё один вариант, приведённый в документации, позволяющий последовательно вытаскивать объекты и тут же их обрабатывать, но с ним проблематично работать, если у вас объект разных массивов объектов, а не просто массив однотипных. Впрочем, если у вас есть красивое решение – с удовольствием увижу его в комментариях, и обязательно включу в статью в update’е!
В качестве бонуса – немного статистики.
Размер файла | Число объектов внутри | Время десериализации на эмуляторе | Время десериализации на Highscreen Boost |
5.79 МБ | 4000 | 35 секунд | 2 секунды |
13.3 МБ | 9000 | 1 минута 11 секунд | 5 секунд |
Пример использования – на гитхабе, тестовые файлы там же.
Ссылка на библиотеку Gson.
Если кому будет интересна тема разработки под андроид, то впереди как минимум посты о push-нотификациях (серверная и клиентская сторона – на хабре были статьи на эту тему, но они все несколько устарели), о работе с базой и иные на тему разработки под Android.
ссылка на оригинал статьи http://habrahabr.ru/post/200898/
Добавить комментарий