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 на всё бОльшее количество устройств.
Автор: Captcha