Под капотом: патч для Dalvik от Facebook для Android

Original author: David Reiss
  • Translation
Facebook — одно из самых функциональных приложений, доступных на Android. С такими функциями, как push-нотификации, новостная лента и встроенная версия Facebook Messenger (фактически, являющаяся полноценным приложением), которые работают одновременно в реальном времени, сложность и объём кода порождает ряд технических сложностей, с которыми сталкиваются в том числе и другие Android разработчики — особенно на старых версиях платформы. (Наши последние приложения поддерживают старую версию Android 2.2 — Froyo, которой уже почти 3 года).

Одна из таких проблем связана с тем, как виртуальная машина Android — Dalvik, обращается с Java методами. В конце прошлого года мы закончили переработку нашего Android приложения, которая включала в себя перевод большого объёма JavaScript кода в Java, а так же использование новых абстракций, которые породили большое число небольших методов (в большинстве случаев, это считается хорошей практикой программирования). К сожалению, это привело к резкому увеличению числа методов в нашем приложении.


Как мы выяснили, проблема впервые проявила себя, как описано в этом баге, что приводило к невозможности установки нашего приложения на старых версиях Android. Во время стандартной установки запускается программа «dexopt», чтобы подготовить приложение к установке на конкретный телефон. dexopt использует буфер фиксированного размера («LinearAlloc» буфер) для сохранения информации обо всех методах приложения. Последние версии Android используют буфер размером 8 или 16 Мб, но Froyo и Gingerbread (версии 2.2 и 2.3 соответственно) имеют ограничение только в 5 Мб. Поэтому, большое число наших методов приводило к превышению его размера и крэшу dexopt на старых версиях Android.

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

Однако, это был не тот путь, которым мы могли разбить наше приложение — слишком много классов ссылаются непосредственно во фреймворк Android. Вместо этого, необходимо было внедрить наши дополнительные dex файлы прямо в системный загрузчик классов. Это невозможно сделать стандартным путём, но мы исследовали исходный код Android и использовали рефлексию, чтобы напрямую модифицировать некоторые из его внутренних структур. Конечно, мы очень рады и благодарны, что Android является проектом с открытым исходным кодом, иначе эти изменения были бы невозможны.

Но как только мы подошли ближе к запуску нашего обновленного приложения, мы столкнулись с новой проблемой. LinearAlloc буфер используется не только в dexopt — он существует в каждой запущенной Android программе. В то время, как dexopt использует LinearAlloc для хранения информации обо всех методах вашего dex файла, запущенному приложению он нужен только для методов классов, которые в данный момент используются. К сожалению, сейчас мы используем слишком много методов для версий Android вплоть до Gingerbread, и наше приложение стало падать почти сразу после старта.

Не оставалось способа обойти эту проблему с dex файлами, т.к. все наши классы загружались в одном процессе и мы не смогли найти информации о ком-либо, столкнувшимся с этой проблемой раньше (ибо это возможно, если вы используете множественные dex файлы, что само по себе является сложной техникой). Мы были предоставлены сами себе.

Мы пробовали различные способы добыть память, включая агрессивное использование ProGuard и переработку исходного кода, с тем чтобы уменьшить число методов. Мы даже разработали профилировщик использования LinearAlloc, чтобы найти крупнейших его потребителей. Ничто не помогало, а нам всё ещё нужно было написать ещё больше методов для поддержки различных типов контента в нашей улучшенной новостной ленте и таймлайне.

Выпуск долгожданной версии 2.0 Facebook для Android оказался под угрозой. Казалось, мы должны были выбирать: либо существенно урезать функциональность приложения или же доставлять наше приложение только для последних версий Android (Ice Cream Sandwich и выше). Ни то, ни другое не было приемлемо. Нам нужно было лучшее решение.

И так, мы снова обратились к исходному коду Android. Посмотрев на декларацию LinearAlloc буфера, мы осознали, что если бы было возможно увеличить размер буфера с 5 до 8 Мб, мы были бы спасены!

Вот тогда нам пришла в голову идея использовать JNI расширение, чтобы заменить существующий буфер буфером большего размера. На первый взгляд, эта идея кажется совершенно безумной. Модификация внутренностей загрузчика классов Java — это одно, но изменение виртуальной машины Dalvik в то время, когда она выполняет наш код — может быть очень опасным.
Но как только мы покорпели над кодом, анализируя использование LinearAlloc, мы начали понимать, что это должно быть безопасно, если сделать в самом начале программы. Всё что нам нужно было — это найти объект LinearAllocHdr, заблокировать его и заменить буфер.

Поиск оказался самой трудной частью. Вот, где находится этот объект, внутри объекта DvmGlobals, около 700 байт от начала. Поиск объекта целиком может быть рискованным, но к счастью, у нас была опорная точка — объект vmList всего несколькими байтами ранее. Он содержит значение, которое мы могли сравнить с указателем JavaVM, доступным через JNI.

