Код, который вы пишете, сам по себе является формой использования памяти. Каждый класс, метод и строковая константа в вашем приложении должны быть загружены в ОЗУ при его выполнении. Чем больше база кода приложения, тем больше памяти оно будет потреблять.
Память, поддерживаемая файлами, и подкачка по требованию
Android загружает исполняемый код из файла .apk (например, .oat или .so) с помощью mmap. Это означает, что код поддерживается файлом.
Важно, что Android использует подкачку страниц по запросу. При запуске приложения ядро не загружает весь APK-файл в ОЗУ сразу. Вместо этого он только сопоставляет файл с виртуальным адресным пространством процесса. Когда приложение выполняется и ЦП переходит к новой функции, возникает ошибка отсутствия страницы. Ядро приостанавливает поток, считывает эту страницу кода размером 4 КБ из хранилища в физическую оперативную память и возобновляет выполнение.

Это означает, что код, который вы упаковываете, но никогда не выполняете, не использует физическую память для самих страниц кода. Однако неиспользуемые библиотеки все равно увеличивают общий размер APK и могут значительно увеличить объем памяти, используемой внутренними метаданными системы (такими как индексы DEX и дескрипторы классов), которые необходимо прочитать, чтобы хотя бы узнать о существовании кода. Кроме того, многие библиотеки содержат статические инициализаторы или затрагиваются фреймворками внедрения зависимостей во время запуска приложения, что в любом случае приводит к их загрузке в ОЗУ.
Вытеснение страниц и замедление работы
Поскольку память, поддерживаемую файлами, всегда можно перечитать из хранилища, ядро считает эти страницы "чистыми". Когда системе не хватает памяти, ядро вытесняет (удаляет) эти чистые страницы кода из ОЗУ, чтобы освободить место для других данных.
Если приложению снова потребуется выполнить этот код, произойдет сбой ЦП и ядру придется повторно считать страницу из хранилища. Чем больше кода в приложении, тем выше вероятность, что он будет вытеснен. Когда пользователь возвращается к вашему раздутому приложению после работы с другими, он сталкивается со случайными временными зависаниями и замедлениями, поскольку ЦП постоянно простаивает в ожидании возврата кода из хранилища.
Цена ошибки отсутствия страницы. Хотя она сильно зависит от скорости хранилища устройства (UFS или eMMC) и состояния ядра, серьезная ошибка отсутствия страницы (чтение 4 КБ из хранилища) может стоить от 0,5 до 5 мс. Если путь запуска затрагивает 500 разных страниц неоптимизированного кода, то задержка I/O при запуске приложения может составить несколько сотен миллисекунд.
Изучение размера кода с помощью Compiler Explorer
Чтобы понять, как код Java или Kotlin преобразуется в машинный код (и, следовательно, в байты памяти), можно использовать Compiler Explorer.
Поддержка Android встроена непосредственно в Godbolt. Он позволяет увидеть, как различные части набора инструментов Android (D8, R8 и dex2oat) преобразуют ваш исходный код.
Как использовать Compiler Explorer с Android
- Перейдите на сайт godbolt.org.
- В раскрывающемся списке языков (слева вверху) выберите Android Java или Android Kotlin.
- В раскрывающемся списке компиляторов (в правом верхнем углу панели кода) можно выбрать один из следующих инструментов:
d8– байт-код Dalvik (.dex). Он наиболее близок к исходному коду и удобен для чтения.r8– показывает, как оптимизатор R8 сокращает и оптимизирует байт-код.dex2oat– показывает итоговый машинный код ARM64, который выполняется на устройстве. Здесь вы можете увидеть реальное влияние на память (4 байта на инструкцию).dex2oatможет быть нацелен на разные ISA, но ARM64 – наиболее распространенная архитектура для мобильных телефонов.
- Выделение исходного кода и выходных данных. При наведении указателя на строку кода будут выделены соответствующие инструкции байт-кода или машинного кода, что позволяет легко отслеживать влияние определенных операторов.
- Конвейер оптимизации. В режиме дизассемблирования можно нажать Добавить -> Конвейер оптимизации. Это позволяет увидеть внутренние шаги, которые предпринимает компилятор. Вы можете посмотреть, как внутреннее представление (IR) преобразуется на каждом этапе (например, между шагами Inliner (before) и Inliner (after)), прежде чем оно будет преобразовано в окончательный машинный код ARM64.

Почему это важно для памяти
Каждая инструкция в выходных данных dex2oat, предназначенная для ARM64 ISA, занимает 4 байта в исполняемом файле приложения (.odex или .oat).
Попробуйте ввести код, в котором используются разные функции языка, и изучите выходные данные компилятора:
- Доступ к массиву и итераторы списков
- Простой цикл массива по
int[]может компилироваться примерно в 10 инструкций (около 40 байт). - Цикл foreach по
Listнеявно используетIterator. Это может привести к 30–40 инструкциям (около 160 байт) из-за дополнительных вызовов методов (hasNext(),next()) и выделения самого объекта итератора. - Оптимизация R8. При соблюдении определенных условий (например, если
ListявляетсяArrayList), оптимизатор R8 может преобразовать цикл foreach обратно в простой индексированный цикл, устраняя накладные расходы на итератор и уменьшая как размер кода, так и перемешивание памяти во время выполнения.
- Простой цикл массива по
- Вызовы виртуальных методов. Включают загрузку класса объекта, поиск метода в
vtableи последующее ветвление. Обычно это занимает 4–5 инструкций (около 20 байт). - Прямые/статические вызовы. Часто преобразуются в одну инструкцию
bl(Branch with Link) размером 4 байта. - Лямбда-выражения Kotlin. Могут генерировать целые анонимные классы и дополнительные мостовые методы, добавляя сотни байтов кода и метаданных для простого функционального блока.
С помощью Compiler Explorer вы можете увидеть, как сложные языковые функции (например, лямбда-выражения Kotlin, API потоков или активное использование обобщений) влияют на окончательный размер скомпилированного приложения и как оптимизаторы, такие как R8, могут в некоторых случаях противодействовать стоимости языковых абстракций. Этот инструмент может помочь вам сделать обоснованные компромиссы при проектировании и реализации приложения.
В целом, чем сложнее код приложения, тем больше памяти оно использует. И наоборот, более простой код (или код, упрощенный с помощью R8) занимает меньше места в хранилище и ОЗУ и требует меньше инструкций ЦП.
Оценка влияния кода с помощью meminfo и showmap
Чтобы узнать, сколько памяти занимает код приложения, используйте стандартные инструменты Android.
dumpsys meminfo
При запуске adb shell dumpsys meminfo <package> в категории Code (Код) раздела App Summary (Сводка по приложению) можно посмотреть общие сведения о памяти, связанной с кодом.
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
Для более детального просмотра используйте showmap. Она показывает, какие области из определенных файлов сопоставляются с памятью.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
Вы увидите записи для скомпилированного кода приложения:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
Неиспользуемый код и R8
Поскольку каждый выполняемый метод занимает память, "раздутое" приложение с ненужными инициализациями или неиспользуемыми библиотеками может сильно повлиять на производительность при запуске и базовое использование памяти.
Поэтому такие инструменты, как R8 (ProGuard), имеют решающее значение. R8 анализирует байт-код приложения и удаляет все классы и методы, которые никогда не вызываются.
Практическое упражнение: стоимость раздувания
Чтобы продемонстрировать влияние размера кода, рассмотрим эксперимент, в котором сравниваются две сборки приложения, содержащие 300 сгенерированных классов (каждый с 500 методами):
- CodeBloat (не оптимизировано). Стандартная неоптимизированная сборка, содержащая все сгенерированные классы и уникальные строки.
- CodeBloatOptimized – тот же исходный код, но скомпилированный с включенным сжатием R8.
1. Предварительная компиляция (AOT)
Чтобы максимально увеличить объем памяти, поддерживаемой файлами, мы будем использовать инструмент cmd package compile для предварительной компиляции приложений в файлы .oat.
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
Обратите внимание, что это синтетический пример. Обычно приложения используют режим компиляции speed-profile (подробнее об этом ниже).
2. Запуск и сравнение
Чтобы увидеть по-настоящему холодный запуск, при котором система должна считывать код из хранилища, мы очистим кеш страниц ядра перед запуском каждого приложения. Для этого требуется root-доступ.
Запустите неоптимизированное приложение.
adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat
Теперь сделайте то же самое для оптимизированного приложения:
adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
Результаты
Если вы посмотрите на строку Код в разделе App Summary, то увидите огромную разницу:
- Неоптимизированный файл
Code: около 30 000 КБ (30 МБ). - Оптимизированный
Code: ~2000 КБ (2 МБ).
Поскольку R8 определил, что 500 методов внутри этих классов никогда не делали ничего полезного (метод doSomething() вызывает только method0(), а результаты игнорируются), он удалил почти весь искусственно сгенерированный код из окончательного APK.
3. Как посмотреть результат в Perfetto
Влияние раздувания кода хорошо заметно на этапе начальной загрузки приложения. В частности, найдите фрагмент bindApplication в основном потоке и вложенные фрагменты, начинающиеся с madvising. Они указывают на то, что система готовится загрузить файлы из APK и скомпилированного кода (.odex).
При интерактивном холодном запуске система mmap() и madvise() код и другие данные из этих файлов, необходимые для загрузки и запуска приложения. Значение после size= в фрагментах madvising указывает, сколько данных нужно загрузить. Это позволяет ускорить запуск приложения.
Из сравнения видно, что объем кода приложения, который необходимо было загрузить из хранилища в ОЗУ, был намного больше в случае раздутого приложения, что привело к увеличению продолжительности, что способствовало более медленному запуску приложения. Кроме того, в трассировке запуска раздутого приложения видны фрагменты загрузки дополнительных DEX-файлов (classes2.dex, classes3.dex), которые пришлось добавить, потому что приложение не помещалось в один DEX-файл.
Для сравнения (холодный запуск на Pixel 10a)
| Показатель | Без оптимизации (CodeBloat) | Оптимизированный (CodeBloatOptimized) |
|---|---|---|
base.odex madvise size |
~7,9 МБ (2,0 мс) | ~16 КБ (0,003 мс) |
base.apk madvise size |
~2,4 МБ (2,4 мс) | ~4 КБ (0,001 мс) |
classes2.dex madvise size |
~7,3 МБ (8,6 мс) | Н/Д |
classes3.dex madvise size |
~7,3 МБ (8,0 мс) | Н/Д |
Общая продолжительность (madvising) |
~21 мс | ~0,004 мс |
Неоптимизированная загрузка приложения

