686 слова
3 минуты
Как запустить Activity без регистрации в Manifest?
Вступление
Привет! Я когда только начинал изучение Android в далёком 2016 году, натыкался на такую ошибку:

1. Для чего вообще регистрировать Activity в Manifest?
Это нужно Android системе по многим причинам, но основные я бы выделил следующие:
- Система должна знать, некоторую информацию о приложении, не запуская его. Например запуск неявных интентов: без регистрации нашей активити в системе Android, она не поймёт что наше приложение, допустим умеет открывать GPS координаты на карте.
- Безопасность. В широком смысле этого слова. Тут могу выделить кейс, использования запрета запуска нашей Activity другими приложениями, это атрибут exported=false
2. А зачем пытаться запустить Activity не регистрируя её в в Manifest?
- Тут могут быть разные причины, начиная от банального любопытства(это как раз мой случай), до использования этой возможности в каких-то внутренних потребностях бизнеса.
- Конкретный пример, это один из способов, который позволяет поставлять новые фичи для приложения, без необходимости его обновления. В Китае очень популярен такой способ разработки приложений, например есть такая библиотека как RePlugin, она позволяет поставлять новые фичи и фиксить баги очень быстро, без необходимости загрузки новый версии приложения в магазин.
- Google запрещает такой способ разработки и блокирует приложения в Play Store, которые динамически поставляют фичи, в обход его официальной возможности, которая у них называется Play Feature Delivery. Это отдельная тема, о которой много можно говорить, тут я просто хочу показать с технической точки зрения, как можно запустить Activity не регистрируя её в Manifest.
3. Как происходит запуск Activity?
Для реализации кода, нам сначала нужно понять, как вообще запускается Activity и потом уже можно придумать решение. В детали мы погружаться не будем, так как это сложно и долго, пройдёмся поверхностно, чтобы понимать общую картину.
- В Android SDK, есть класс ActivityThread - это как раз тот класс, где вызывается метод main нашего приложения и стартует бесконечный цикл UI потока.
- Дальше есть класс Instrumentation, экземпляр которого создаётся в ActivityThread. Задача класс Instrumentation, как раз выполнять методы жизненного цикла, у конкретной Activity.
- В этой цепочке главный ActivityThread, он вызывает методы у Instrumentation, а Instrumentation уже у конкретной активити вызывает нужный метод, например onCreate(). Т.е ActivityThread не вызывает методы жизненного цикла у Activity напрямую. Он делегирует эту задачу объекту Instrumentation.
- При установке любого приложения, система считывает его Manifest и сохраняет внутри системы, все Activity которые прописаны там, чтобы потом ссылаться на них при запуске.
- Далее, при вызове метода startActivity() мы попадаем в метод Instrumentation.execStartActivity(), где через Binder происходит обращение к ActivityManagerService, чтобы понять зарегистрирована ли вызываемая Activity в Manifest или нет?
- Если - да, то запускаем. Если нет - то метод checkStartActivityResult() внутри Instrumentation, вернёт ошибку ActivityNotFoundException
4. Идея, как можно запустить Activity без регистрации в Manifest
Идея такая:
- Подкинуть системе существующую Activity, которая зарегистрирована в Manifest. А потом на каком-то этапе подменить её на другую, которая не зарегистрирована в Manifest.
- Дальше, мы можем создать собственную реализацию экземпляра класса Instrumentation и подменить его в ActivityThread. Теперь любой вызов метода startActivity() в нашем приложении, будет происходить именно, через наш прокси объект Instrumentation, а не системный.
5. Реализация идеи
Разобьём эту работу на несколько этапов:
- Создать Activity заглушку, которая будет зарегистрирована в Manifest.
- Реализовать класс InstrumentationProxy, который будет содержать наш изменённый код.
- Создать хук с помощью рефлексии, который заменить оригинальный экземпляр класса Instrumentation в ActivityThread на наш InstrumentationProxy.
- Обойти защиту на Android 9+, с помощью библиотеки AndroidHiddenApiBypass. Начиная с версии Android 9, гугл начал внедрять защиты от рефлексии:
- Класс ActivityThread является скрытым (@hide), а поле mInstrumentation - приватным. Начиная с Android 9, система на уровне ART, блокирует доступ к таким полям через рефлексию.
- Библиотека AndroidHiddenApiBypass использует класс sun.misc.Unsafe из Java, благодаря которому можно делать низкоуровневые манипуляции с памятью, что позволяет отключить эту защиту.
6. Схема работы обхода:

7. Исходный код:
Исходный код с реализацией, можно посмотреть тут, он там с комментариями:
Waiting for api.github.com...
8. Статья в видео формате:
Youtube:
Rutube:

