...

вторник, 5 ноября 2013 г.

[Из песочницы] Быстрая десериализация действительно больших JSON-ответов

Под катом находится небольшое, но полезное описание того, как быстро и просто превратить пришедший JSON-ответ в набор объектов. Никакого ручного парсинга. А если вы сталкивались с OutOfMemory проблемой на старых смартфонах – и для этого есть решение, поддерживающее Android 2.X версий.

Кроме того, под катом будет ссылка на репозиторий на гитхабе с примером кода. А вот картинок не будет, зато найдётся место для небольшой таблички.



Итак, на текущем проекте у меня возникла необходимость парсить ответ сервиса, состоящий из пачки вложенных друг в друга объектов, внутри которых могли быть объекты, внутри которых… Данные были в формате 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;
}
}





Обёртка вокруг Log и метод для оформления размера файла


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 МБ400035 секунд2 секунды
13.3 МБ90001 минута 11 секунд5 секунд



Пример использования – на гитхабе, тестовые файлы там же.

Ссылка на библиотеку Gson.

Если кому будет интересна тема разработки под андроид, то впереди как минимум посты о push-нотификациях (серверная и клиентская сторона – на хабре были статьи на эту тему, но они все несколько устарели), о работе с базой и иные на тему разработки под Android.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий