Секрет Parcelable

Секрет Parcelable

Askhar Aydarov

Казалось бы, кто в 2024 году будет сравнивать Serializable и Parcelable? Уже ведь все ясно, так как написали кучу статей, на Github лежат примеры сравнения скорости работы и даже есть крутой доклад с подробнейшим разбором.

Но, пока я пробовал в предыдущем посте передавать большие данные между процессами, обнаружил довольно интересную вещь.

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

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

Для этого:

  1. Написал тесты для измерения скорости чтения, записи. А что насчет размера? Его будем определять по размеру файла, в которую происходит запись.
  2. В качестве тестовых данных взял списки разного размера со String и Long и обернул их в классы, которые реализуют Parcelable или Serializable

Код тестов можно посмотреть тут.

По результатам тестов я получил, что уже на 10 строках Parcelable начинает проигрывать, а разница стремительно растет. По итогу, если записать еще больше данных, то разница будет в среднем в два раза.

Причем на скорости чтения и записи это не отражается - Parcelable все равно быстрее.

Почему так происходит? Если заглянуть в файлы в hex редакторе, то можно заметить, что в Serializable символы занимают по 1 байту, когда как в Parcelable символ занимает 2 байта, где в 1 байте наш символ, а в другом нолики. Получается, что половина файла Parcelable это просто пустота и причиной этому является то, что Serializable записывает строки в UTF-8 кодировке, а Parcelable в UTF-16.

Символ в Serializable
Символ в Parcelable

То, что проблема из-за кодировки подтверждается еще тем, что если мы запишем списки с Long, то ситуация полностью изменится.

Ну и еще точнее причина проблемы подвержается исходным кодом.

В Parcel далеко ходить не надо - сразу видно, что метод проксирует вызов, где по названию очевидно, что используется UTF-16.

В ObjectOutputStream же в javadoc указывается, что используется модифицированный UTF-8.

Подробнее про кодировки можете почитать у Никиты. The Absolute Minimum Every Software Developer Must Know About Unicode in 2023 (Still No Excuses!)

Выбор кодировки довольно интересный вопрос. Понятное дело, что у нас в Android выгоднее использовать UTF-16, потому что много данных передается не в ASCII. Да и String с Char тоже кодируются 16 битами.

Но все бы хорошо, пока в 2020 году не появляются два коммита (один, два) с общим описанием:

Ребята решили попробовать добавить UTF-8 и получили прирост в скорости записи и чтения, так как уменьшился размер занимаемое памяти. Кроме того, прирост скорости был существенный даже для non-ASCII символов (тестировали на иероглифах китайских).

Захотелось попробовать поэкспериментировать, вызывая методы write/readString8 вместо write/readString.

Аналогично для read методов

Правда обнаружилось, что эти методы помечены как blocked api, используются в парочке кейсов в sdk, а доступ к ним получить в runtime не получится. Но в рамках тестирования, мы можем отключить запрет на доступ к заблокированным методам через adb:

adb shell settings put global hidden_api_policy  1 // включаем доступ
adb shell settings delete global hidden_api_policy // выключаем

По результатам получилось так, что размер довольно сильно сократился по сравнению с первоначальный вариантом. И несмотря на это, странно, что все еще уступает Serializable.

Если посмотрим в hex, то можно увидеть, что оптимизация сработала - символ данных занимает 1 байт. А вот для символов мета информации (например, название класса) которые определены в шапке файла, все еще используется UTF-16, так как на это мы уже повлиять не можем, потому что это чуть глубже по коду в Parcel.

Символ, записанный в Parcel в UTF-8

Но вот для non-ASCII символов ситуация не очень хорошая. Для кириллицы UTF-8 в Serializable показал себя лучше всего и особо не прибавил в размере (магия какая-то), а варианты с Parcelable как с UTF-8, так и с UTF-16 прибавили заметно. При этом 16 битный вариант чуть лучше, чем 8 битный, когда данных мало, но там совсем копейки, которые не имеют никакого влияния при больших размерах списков.

Для китайских иероглифов похожая ситуация. UTF-8 в Serializable вообще не поменялся, а UTF-8 и UTF-16 в Parcelable практически сравнялись.

Что же касается скорости, то там есть улучшения в пользу 8 битного варианта, но правда не такие, как в описании коммита, а просто стабильно немного лучше становится по мере роста количества данных.


Почему размер Serializable все равно меньше, чем Parcelable c UTF-8?

Действительно, что за магия? На самом деле все просто и гениально. Если снова заглянуть в файл через hex редактор, то можно заметить, что присутствует только одна строка hello, хотя записывалось 5.

Все дело в том, что при сериализации есть проверка на то, чтобы не записывать одно и то же значение повторно. Это регулируется булевой переменной unshared.

Потом, при наличии такого же объекта в таблице handles, в файл записывается ссылка на данные.

Подтвердить то, что в нашем случае были записаны ссылки на строку hello, можно по значению TC_REFERENCE. Значение 0x71 в ASCII таблице как раз таки соответствует символу q, который повторяется в hex отображении.

Эта оптимизация работает не только для строк, но и для любых объектов - например, для описания классов Class -, что действительно очень здорово. Если интересно, то можно попробовать написать реализацию Serializable, в которой будет выключена возможность объявлять ссылки на данные, но это скорее вредительство, которым мы заниматься не будем :)


Можно ли заменить UTF-16 на UTF-8 в Parcelable в AOSP?

Да, но это будет непросто из-за того, что сломается обратная совместимость. Со своей стороны закинул CL в AOSP, чтобы поднять вопрос об оптимизациях, потому что не нашел таких обсуждений в интернете. В итоге, быстро получит ответ, что были попытки, но пока все сложно...


Итог

Получается, что вопрос "Что выбрать: Parcelable или Serializable" еще не закрыт. И пока правильным ответом будет, что это зависит от задачи. Если важна скорость записи и чтения, то следует выбрать Parcelable. Если важен размер данных - например, для записи в файл -, то Serializable

Конечно, ситуация изменится, если будут использоваться алгоритмы сжатия, другие протоколы сериализации данных, но давайте оставим это вне рамок этого поста :)


P. S. К слову есть еще небольшая проблемка с примитивными типами Short, Char, Byte, Boolean - Parcel конвертирует их в Int, а вот Serializable умеет их обрабатывать и складывать в меньшее количество байт:

  • Для Char и Short 2 байта вместо 4
  • Для Byte и Boolean 1 байт вместо 4
Byte, записанный в Parcel

Но в размере Parcelable все равно выиграет, потому что Serializable докидывает сверх своей оптимизации больше метаданных, что нивелирует преимущество.


Все результаты и тесты


Опубликовано в Полуночные Зарисовки

Report Page