Быстрая десериализация действительно больших 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 МБ 4000 35 секунд 2 секунды
13.3 МБ 9000 1 минута 11 секунд 5 секунд

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/200898/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *