shalkoff.ru
686 слова
3 минуты
Как запустить Activity без регистрации в Manifest?

Вступление#

Привет! Я когда только начинал изучение Android в далёком 2016 году, натыкался на такую ошибку:

Ошибка ActivityNotFoundException
Картинка 1. Ошибка ActivityNotFoundException
Это ошибка означала: что нужно зарегистрировать нашу activity в Manifest, собственно я так и делал, регистрировал и ошибка пропадала. И я задумался, а можно ли всё же запустить Activity, не регистрируя её в Manifest? Спойлер: да - можно и сейчас я покажу как.

1. Для чего вообще регистрировать Activity в Manifest?#

Это нужно Android системе по многим причинам, но основные я бы выделил следующие:

  1. Система должна знать, некоторую информацию о приложении, не запуская его. Например запуск неявных интентов: без регистрации нашей активити в системе Android, она не поймёт что наше приложение, допустим умеет открывать GPS координаты на карте.
  2. Безопасность. В широком смысле этого слова. Тут могу выделить кейс, использования запрета запуска нашей Activity другими приложениями, это атрибут exported=false

2. А зачем пытаться запустить Activity не регистрируя её в в Manifest?#

  1. Тут могут быть разные причины, начиная от банального любопытства(это как раз мой случай), до использования этой возможности в каких-то внутренних потребностях бизнеса.
  2. Конкретный пример, это один из способов, который позволяет поставлять новые фичи для приложения, без необходимости его обновления. В Китае очень популярен такой способ разработки приложений, например есть такая библиотека как RePlugin, она позволяет поставлять новые фичи и фиксить баги очень быстро, без необходимости загрузки новый версии приложения в магазин.
  3. Google запрещает такой способ разработки и блокирует приложения в Play Store, которые динамически поставляют фичи, в обход его официальной возможности, которая у них называется Play Feature Delivery. Это отдельная тема, о которой много можно говорить, тут я просто хочу показать с технической точки зрения, как можно запустить Activity не регистрируя её в Manifest.

3. Как происходит запуск Activity?#

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

  1. В Android SDK, есть класс ActivityThread - это как раз тот класс, где вызывается метод main нашего приложения и стартует бесконечный цикл UI потока.
  2. Дальше есть класс Instrumentation, экземпляр которого создаётся в ActivityThread. Задача класс Instrumentation, как раз выполнять методы жизненного цикла, у конкретной Activity.
  3. В этой цепочке главный ActivityThread, он вызывает методы у Instrumentation, а Instrumentation уже у конкретной активити вызывает нужный метод, например onCreate(). Т.е ActivityThread не вызывает методы жизненного цикла у Activity напрямую. Он делегирует эту задачу объекту Instrumentation.
  4. При установке любого приложения, система считывает его Manifest и сохраняет внутри системы, все Activity которые прописаны там, чтобы потом ссылаться на них при запуске.
  5. Далее, при вызове метода startActivity() мы попадаем в метод Instrumentation.execStartActivity(), где через Binder происходит обращение к ActivityManagerService, чтобы понять зарегистрирована ли вызываемая Activity в Manifest или нет?
  6. Если - да, то запускаем. Если нет - то метод checkStartActivityResult() внутри Instrumentation, вернёт ошибку ActivityNotFoundException

4. Идея, как можно запустить Activity без регистрации в Manifest#

Идея такая:

  1. Подкинуть системе существующую Activity, которая зарегистрирована в Manifest. А потом на каком-то этапе подменить её на другую, которая не зарегистрирована в Manifest.
  2. Дальше, мы можем создать собственную реализацию экземпляра класса Instrumentation и подменить его в ActivityThread. Теперь любой вызов метода startActivity() в нашем приложении, будет происходить именно, через наш прокси объект Instrumentation, а не системный.

5. Реализация идеи#

Разобьём эту работу на несколько этапов:

  1. Создать Activity заглушку, которая будет зарегистрирована в Manifest.
  2. Реализовать класс InstrumentationProxy, который будет содержать наш изменённый код.
  3. Создать хук с помощью рефлексии, который заменить оригинальный экземпляр класса Instrumentation в ActivityThread на наш InstrumentationProxy.
  4. Обойти защиту на Android 9+, с помощью библиотеки AndroidHiddenApiBypass. Начиная с версии Android 9, гугл начал внедрять защиты от рефлексии:
  • Класс ActivityThread является скрытым (@hide), а поле mInstrumentation - приватным. Начиная с Android 9, система на уровне ART, блокирует доступ к таким полям через рефлексию.
  • Библиотека AndroidHiddenApiBypass использует класс sun.misc.Unsafe из Java, благодаря которому можно делать низкоуровневые манипуляции с памятью, что позволяет отключить эту защиту.

6. Схема работы обхода:#

Ошибка ActivityNotFoundException
Картинка 2. Схема запуска Activity без регистрации в Manifest

7. Исходный код:#

Исходный код с реализацией, можно посмотреть тут, он там с комментариями:

shalkov
/
HookActivity
Waiting for api.github.com...
00K
0K
0K
Waiting...

8. Статья в видео формате:#

Youtube:

Rutube: