---
title: "DexClassLoader, или зареждане на част от приложението от мрежата"
url: "https://romankryvolapov.com/bg/dex-class-loader/"
description: "Как Android приложение изтегля .dex от сървър и зарежда класове от него чрез DexClassLoader, с какво е ограничено това от Android 14 и защо в Google Play така не бива."
language: bg
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.