Оптимизирована загрузка приложений.

Влияние раздувания кода зависит от размера приложения, характеристик устройства пользователя и нагрузки на систему.
PerfettoSQL для анализа загрузки
Чтобы извлечь эти показатели из трассировок, можно использовать следующие запросы.
1. Длительность запуска приложения
Это время с момента запуска Activity приложения до момента, когда Activity отрисовало первый кадр.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
Подробнее о состояниях запуска приложения…
На время запуска приложения влияет множество факторов, помимо описанных в этом руководстве.
2. Извлечь madvising размеров и длительностей
Этот запрос позволяет подробнее рассмотреть часть madvising, которую мы видели выше.
INCLUDE PERFETTO MODULE slices.with_context;
SELECT
name,
dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
AND name LIKE 'madvising %';
3. Распределение состояний основного потока (общая продолжительность каждого состояния)
Этот запрос показывает, сколько времени основной поток приложения провел в разных состояниях.
SELECT
p.name AS process_name,
state,
sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
GROUP BY p.name, state;
Вы можете уточнить запрос, чтобы посмотреть только состояния основного потока во время запуска приложения.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT
p.name AS process_name,
ts.state,
-- Calculate only the duration that falls within the startup window
SUM(
MAX(0,
MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
)
) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
-- Only select thread states that overlap with the startup interval
AND ts.ts + ts.dur > s.ts
AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;
Это может привести к интересным проблемам, например:
- Большое время в состоянии Runnable (R), но не Running. Это указывает на то, что запуск приложения был отложен из-за конкуренции за ресурсы ЦП, то есть основной поток приложения не мог выполняться, поскольку другие потоки (возможно, из других приложений) занимали ЦП.
- Высокое значение времени в прерываемом сне (D). Обычно это указывает на медленный ввод-вывод или нехватку памяти, что задерживает запуск приложения.
- Высокое значение времени ожидания (S). Это означает, что основной поток ожидал завершения работы других потоков. Иногда это указывает на конфликт блокировок в пути запуска приложения (т.е. основной поток был заблокирован на эксклюзивном ресурсе, который был занят другим потоком в приложении).
4. Максимальный объем памяти, поддерживаемой файлами (файл RSS)
Этот показатель тесно связан с объемом кода и данных, загружаемых приложением при запуске. Чем больше приложение, тем выше это значение и тем сильнее нагрузка на память системы. Это может привести к задержке запуска приложения, поскольку система будет пытаться удовлетворить запросы на выделение памяти или перенаправлять процессорное время с запуска приложения на освобождение памяти от других процессов, чтобы удовлетворить непосредственные потребности запускаемого приложения.
SELECT
p.name AS process_name,
max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.name = 'mem.rss.file'
GROUP BY p.name;
Режимы компиляции ART и память
Android Runtime (ART) может компилировать код приложения в одном из нескольких режимов, которые также называются фильтрами компилятора. Выбранный фильтр компилятора напрямую влияет на объем потребляемой памяти.
verify– ART выполняет только проверку байт-кода. AOT-компиляция не выполняется. Код выполняется с помощью интерпретатора или компилируется во время выполнения JIT-компилятором.- Влияние на память. Самый маленький размер на диске. Использование памяти нативного кода переносится в
JIT Cache(анонимная измененная память).
- Влияние на память. Самый маленький размер на диске. Использование памяти нативного кода переносится в
speed– ART выполняет полную AOT-компиляцию всех методов.- Влияние на память. Самый большой размер
.odex. Максимально увеличивает использование памяти, связанной с файлами (чистой).
- Влияние на память. Самый большой размер
speed-profileART компилирует только методы, помеченные как "горячие" в профиле JIT.- Влияние на память. Сбалансированный подход. Компилируется только самый важный код.
Самый распространенный фильтр – speed-profile, который используется при установке пользовательских приложений. Это настраивается в системных свойствах pm.dexopt.install и pm.dexopt.bg-dexopt и обычно задается в build/make/target/product/runtime_libart.mk.
Некоторые системные приложения будут использовать компиляцию speed, а также компилироваться во время сборки образа системы. verify обычно используется только в целях разработки.
| Пример использования | Типичный фильтр компилятора |
|---|---|
| Разработка | verify |
| Образ системы | speed |
| Пользовательские приложения | speed-profile |
Практическое упражнение: режимы компиляции и память
Чтобы посмотреть, как эти фильтры влияют на память, можно использовать приложение CodeBloat. Чтобы воспроизвести эти измерения:
- Принудительно перекомпилировать приложение в целевой режим.
- Принудительно остановите приложение и выполните холодный запуск.
- Дождитесь, пока фоновый поток закончит работу с классами (следите за logcat или подождите 5 секунд).
- Запустите
adb shell dumpsys meminfo com.android.codebloat.
Режим: verify (без AOT)
adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
В режиме verify в сводке приложения показано: * Code PSS: ~8000 КБ * Dalvik Other (JIT): ~25 000 КБ
Поскольку код не компилируется заранее, среда выполнения должна скомпилировать часто используемые методы в кеш JIT, который отображается как грязная анонимная память (Dalvik
Other).
Режим: speed (полная AOT-компиляция)
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
В режиме speed результаты значительно меняются: * Code PSS: ~24 000 КБ *
Dalvik Other (JIT): ~5000 КБ
Код приложения теперь сопоставляется с файлом .odex как чистая память, поддерживаемая файлом. Это снижает нагрузку на кеш JIT и позволяет освобождать память при необходимости, а не оставлять ее занятой.
Режим: speed-profile (выборочная AOT-компиляция)
Современные приложения могут включать базовый профиль baseline.prof. ART использует эти данные, чтобы выборочно компилировать только тот код, который необходим для быстрого запуска с эффективным использованием памяти.
В этом упражнении мы создадим базовый профиль, чтобы перечислить классы запуска приложения. Однако на практике компилятор может получать профили из внешних источников, например из магазина приложений (облачные профили). Такие профили JIT могут быть созданы на основе краудсорсинга независимо от того, добавил ли разработчик базовый профиль.
Создание и использование профилей на устройстве
Чтобы оценить влияние speed-profile, вы можете создать собственный профиль на устройстве:
Сбросить и начать.
adb shell am force-stop com.android.codebloatВзаимодействие. Запустите приложение и дождитесь, пока оно загрузится.
Dump Profile:
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)(Это заставит приложение записать текущий профиль на диск).
Установить профиль.
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profКомпиляция
adb shell cmd package compile -m speed-profile -f com.android.codebloat
При следующем запуске вы увидите, что Code PSS меньше speed (например, около 16 000 КБ), потому что скомпилированы были только методы, необходимые для запуска. Остальные будут обрабатываться интерпретатором или JIT-компилятором, только если они действительно используются.
См. также:
- Общие сведения о профилях базовой конфигурации
- Разница между профилями базовой конфигурации и профилями запуска
Подробный анализ скомпилированного кода
Если вы хотите посмотреть, какие именно инструкции генерирует ART, ознакомьтесь с art/DISASSEMBLY_GUIDE.md.
В нем содержатся подробные инструкции по использованию:
oatdump– чтобы посмотреть инструкции ARM64 в существующем файле.odex.dex2oat– имитация компиляции с подробными флагами отладки.
Упражнение: встраивание кода
Одна из причин, по которой скомпилированный код может неожиданно увеличиться, – это встраивание методов. Компилятор может решить скопировать тело небольшого, часто вызываемого метода непосредственно в вызывающие его объекты.
В нашем приложении CodeBloat метод doSomething() в каждом сгенерированном классе просто вызывает method0(). При компиляции в режиме speed оптимизирующий компилятор ART, скорее всего, встроит method0() в doSomething().
Упражнение. Проверьте это с помощью oatdump на своем устройстве:
# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk
adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]
# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
--class-filter=com.android.codebloat.GeneratedClass0
Найдите в выходных данных метод doSomething. Если она была встроена, вы увидите инструкции по загрузке длинной строковой константы непосредственно в doSomething, а не инструкцию bl, нацеленную на method0.
Визуализация оптимизации (CFG)
Чтобы точно узнать, когда компилятор решил встроить метод, можно создать граф потока управления. Это позволяет увидеть состояние кода на каждом этапе конвейера оптимизации, включая все преобразования промежуточного представления компилятора, пока код не будет преобразован в целевую архитектуру набора команд (например, ARM64).
Запустите
dex2oatс флагами дампа. Используйте флаг--verbose-methods, чтобы ограничить вывод определенными методами. В противном случае файл.cfgдля большого приложения может вырасти до нескольких гигабайт.# Substitution of actual paths required: adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \ --oat-file=/data/local/tmp/dump.odex \ --compiler-filter=speed \ --dump-cfg=/data/local/tmp/codebloat.cfg \ --verbose-methods=doSomethingИзвлечение и просмотр. Извлеките файл
.cfgна рабочую станцию и откройте его с помощью IR Hydra.Найдите Inliner. Загрузите артефакты компиляции в IR Hydra и выполните поиск по запросу
doSomething. Сравните представление до и после прохода Inliner. Вы увидите, как граф расширяется по мере того, как инструкции изmethod0объединяются в вызывающем объекте.
Вы также можете использовать инструмент Opt Pipeline в Compiler Explorer (как описано в разделе выше) и ввести аналогичный код, чтобы увидеть аналогичное преобразование, выполненное на этапе Inliner.
Упражнение: volatile-поля и барьеры памяти
В приложении MemoryLab поле mGarbageSink помечено как volatile. Это гарантирует, что компилятор не оптимизирует выделение памяти под мусор.
public volatile byte[] mGarbageSink;
В дизассемблированном коде ARM64 вы увидите, что каждое сохранение в это поле сопровождается барьером памяти (dmb ish) или использованием инструкций Load-Acquire/Store-Release (ldar/stlr). Это обеспечивает видимость потока, но добавляет несколько дополнительных инструкций к каждому доступу, немного увеличивая размер кода по сравнению с обычным полем.
Упражнение. Найдите в дизассемблированном коде обращения к полям и связанные с ними барьеры памяти.
Упражнение: неявные проверки приостановки
Если вы деассемблируете цикл, например тот, что в generateAllocationChurn, то заметите в конце тела цикла любопытную инструкцию:
ldr x21, [x21]
Это неявная проверка блокировки. ART использует это, чтобы позволить сборщику мусора безопасно приостанавливать потоки. Регистр x21 обычно указывает на самого себя.
Когда сборщику мусора нужно приостановить поток, он "отравляет" эту ячейку памяти. При следующем выполнении потоком этой инструкции ldr возникнет ошибка, которую среда выполнения перехватит и использует для перевода потока в приостановленное состояние.
Этот шаблон повторяется в каждом цикле и в начале каждого метода, что увеличивает общий размер кода приложения.
Упражнение. Найдите все неявные проверки приостановки в дизассемблированном методе и попробуйте сопоставить их с исходным кодом.
← WebView | ↑ Наверх | Потоки →