План, наконец, сложился воедино: найти нужное значение из vmList, найти совпадение в DvmGlobals, прыгнуть на несколько байт назад к хедеру LinearAlloc и заменить буфер. Таким образом, мы разработали JNI расширение, встроили его в наше приложение и… увидели, как наше приложение запускается на Gingerbread телефоне первый раз за недели. План сработал.

Но по какой-то причине не запускалось на Samsung Galaxy S II…
Самом популярном Gingerbread телефоне…
Всех времён…

Похоже, Samsung сделала небольшое изменение в Android, которое вводило наш код в заблуждение. Другие производители могли сделать то же самое, поэтому мы решили сделать наш код более надежным.

Ручная проверка кода GSII показала, что буфер LinearAlloc находился всего в 4 байтах от того места, где мы его ожидали, поэтому мы переписали наш код так, чтобы просматривать нескольких байт в каждую сторону, если невозможно найти LinearAlloc в его предполагаемом месте. Это породило необходимость парсить таблицу памяти нашего процесса, чтобы убедиться, что мы не делаем никаких ссылок на невалидную память (что может привести к мгновенному крашу), а так же построить сильный эвристический алгоритм, чтобы быть уверенным, что мы опознаем LinearAlloc, когда его найдем. И, наконец, мы нашли самый безопасный путь просканировать весь хип процесса, чтобы найти буфер.

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

Мы использовали ручное тестирование, DeviceAnywhere и тестовую лабораторию, предоставленную Google, чтобы протестировать наше приложение на 70 различных устройствах и к счастью, оно заработало на каждом их них!

Мы выпустили этот код вместе с Facebook для Android 2.0 в декабре. Теперь, он работает на сотнях различных моделей телефонов. Большой прирост скорости в этом релизе был бы невозможен без этого сумасшедшего хака. И, само собой, без исходного кода Android мы не имели бы возможности выпустить нашу лучшую версию приложения. Android предоставляет обширные возможности для разработки и мы рады приносить Facebook на всё бОльшее количество устройств.
Share post

