---
title: "DexClassLoader, или загрузка части приложения из сети"
url: "https://romankryvolapov.com/ru/dex-class-loader/"
description: "Как Android-приложение скачивает .dex с сервера и загружает из него классы через DexClassLoader, чем это ограничено с Android 14 и почему в Google Play так делать нельзя."
language: ru
published: 2021-01-21
updated: 2026-11-05
---
Как-то я написал небольшую программу, которая скачивает с сервера заранее скомпилированный код в формате `.dex` и запускает его. Хотелось выяснить, может ли часть приложения жить не в самом APK, а в виде кода на сервере, который подгружается во время работы.

Идея пришла после статьи на «Хакере» про модули для Android-троянов ([часть 2](https://xakep.ru/2016/07/05/android-plugins-binder-2/)). Трояны меня не интересовали: интересно было, как устроена сама механика и что на это скажет Google Play.

## Как это работает

Код Android-приложения после сборки лежит в файлах `.dex` — это байт-код для виртуальной машины ART. Обычно все `.dex` упакованы в APK, и их загружает системный загрузчик классов. Но в Android есть и публичные загрузчики, которым можно передать свой файл:

- `DexClassLoader` — загружает классы из `.dex`, `.jar` или `.apk`, лежащего на диске;
- `PathClassLoader` — то же самое, его система использует для самого приложения;
- `InMemoryDexClassLoader` (с API 26) — загружает `.dex` прямо из `ByteBuffer`, без файла.

Загруженный класс ничем не отличается от «своего»: его можно создать, вызвать его методы, передать ему контекст. Исключение — компоненты приложения: Activity или Service из такого кода не объявлены в манифесте, поэтому система их не запустит.

## Код

Плагин удобнее всего связать с приложением общим интерфейсом: интерфейс есть в приложении, а класс в `.dex` его реализует. Тогда после загрузки класс приводится к интерфейсу и вызывается без рефлексии.

```java
File dexFile = new File(getCodeCacheDir(), "plugin.dex");
if (dexFile.exists()) {
    dexFile.delete();
}

try (InputStream input = new URL(baseUrl + "plugin.dex").openStream();
     FileOutputStream output = new FileOutputStream(dexFile)) {
    // Android 14+: файл должен стать read-only до того, как в него что-то записано
    dexFile.setReadOnly();
    byte[] buffer = new byte[8192];
    int count;
    while ((count = input.read(buffer)) != -1) {
        output.write(buffer, 0, count);
    }
}

DexClassLoader loader = new DexClassLoader(
        dexFile.getAbsolutePath(),
        null,               // optimizedDirectory: с API 26 игнорируется
        null,               // путь к нативным библиотекам, если они нужны
        getClassLoader());  // родитель: через него плагин видит классы приложения

Class<?> pluginClass = loader.loadClass("com.example.plugin.PluginImpl");
Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
plugin.run(getApplicationContext());
```

Если общего интерфейса нет, остаётся рефлексия — так было в моём первом варианте:

```java
for (Method method : pluginClass.getDeclaredMethods()) {
    method.setAccessible(true);
    Log.d(TAG, "method: " + method.getName());
}
for (Field field : pluginClass.getDeclaredFields()) {
    field.setAccessible(true);
    Log.d(TAG, "field: " + field.getName());
}
```

## Как собрать .dex

Плагин компилируется обычным `javac` или `kotlinc` против того же интерфейса `Plugin`, а получившиеся `.class` переводятся в `.dex` утилитой `d8` из Android SDK (папка `build-tools`). `d8` нужны уже существующая папка для результата, `android.jar` из SDK и в classpath скомпилированный интерфейс `Plugin`, который остаётся в приложении и в плагин не попадает:

```bash
mkdir -p out
d8 --lib "$ANDROID_HOME/platforms/android-36/android.jar" --classpath build/classes \
  --output out/ build/classes/com/example/plugin/PluginImpl.class
```

В `out/` появится `classes.dex` — его и нужно положить на сервер.

В релизной сборке интерфейс должен ещё пережить R8: плагин скомпилирован против его исходных имён, и сохраняет их правило `-keep interface com.example.app.Plugin { *; }`.

## Ограничения

**Android 14.** Если приложение нацелено на API 34 и выше, каждый динамически загружаемый файл должен быть доступен только для чтения, иначе система выбросит исключение при загрузке. Пометить файл read-only нужно сразу после открытия, до записи содержимого, как в коде выше. Уже существующий файл лучше удалить и скачать заново. Подробности — в [изменениях поведения Android 14](https://developer.android.com/about/versions/14/behavior-changes-14).

**Google Play.** В первой версии этой заметки я написал, что после модерации на устройство можно грузить что угодно. Это неверно. Правило Google Play [Device and Network Abuse](https://support.google.com/googleplay/android-developer/answer/16559646) прямо запрещает приложениям и встроенным в них SDK скачивать исполняемый код — `.dex`, `.jar`, `.so` — из любого источника, кроме самого Google Play. Модерация может пропустить такое приложение, но это нарушение правил и основание для удаления. Исключение — код, который выполняется в виртуальной машине или интерпретаторе и не имеет прямого доступа к Android API, например JavaScript в WebView. Законный способ доставлять части приложения по требованию — [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery).

**Безопасность.** Загружаемый код получает все права приложения. Поэтому только HTTPS и проверка подписи файла до загрузки, с открытым ключом, зашитым в APK, иначе подмена файла на сервере или в сети превращается в выполнение чужого кода внутри вашего приложения. Хеш, скачанный с того же сервера, ничего не доказывает: кто подменил файл, подменит и хеш. Google в целом [советует](https://developer.android.com/privacy-and-security/risks/dynamic-code-loading) избегать динамической загрузки кода, если для неё нет веской причины.

Где это всё же применимо: корпоративные приложения, которые распространяются не через Google Play, системы плагинов во внутренних инструментах, эксперименты и исследование того, как устроена загрузка классов в ART.
