Об особенностях архитектуры Android глазами не-Android разработчика

    Недавно мы полностью переработали приложение Pyrus для Android. Первая версия приложения работала аж под Android 2.2. Отказавшись от поддержки Android ниже 4.1, мы смогли выплатить накопленный технический долг и заметно упростили исходный код. Да, мы потеряли часть пользователей (менее 1%), но зато мы сэкономили время разработчиков на исправление редких багов. Мы сможем инвестировать его в развитие функционала для всех текущих и новых пользователей. В долгосрочной перспективе это гораздо важнее.

    Здесь мы делимся опытом, который может быть полезен тем, кто подумывает начать разработку для платформы Android.

    В Android привлекают хорошие средства разработки, проверенный временем язык (Java) с привычным синтаксисом, огромная аудитория пользователей. Однако создавать удобные приложения на Android сложно: несмотря на развитый API часто бывает, что готового компонента с подходящим поведением нет. Приходится либо отказываться от задумок UX-дизайнера, либо соглашаться на зависимость от сторонних библиотек, либо писать самому. Не все архитектурные решения в Android оказались удачными и это также увеличивает объем кода без явной пользы.

    Умирающие и восстающие из пепла Activity


    Интерфейс в Android построен на Activity. Это контейнер, который обычно занимает целиком экран устройства, в нем живут остальные виджеты и он получает события о взаимодействии с пользователем.

    Когда пользователь активирует приложение, в Activity приходит событие onResume. Нажал кнопку Home — Activity исчезло, но перед этим ему пришло событие onPause. Пока все логично. Что происходит когда пользователь поворачивает экран устройства на 90 градусов? Ни за что не догадаетесь: ОС убивает Activity и создает заново! При удалении Activity удаляются все компоненты, которые она содержит. Это означает, что приходится писать специальный код, который сохраняет состояние виджетов внутри Activity (позицию скролла, выделенный кусок текста, состояние чекбоксов) и восстанавливает их после воссоздания родителя. Android делает часть этой работы за вас, но не всё.

    Сложно понять, что сподвигло архитекторов на подобное решение. Позже кто-то решил, что выглядит как-то некрасиво, и добавил таки параметр, который подавляет поведение системы по умолчанию. Теперь вместо пересоздания Activity ОС может вызывать специальный метод onConfigurationChanged. Это “решение” похоже на грязный хак и только усугубило проблему, ведь в документации Android сказано “эта техника должна рассматриваться как крайняя мера и не рекомендуется для большинства приложений”.

    Фрагментированные фрагменты


    Изначально ОС Android была создана для телефонов. С появлением планшетов команда Android справедливо решила, что те элементы интерфейса, которые раньше из-за маленьких размеров экрана телефона были разнесены по разным Activity, теперь могут одновременно сосуществовать на большом экране планшета. (Пример: список писем и электронное письмо на телефоне были в двух разных Activity, а на планшете — делят вместе один экран.) Да вот беда: несколько Activity не могут одновременно взаимодействовать с пользователем by design. Поэтому нужен другой механизм повторного использования компонентов, и выход был найден: появились фрагменты (Fragment).

    Согласно определению, Fragment представляет поведение или часть пользовательского интерфейса в Activity. Все ясно. Однако в инструкции по использованию фрагментов находим раздел “Добавление фрагмента, не имеющего пользовательского интерфейса”. ЧТОА? Оказывается, у фрагмента может принципиально не быть своего окна для отрисовки!

    А что же происходит с фрагментами при поворотах экрана? Разработчики Android подумали об этом — при пересоздании Activity все ее фрагменты также автоматически пересоздаются. Однако, при внимательном рассмотрении у фрагмента есть метод “setRetainInstance”, если вы его вызовете, то именно этот фрагмент не будет удаляться и восстанавливаться при поворотах.

    Стройной и непротиворечивой эту концепцию не назовешь.

    Асинхронная работа


    Поток обработки событий нельзя блокировать долгими операциями, поэтому обмен данными с диском и сетью должны делаться асинхронно в других потоках. Android предоставляет аж целых четыре механизма асинхронной работы: AsyncTask, Service, IntentService, Thread. Разработчикам предлагается самим разобраться, какой им лучше использовать. Выбор нетривиален: например, если приложение запустило AsyncTask, а пользователь во время его выполнения повернул экран, то по окончании работы AsyncTask не имеет возможности найти новую (созданную после поворота) Activity. И ни в одном из четырех механизмов не реализована простая логика: если пользователь запустил второй асинхронный процесс, не дождавшись результатов первого, а затем первый все-таки закончился раньше, то уместно игнорировать и не отражать в UI его результаты.

    Становится понятным появление многочисленных библиотек — шин данных для организации асинхронной работы внутри Android-приложения.

    Перезапуск приложения после OOM


    Если у Android заканчивается свободная оперативная память, ОС может завершить любой процесс. Когда в будущем пользователь активирует приложение, система постарается восстановить его состояние. По идее, это должно быть незаметно для разработчика (если он реализовал во всех компонентах метод onSaveInstanceState): система сама пересоздает стек Activity в том виде, как он был во время принудительной остановки. Однако, если инициализация требует времени (например, загрузка кэшей с диска или из сети), то правильно будет показать пользователю индикатор ожидания. Получается, при создании Activity все равно нужно отслеживать “холодный старт” и проводить инициализацию вручную. Не говоря уже о том, что при перезапуске не восстанавливаются никакие Thread и AsyncTask.

    Насколько вероятно столкнуться с нехваткой памяти на практике? Обычно такая ситуация возникает при обработке изображений высокого разрешения. Например, картинка 2048x1536 занимает 12Mb в памяти объекта Bitmap. Объем доступной приложению памяти зависит от конкретной модели устройства и иногда бывает совсем небольшим (64Mb или 128Mb). Поскольку garbage collector во всех версиях Android до 8.0 не был сжимающим (compacting), то даже если у вас есть 100Mb свободной памяти, но она разбита на блоки по 10Mb, то попытка выделить память под такую картинку приводила к аварийной остановке приложения.

    Вечная сборка мусора


    Однажды наш пользователь заметил, что при прокрутке длинных списков приложение “лагало” — каждые 2-3 секунды анимация останавливалась на короткие паузы (200-300 мс), и затем продолжалась. Анализ показал, что в приложении подозрительно часто запускается сборка мусора. Оказалось, что стандартный класс HashMap (который мы используем для получения объекта Java по его ID) в нашем случае неэффективен: нужно создавать объект-обертку на каждый ключ, являющийся целым числом (int). Поэтому количество аллокаций памяти увеличивается без какой-либо пользы. Решением было перейти на специальный контейнер SparseArray (есть только в Android, отсутствует в стандартной платформе Java), что сразу снизило давление на сборщик мусора.

    При чем тут лаги в анимации, спросите вы? Дело в том, что сборщик мусора в Android останавливал все нити во время своей работы, в том числе основную нить, занимающуюся отрисовкой. В версиях Android начиная c 5.0 используется другая виртуальная машина (ART вместо Dalvik) и другой алгоритм сборки мусора, делающий паузы в анимации реже и короче. (Про сравнение времени сборки мусора можно почитать здесь.)

    Просмотр документов


    Если вы хотите вставить в ваше приложение предпросмотр документов, вынуждены разочаровать: в Android нет встроенного компонента, который умеет показывать файлы Word, Excel, Powerpoint. Не говоря уже об архивах ZIP или документах PDF. Выход? Заставлять пользователя устанавливать сторонние приложения для просмотра каждого типа файла или использовать компонент WebView (фактически браузер). В обоих вариантах при тапе пользователя по файлу будет запускаться внешнее приложение и некоторые сценарии просто невозможно реализовать, например, ленту изображений: нет простого способа встроить WebView в стандартный ViewPager.

    Что дальше


    Некоторые исследования утверждают, что трудоемкость разработки под Android сравнительно высока. Наш опыт показывает, что грамотному разработчику легко привыкнуть к особенностям этой платформы. И хотя сегодня Android — самая популярная в мире мобильная ОС, через 5-10 лет ситуация может радикально поменяться.

    Последние годы компания Google секретно разрабатывает новую ОC Fuchsia с принципиально иной (по сравнению с современными ОС) моделью безопасности. В качестве основного языка программирования в ней, вероятно, будет использоваться Dart, а в качестве основного способа создания приложений — фреймворк Flutter. Ходят слухи, что Fuchsia может заменить Android и ChromeOS на всех устройствах. Если Google пойдет на это, компания скорее всего предоставит нативную поддержку написанных под Android приложений (как это делала Microsoft при переходе с DOS на Windows). Поэтому пока можно не беспокоиться и продолжать накапливать экспертизу в Android. А тем, кто хочет заглянуть в будущее, скачать Flutter и поиграться с ним можно здесь.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 23
      +4
      Сложно понять, что сподвигло архитекторов на подобное решение

      Я тоже над этим размышлял, думаю это по большей части связано с 2 проблемами:
      1) Что делать, если Activity имеет разные ресурсы для портрета и лендскейпа. С одной стороны, наверно можно было бы сделать какое-то отдельное АПИ, чтобы обрабатывать эти случаи. Но тогда жизненный цикл отличался бы для Activity с разными ресурсами, что довольно неудобно.
      2) Есть много других ситуаций, когда Activity меняет конфигурацию и тоже пересоздается. Но они менее очевидные и частые, чем простой поворот экрана. Возможно архитекторы хотели таким образом зафорсить обработку всех подобных случаев, чтобы разработчики на оставляли баги.

      Может есть еще какие-то более глубокие причины, систему очень давно начали писать.

      Ну а в целом архитектура говно, да.
      Но перечисленные проблемы очень базовые и все уже как-то научились с этим жить, используя либы, наработки умных дядек и правильные подходы к написанию кода.
        0
        Ну а в целом архитектура говно, да.

        А можно в двух словах почему именно говно? Что бы вы сделали по-другому?
          +2
          В двух словах — вряд ли, это все-таки SDK.

          Плохо практически все, начиная с View. Сырцами View, ViewGroup и ViewRootImpl можно детей пугать, об этом говорят даже ребята из гугла, которые работают над графическим фреймворком.
          View знает обо всем на свете, имеет много лишнего API, которое должно быть у сабклассов, а у нее.

          Были хреновые ListView, ScrollView. Решили сделать хорошо в RecyclerView, в итоге урезали некоторые фичи, нужно все так же писать тонну кода, чтобы сделать простейший список, а LayoutManager по сложности реализации может потягаться с полностью кастомной вьюхой.
          Про NestedScrollView вообще молчу, в нем до сих пор столько нелепых багов, что использовать в более-менее сложном сетапе просто невозможно.
          FitsSystemWindows, Window Insets, CoordinatorLayout с его Behavior, для всего приходится писать кучу костылей, чтобы оно хоть как-то вменяемо работало.
          Chris Banes, главный по суппорт либе, целые гайды по костылям пишет.

          Куча глобальных состояний и мемори ликов связанных с этим, начиная с абсолютно базовых вещей типа InputMethodManager.

          Нет нормального API для асинхронщины. Но это по крайней мере решается библиотеками.

          Bluetooth — тихий ужас.

          Фрагменты — неудобное API, нет чего-то типа transaction listener, сложный lifecycle, особенно для вложенных фрагментов. А для фрагментов добавленных в XML он еще другой. Краши на коммитах после saveInstanceState, баги и лаги в анимациях перехода, рандомные краши в местах типа onCreateOptionsMenu.

          Различные проблемы с клавиатурой и фокусом.

          Architecture Components сейчас пытаются решить некоторые проблемы с архитектурой для отдельных скринов, сложностью жизненного цикла компонентов нетестируемым кодом.
          Но все равно как-то местами криво получается. ViewModel хранится, используя retained фрагменты, LiveData имеет несколько дизайн просчетов…

          Вообще многовато проблем для комментария, но статью я не сильно хочу писать, т.к. в ней нет особого смысла, просто будет нытье какое-то :)
        +4
        Гм, весьма спорная статья с точки зрения андроид разработчика. Можно много чего написать, пройдусь только по некоторым моментам.

        Сложно понять, что сподвигло архитекторов на подобное решение.
        Все просто. Приложение может иметь разную разметку экрана (layout) в портретной и ландшафтной ориентации. Чтобы ее загрузить, старая активити убивается, а новая создается, так как набор view на экране может изрядно отличаться. А тот самый флаг как раз таки говорит о том, что разработчик сам знает, что делает. Т.е. если я знаю, что разметка будет та же самая, то могу выставить этот флаг и все останется так как есть.

        Android предоставляет своим пользователям аж целых четыре механизма асинхронной работы: AsyncTask, Service, IntentService, Thread.
        Не совсем так. Service сам по себе никак не связан с асинхронной работой, так как использует основной поток. IntentService является наследником Service, и имеет внутри Handler для обработки очереди событий в отдельном потоке. AsyncTask это просто обертка над работой с потоками. Так же не упомянуты Handler и Loaders. А если отступить от собственно андроида, то есть еще RxJava и Kotlin coroutines.

        Получается, при создании Activity все равно нужно отслеживать “холодный старт” и проводить инициализацию вручную. Не говоря уже о том, что при перезапуске не восстанавливаются никакие Thread и AsyncTask.
        Не вижу особых отличий от десктопа. Да, на десктопе обычно система не выносит приложения, но если уж оно умерло, то умерло, вместе со всеми потоками и данными в памяти.

        Оказалось, что стандартный класс HashMap (который мы используем для получения объекта Java по его ID) в нашем случае неэффективен
        Даже не могу представить, что нужно делать с HashMap, чтобы задолбать сборщик мусора…

        Если вы хотите вставить в ваше приложение предпросмотр документов, вынуждены разочаровать: в Android нет встроенного компонента, который умеет показывать файлы Word, Excel, Powerpoint. Не говоря уже об архивах ZIP или документах PDF.
        А где же он есть-то? В Windows? В Linux? Думаю в большинстве ОС эти функции делегированы сторонним приложениям. Андроид тут ничем не выделяется.
          +2
          Андроид тут ничем не выделяется.
          Как раз и выделился.

          В macOS/iOS это всё есть.

            +1
            Насколько я понимаю (а яблочных устройств у меня нет), этим занимается отдельное приложение, пусть и поставляемое с системой, но не встроенный компонент системы, о котором речь в статье.
              0

              А explorer.exe в windows — компонент или отдельное приложение поставляемое с системой?

              0
              Не силён в разработке под iOS, но немного погуглив вижу, что везде эти форматы отображаются через загрузку в UIWebView, это всё же не то же самое, что поддержка форматов на уровне SDK. Так и в андроиде можно сделать, загрузив нужный файл в WebView.
              0
              Если я не ошибаюсь, то macOS из коробки умеет только docx из офисных форматов. В iOS по-моему нет поддержки из коробки. В Android при желании можно много что добавить. И судя по тому, что большинство телефонов на Android доброго слова не стоит лучше таки что бы из коробки была поддержка не всего.

              Но вот что меня по-настоящему смутило, так это то, что в Android нет поддержки ZIP. Как ее может не быть если jar это и есть zip.
                0
                Но вот что меня по-настоящему смутило, так это то, что в Android нет поддержки ZIP.
                Так она есть.
                  0

                  1) вы комменты читали? :)


                  2) added in API level 1
                  ZipInputStream

                0
                Спасибо за ваш комментарий.

                Вы пишете, что механизмов асинхронной работы еще больше. Это делает восприятие еще более сложным. Loader, кстати, будет deprecated, начиная со следующей версии API (28). Непонятно, почему AsyncTask до сих пор не deprecated, ведь он явно входит в противоречие с идеей пересоздания всех компонентов интерфейса при повороте. Любая ссылка, запомненная в объекте AsyncTask, будет нерабочей после окончания асинхронной задачи, если пользователь повернул экран во время ее выполнения.

                Понятно, что архитекторы Android каждый свой выбор на чем-то основывали. И документация, конечно, опишет, какие компоненты и как использовать, чтобы получить требуемое вам поведение. Мы в большей степени акцентируем внимание на том, что некоторые архитектурные решения Android сомнительны, и в других ОС сделано по другому и в чем-то проще.

                В хорошей архитектуре сама структура кода API подсказывает разработчикам как использовать платформу, а документация лишь дополняет ее. В Android же доверять интуиции не стоит, нужно всегда читать документацию. Например, идея, что Service обрабатывает события в том же потоке, а наследник от него IntentService создает отдельный поток, совершенно контринтуитивна.

                Про «холодный старт» — в том-то и дело, что Android претендует на то, что в отличие от десктопа, ваше приложение, если написано правильно, вообще не заметит, как его выкидывали из памяти и потом рестартовали. А на самом деле технология не срабатывает, как только приложению нужна инициализация с подогреванием кешей.

                Что касается просмотра файлов — в iOS аналог WebView приемлемо показывает документы Word/Excel/PDF. В Excel-документе можно даже переключаться между листами. В Android это не так.
                0
                Если вы хотите вставить в ваше приложение предпросмотр документов, вынуждены разочаровать: в Android нет встроенного компонента, который умеет показывать файлы Word, Excel, Powerpoint. Не говоря уже об архивах ZIP или документах PDF.
                А где же он есть-то? В Windows?

                В Windows, zip-файл открывается как папка и можно с ним делать что угодно, в Линуксе gzip,bzip и прочие также присутствуют
                  +1
                  Ну насчёт zip-архивов — в android есть классы для работы с ними, особых проблем с этим нет. А касательно стандартного компонента для отображения doc/docx/ppt/xls/etc — конечно, такого нет, да и незачем пихать в SDK компоненты для поддержки различных проприетарных форматов на все случаи жизни.
                    0

                    Это ISO форматы. С таким же успехом можно и jpeg/png из коробки не держать.

                      +1

                      doc, ppt и xls вроде как совсем не ISO.
                      docx — вроде OpenXML, который ISO, но по слухам не всегда следует заявленному стандарту.

                        0

                        А патенты на MP3 истекли в 2017, но это не мешало андроиду их проигрывать из коробки.
                        А h.264 до сих пор поддерживает.
                        А по слухам Android лучше iOS.

                    +2

                    Для того, кто не пишет под андроид годами (не набил еще всех шишек, а может уже и привык), впечатления от АПИ будут скорее всего плохими, объективно оно далеко от совершенства и удобства и дело тут не в джаве.

                      +1
                      С точки зрения андроид-разработчика есть очень много спорных моментов.
                      Например вот:
                      некоторые сценарии просто невозможно реализовать, например, ленту изображений

                      а как как же RecyclerView? Он как раз для такого и создавался.

                      Про активити уже ответили выше, не буду повторяться :)
                        0
                        Перед нами стояла задача показать в одной галерее файлы разных типов: и картинки, и видео, и документы. Проблема не в RecycleView, а в компоненте WebView, который не умеет показывать офисные файлы.
                        0
                        А вы свое приложение под iOS делали?
                        Если да, то можете такую же статью про iOS глазами Android разработчика написать?
                          0
                          У нас есть хорошее приложение под iOS. Первая версия вышла, если не изменяет память под iOS 3 или 4. Постараемся со временем поделиться опытом. Но основная наша работа — код писать, поэтому сроков не обещаем.

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

                        Самое читаемое