Facebook — одно из приложений для Android с самыми богатыми возможностями. Такие фичи, как push-уведомления, новостная лента и встроенная версия Facebook Messenger(отдельное полноценное приложение), работающие одновременно, делают сложность и объем кода настоящим вызовом, который могут принять очень немногие из разработчиков, особенно для старых версий платформы. (Наши самые старые приложения — ровесники Android 2.2, которому уже 3 года)
Один такой вызов относится к способу, которым виртуальная машина Dalvik, работает с Java-методами. В прошлом году мы совершили полную перестройку нашего Android-приложения, в течение которой мы перенесли огромное количество кода с JavaScript на Java, а также внесли множество абстракий, которые требуют огромное число небольших методов (что вообще-то является хорошой практикой программирования). К сожалению, это привело к невероятному увеличению количества методов в нашем приложении.
В процессе тестирования проблема впервые обнаружилась так, как описано в этом баге, приводя к ошибке установки на старых телефонах. Во время установки, программа «dexopt» выполняет подготовку приложения к конкретному телефону. Dexopt использует буфер ограниченного размера(он называется «LinearAlloc») для хранения информации обо всех методах в приложении. В современных версиях Android размер этого буфера составляет 8 или 16 МВ, но в Froyo и Gingerbread(2.2 и 2.3) он не превышает 5 МВ. Большое количество методов переполняло этот относительно небольшой буфер, что и приводило к ошибке в dexopt.
После секунды паники, мы поняли, что могли бы обойти эту ошибку, разбив приложение на несколько dex-файлов, использую вот эту технику, предусматривающую использование дополнительных dex-файлов для модулей расширения, а не для частей ядра.
Тем не менее, у нас не было возможности так разбить наше приложение — слишком много наших классов вызываются напрямую системой Android. Вместо этого, нам нужно было внедрить наши дополнительные dex-файлы прямиком в системный загрузчик классов. Вообще-то, это невозможно, но мы изучили исходный код системы и немного поменяли ее внутренние данные через reflection. Мы были очень довольны тем фактом, что исходный код системы доступен — иначе нам просто бы не удалось ничего сделать.
Но как только мы начали запускать наше переработанное приложение, мы столкнулись еще с одной проблемой. LinearAlloc существует не только в dexopt — он работает внутри каждого приложения Android. В отличие от dexopt, в котором LinearAlloc содержит информацию обо всех методах в dex-файле, работающее приложение хранит в LinearAlloc информацию о тех методах, которые вы на самом деле используете. К сожалению, мы использовали слишком много методов по мнению Android систем начиная с Gingerbread и старше, и наше приложение падало вскоре после старта.
Вот это уже обойти не удалось, потому что все наши классы загружались в единый процесс, и попытки найти данные о ком-либо, кто уже сталкивался с этой проблемой( а это возможно, только если вы используете технику нескольких dex-файлов, которая сложна сама по себе), не увенчались успехом. Мы остались наедине со своими проблемами.
Мы пробовали разные способы экономии места в буфере, включая агрессивное использование Proguard и преобразования исходного кода для уменьшения числа методов. Мы даже создали профилировщик использования буфера LinearAlloc, чтобы выяснить, что расходует место в нем больше всего. Ничего из этого не оказало заметного эффекта, а нам по прежнему нужно было множество методов для поддержки насыщенного содержимого и новых новостных лент, а также ленты истории.
Так как проблема не решалась, релиз столь ожидаемого Facebook 2.0 для Android был под угрозой. Похоже, нам пришлось бы выбирать между урезанием важных возможностей и поставкой приложения только для новых систем(ICS и позже). Ни то, ни другое не было приемлимым. Нам нужно было решение по-лучше.
Мы заглянули в исходный код платформы еще раз. Посмотрев на определение буфера LinearAlloc мы осознали, что стоит нам увеличить
его размер до 8 МВ, как все проблемы уйдут.
Тогда нам пришла в голову идея написать JNI расширение, которое заменит существующий буфер на буфер побольше. Сперва идея нам казалось абсолютно безумной. Одно дело — исправление загрузчика классов; модификация внутренностей виртуальной машины во время выполнения нашего кода — совсем другое, куда более опасное. Но чем больше мы изучали код, анализируя использования буфера, тем больше убеждались в том, что исполнение кода будет безопасным, если модификацию сделать в самом начале работы приложения. Нам нужно было лишь найти объект заголовка LinearAllocHdr, заблокировать его и заменить буфер.
Его поиск оказался нелегким делом. Вот где он лежит, закопанный в объекте DvmGlobals, более чем за 700 байт после начала. Поиск объекта — рискованное предприятие, но нам повезло, у нас было за что зацепиться: vmList лежит несколькими байтами ранее. Это значение мы можем сравнить с указателем на JavaVM, всегда доступным внутри JNI-вызовов.
Так вырисовывался план: получить ожидаемое значение vmList, просканировать DvmGlobals на предмет совпадения, прыгнуть на несколько байт вперед к заголовку LinearAlloc, и заменить буфер. Мы написали библиотеку, встроили ее в приложение, запустили его… и увидели работающее на Gingerbread приложение в первый раз за несколько недель. План сработал.
По какой-то причине, приложение падало на Samsung Galaxy S II. На самом популярном смартфоне на Gingerbread. Каждый раз.
Вероятно, Samsung внесли небольшие изменения в платформу, что и заставляло наш код ошибаться. Прочие производители
могли делать тоже самое, так что мы решили сделать наш код чуть сообразительней.
Ручная проверка GSII выявила, что LinearAlloc лежал всего в 4 байтах от того места, где мы ожидали его увидеть, так что мы изменили код так, чтобы он смотрел немного влево и вправо, если ему не удастся найти LinearAlloc там, где ожидается. Этот процесс потребовал парсинга карты памяти процесса, который бы гарантировал, что мы не создаем никаких неправильных ссылок на области памяти(что приводило бы к незамедлительному вылету приложения), а так же создания точной эвристики, которая бы распознавала найденный буфер. А в качестве подстраховки мы нашли наиболее надежный способ сканирования памяти процесса с целью поиска буфера.
На тот момент у нас было приложение, которое работало на нескольких наиболее популярных телефонов, но этого было недостаточно.
Мы запаковали проверочный код в тестовое приложение, которое проведет туже процедуру, что и для приложения Facebook, а результат
представит в виде большого зеленого или красного прямоугольника.
Мы провели ручное тестирование, воспользовались DeviceAnywhere и тестовой лабораторией Google, что позволило проверить приложение на 70 различных моделях телефонов, и к счастью, наше приложение заработало на каждом!
Этот код был выпущен в приложении Facebook для Android 2.0 в декабре. Теперь он работает на сотнях моделях телефонов, и на одном — не работает. Невероятное ускорение приложение не было бы возможным без этого сумасшедшего хака. И можно молчать об открытом коде платформы Android, который позволил нам выпустить лучшую версию нашего приложения. Платформа дает множество возможностей для построения приложений, и мы вдохновлены возможностью предоставлять наилучший опыт использования Facebook все большому количеству людей на большем количестве устройств.
Автор: dzigoro