Comments 47

    +25
    >>Facebook — одно из самых функциональных приложений, доступных на Android
    Серьезно? 0_0
      +6
      Я бы сказал — одно из самых больших приложений. Простенький клиент социалки объёмом под 30 мб! Это мастерство, не иначе.
      +8
      рефакторинг — не, не слышал
        0
        В большинстве случаев рефакторинг увеличивает количество методов а не уменьшает :)
          +7
          Надо перевернуть монитор в вертикальное положение и тогда уменьшит :)
            +2
            У нас над проектом работает два человека: рефакторинг одного всегда увеличивает число методов и абстраций, а рефакторинг второго — всегда уменьшает.
          +4
          Как по мне — Facebook — одно из самых спорных приложений в маркете. Вроде бы работает, а тут, через пол дня «а куда это у меня весь заряд делся». После апдейта — «ура, батарейка не садится… Ой, а что это загрузка фотографий не работает?», и так каждый раз…

          Пред-последняя моя попытка использовать их приложения обломилась после того, как facebook на пару с gpsd терзаемым им, сожрали больше 50% батарейки за 5 часов. В последний раз даже не стал пробовать, как жрет. Отключил все возможные опции автоопределения / указания GPS положения, заметил что все равно приложение терзает GPS при запуске, решил нафиг, нафиг. Ссылка на facebook.com на Home Screen-е будет неплохой заменой.
            0
            Всё так! Когда поставил на свой андроидофон cyanogenmod без по умолчанию прошитого приложения фейсбука со всеми его дополнениями, телефон начал просто летать.

            А на фейсбук можно раз в день и с компьютера зайти. Или мобильная версия сайта, да.
              0
              А я только и удивляюсь людям, которые вопят о проблемах в отзывах на маркете. Вот честно, уже около года использую на планшете и телефоне (и там, и там — CM10) — ни одной проблемы с Facebook никогда не встречал. Осенью приложение было не слишком удобно и шустро — это да, но никаких падений и багов не замечал.
              +14
              Жесть… Теперь понятно откуда растут ноги у этой неповоротливости и глюкавости приложения Facebook…
                +6
                Интересно было бы узнать мнение ребят из Вконтакте. У них и сама соцсеть и приложение в разы функциональнее.
                  0
                  Отвечаю через полтора года (простите, только сейчас коммент заметил, попал сюда по ссылке из другой статьи).
                  Приложение фейсбука состоит из говнокода и кучи ненужных абстракций чуть менее, чем полностью. Я проверял. Например, мне однажды надо было узнать, как там парсятся ссылки при открытии из других приложений — настоящую реализацию спрятали, кажется, аж за 5 (!) последовательными вызовами методов в различных классах и, кажется, там даже был специальный пакет на эту тему.
                  Ну и всякие неоптимизированные лэйауты, неумение правильно работать с ListView и прочее, что отлавливается элементарно при умелом использовании «настроек разработчика» и прямых рук. Сто разработчиков, а нормальное приложение сделать всё никак не могут.

                  Если говорить о приложении ВК — всё сделано максимально на системных компонентах без абстракций сверх необходимого (например, большую часть методов для работы с друзьями я разместил просто в одном классе). Достаточно мощно используется кэширование (в SQLite), в частности, для сообщений и профилей пользователей. Из интересных оптимизаций интерфейса — списки постов: каждый пост сам по себе имеет сложную структуру, но при отображении он разбивается на несколько «частей», вида [заголовок, текст, блок фоток, аудиозапись, нижняя часть с кнопками], каждая из которых является отдельной строкой в ListView. Таким образом можно достичь намного лучшей производительности при прокрутке, т.к. повышается удобство переиспользования view и уменьшается время, необходимое на сам measure/layout. И, естественно, надо думать об overdraw, о котором в фейсбуке, кажется, не слышали.
                  –11
                  Ничего не писал под андройд, но становится страшно, когда начинаю представлять уровни абстракции. Linux, на нём Java, на ней Dalvik… С тысячами вариантов аппаратных платформ… бррр. Почему-то представляется костыльная фабрика.

                  Как пользователь я вполне доволен своей Sony Xpreia, а вот как программист лезть туда не хочу.
                    +6
                    Не совсем верно, что Dalvik на Java. Dalvik — это и есть Java, если в контексте рантайма.

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

                    P.S. Вообще Android мало чем концептуально отличается от того-же Windows, если разработка идет на .NET. У Windows даже уровней еще больше: NT ядро со своим мало кому известным API, поверх него прослойка WinAPI, поверх него Common Language Runtime .NET-а (CLR), поверх него .NET Framework-и, и сторонние библиотеки, используемые приложением. Ну и собственно само приложение, состоящее из сотен классов (если большое), кучи архитектурных элементов, итп, итп.
                      –7
                      Хех :-) C .NET я хорошо знаком, а по поводу Android только периодически почитываю статьи, подобные этой и они постоянно у меня вызвают реакцию «фига они там навертели». Но ничего, в ближайшее время у меня будет повод попробовать себя в разработке под Андройд. Тогда и разберусь что к чему.
                        +9
                        > не писал под андройд, но становится страшно

                        > будет повод попробовать себя в разработке под Андройд

                        лучше не надо.
                          –6
                          Надо, Номад, Надо! :-)
                      0
                      Я ваш антипод =) Пишу под Андроид, причем не без удовольствия. При этом, как пользователю эта операционная система мне не очень нравится в силу некоторых субъективных причин.
                      В архитектуре типа «операционная система < — виртуальная машина < — высокоуровневый язык» нет ничего страшного на самом деле.
                        –2
                        Обязательно опробую свои силы в написании приложений под Андроид, тогда и разберусь как он устроен :-)
                          0
                          Лучше не надо
                            0
                            Андройд, андройд, андройд!
                      +11
                      Какое шикарное инвалидное кресло для отстрелившего себе обе ноги.
                        +4
                        Я не специалист в Андроиде и может чего то не понимаю.
                        В статье написано, что аппликейшен может изменять память в пространстве ядра операционной системы(Dalvik)?
                          +2
                          Dalvik живет в юзерспейсе. Это же виртуальная машина.
                            0
                            Перефразирую вопрос: как процесс запущенный в VM (которая должная, как я понимаю, являться песочницей для это процесса) может модифицировать эту VM?
                              0
                              Посыл неверный — процесс модифицирует не саму VM, а ОТДЕЛЬНЫЙ инстанс этой VM, в которой он запущен.
                                0
                                Нативный код может изменять в своем процессе что угодно. Защита обеспечивается на уровне операционной системы/сервисов, а не на уровне Dalvik VM.
                            0
                            напоминает кодинг под Win32 времен MFC.
                              0
                              Не переживайте. Так только у тех, кто на андроид портирует приложения перекомпиляцией тонны JavaScript в 30 мегабайт Java–кода.
                              +4
                              Процитирую буквально сегодня оставленный последний отзыв (не мой) о приложении «Чат Facebook»:
                              Что снова за лажа????? «Что-то пошло не так», и ничего не запускается!!! Htc one. Только что, до апдейта, все работало! Уроды криворукие!!((

                              И последний отзыв о приложении Facebook (так же не мой):
                              Е#АНАЯ ХРЕНЬ Я ДОХЕРА ВРЕМЕНИ НА РЕГИСТРАЦИЮ УЕ#АЛ!!! #ЛЯТЬ ПОЧЕМУ Я НА КОМПЕ МОГУ ЗАЙТИ А ЗДЕСЬ НЕТ????!!! ПОШОЛ В #ОПУ Ё#АНЫЙ FACEBOOK ЧТОБ ЭТА КОМАНДА Е#ЛАНОВ СДОХЛА ОТ ПРЫЩЕЙ!!!!!!!

                              Немного зацензурил, чтобы меня НЛО не слило, но в целом посыл понятен, думаю…
                                +2
                                вы почитайте комментарии к оригинальному посту и что пишут в твиттере по сабжу. андроид фейсбук команда сама себя позорит
                                +8
                                Читал сегодня эту статью в оригинале. Как по мне, так изменение значения одной глобальной переменной в рантайме (пусть не через API, а по адресу) слабо тянет на такую пафосную статью.

                                А еще в увас ошибка в заголовке — в оригинале патч для Dalvik для Facebook для Android, т.е. сразу дают понять, что это костыль для Dalvik именно для приложения Facebook, а не патч для Dalvik, как может показаться из вашего названия.
                                  –2
                                  Интересно, сколько человек во времена MS DOS умоляли дать не 640кБ доступной памяти, а хотя бы 683.5!
                                  Думал что развязка будет в стиле: «именно поэтому мы разделил приложение на 2 куска: главное приложение и Facebook чат»
                                    0
                                    И были им ниспосланы свыше HIMEM.SYS и EMM386.
                                    +3
                                    Криворукие чуваки понаделали методов на пять мегов таблицы методов, молодцы, че?!
                                      +2
                                      Как я понимаю, даже не понаделали, а понагенерили…
                                        0
                                        Я полазил внутри и с первого взгляда генерированного кода не видно. Зато есть как минимум 3 JSON парсера, неск. вариантов работы с БД, ACRA + свой крашрепортер, куча аннотаций, на каждый чих аналитика, гугл-коммонс, не уверен, что он там жизненно необходим.
                                        0
                                        Код используемых библиотек может сильно раздуть приложение. У нас сейчас как раз такая ситуация: на Android 2.3 не устанавливается приложение после использования библиотеки ical4j.
                                          0
                                          А не надо использовать кривые либы. У сообщества ява есть одна неприятная болезнь, называется over-refactoring. Например, спрашиваешь людей, как посчитать разницу между датами в миллисекундах и тебе сходу предлагают Yoda Time, которая скомпилированная весит почти метр, лол. Ну и да, в итоге получается вот такое вот говнецо, которое никуда не влазит, тащит за собою километры ненужного кода, тормозит и жрёт память не в себя.
                                            0
                                            Я понимаю, что всё это плохо, но у нас ситуация другая: никто в проект не пихает всякие йоба-таймы без необходимости. Конструктивные предложения будут? Велосипедить своими руками CalDAV и CardDAV — не вариант: это, всё-таки, посложнее разницы между датами.
                                              0
                                              Ну конкретно с календарями я сам не сталкивался, так что посоветовать в данном вопросе я так сходу ничего не могу, увы. Разве что посмотреть на код софта для работы с DAV-ами и узнать какими библиотеками они пользуются. Вот тут что-то есть, хз на сколько вам подойдёт code.google.com/p/caldav4j/
                                                0
                                                Только одно «но»: оно использует ical4j и тянет ещё с десяток библиотек :D
                                                Увы, но альтернатива — использовать коммерческие библиотеки (не факт, что они тоньше) или написать свою.
                                        –1
                                        Сколько проблем (потраченных человеко-часов = потраченных денег) можно было бы избежать если бы робот обновлялся везде и везде сейчас был бы 4.2.2…
                                          –1
                                          Судя по комментариями из GooglePlay, разработчикам сначала стоило довести приложение до рабочего состояния с минимумом критичных багов хотя бы для ICS, а потому уже, если останется время, заниматься такими жестокими извращениями с виртуальной машиной. А в итоге, потратили кучу времени на приложение, которое не реализует функционал должным образом, зато запускается на более ранних версиях Андроида. В чем, простите, выгода от такого подхода, если пользователь его в итоге удаляет из-за его глюков, на какой бы версии Андроида его устройство не работало?
                                            0
                                            А у меня работает, и я не удоляю. Почему-то мне кажется, что я не один такой.
                                            0
                                            хак на хаке и хаком погоняет и это фейсбук…
                                              0
                                              Facebook — одно из самых кривых приложений, доступных на Android. Такого количества багов, не говоря уже о фирменном нечеловеческом usability FB, не встречал ни в одном другом приложении для Android.

                                              Only users with full accounts can post comments. Log in, please.