Обновление библиотеки до седьмой версии обернулось списком предупреждений на пол-экрана: мой диммер 2021 года держал память на StaticJsonDocument<256>, соседний скетч с гистерезисом — на DynamicJsonDocument. Я уже потянулся править скетч, а сборка в этот момент закончилась словом «успешно». Это первое, что стоит понять про седьмую версию: устаревшие имена не выброшены — они работают и светят DeprecationWarning. Компилятор ведёт за руку, но не стоит над душой: скетч со старыми декларациями собирается, прошивается и крутится дальше.
Механику переезда и словарик пятой версии разобрал соседний урок — про перенос старых скетчей на v7; здесь другой срез — память. Главный сдвиг седьмой версии помещается в одну строку: пул документа всегда живёт в куче, и документ растёт сам. Из этой строки вытекает всё остальное: ёмкость в декларации потеряла смысл, memoryUsage() сменил показания, а правило «полтора-два размера строки», по которому мы считали ёмкость в уроке про расчёт ёмкости, осталось верным как оценка заказа — но перестало быть лимитом.
Разберём по порядку: куда делись байты Static и Dynamic, что именно сообщает компилятор устаревшими декларациями, таблица переезда по классам — и моё мнение о том, когда обновление честнее отложить: на ESP8266 с её восьмьюдесятью килобайтами ОЗУ спешить некуда.
Пул переехал в кучу
В шестой версии декларация была обещанием плате. StaticJsonDocument<256> вырезал двести пятьдесят шесть байт из статической области ещё на компиляции: место занято с момента запуска и навсегда, зато куча не трогалась вовсе. DynamicJsonDocument звал malloc при создании и просил всю ёмкость разом — не нашлось свободного куска в двести пятьдесят шесть байт подряд, лови NoMemory, хотя свободных байт в сумме могло хватать с запасом. Отсюда вся дисциплина шестой версии: считай ёмкость до прошивки, держи документы глобальными и переиспользуй их, а не создавай в цикле.
Седьмая обещание отменила. JsonDocument doc; не резервирует ни байта: первый узел дерева тянет за собой первый кусок пула, не хватило — документ берёт у кучи следующую порцию, и каждая новая крупнее предыдущей. Внутри это связный список кусков; выросший состав живёт, пока документ не очищен или не умер со своей областью видимости. Один и тот же скетч разбирает пакет в сорок байт и пакет в четыреста — декларация не меняется, угадывать нечего. Механику нового документа мы проходили в уроке про JsonDocument; здесь важна цена.
Цена — предсказуемость. В шестой верхней границей была цифра в скобках: знаю максимум — сплю спокойно. В седьмой граница одна — свободная куча, и бюджет документа считается по наихудшему пакету, а не по декларации. Отсюда и смещение смысла NoMemory, разобранное в уроке про коды разбора: в шестой код означал «увеличь цифру в скобках», в седьмой — «кончилась куча», и список подозреваемых другой: не декларация, а соседние String-буферы и прочие едоки ОЗУ.
#include <ArduinoJson.h> // Седьмая версия: одна декларация без ёмкостиJsonDocument doc; // Устаревшие написания тоже собираются — с DeprecationWarning:// StaticJsonDocument<256> oldStatic; // предупреждение, цифра игнорируется// DynamicJsonDocument oldDynamic(256); // предупреждение, ёмкость игнорируется String packet = "{\"name\":\"nasos\",\"on\":true,\"power\":1450}"; void setup() { Serial.begin(9600); DeserializationError err = deserializeJson(doc, packet); if (err) { Serial.print("razbor sorvan: "); Serial.println(err.c_str()); return; } Serial.print("dlina pechati: "); Serial.println(measureJson(doc)); Serial.print("pul zanyal: "); Serial.print(doc.memoryUsage()); Serial.println(" bajt"); Serial.print("name = "); Serial.println((const char*)doc["name"]); Serial.print("power = "); Serial.println((int)doc["power"]);} void loop() {}На пакете из скетча печать занимает тридцать девять байт, а пул — в полтора-два раза больше: слоты на каждый член объекта, копии строк, служебные поля. Пропорция та самая, по которой в шестой версии считали ёмкость; разница в том, кто и когда платит. В v6 всё это место выделялось заранее и целиком, в v7 — кусочками по мере роста дерева, и memoryUsage() показывает фактический расход в момент вызова, а не потенциальный максимум: очистили документ — показание обнулилось.
Теперь про ловушку предупреждений. DeprecationWarning не блокирует сборку — проект собирается и работает, и соблазн оставить его в таком виде навсегда велик. Я держу два правила. Первое: не гасить предупреждения флагами компилятора — deprecated-список бесплатно ведёт по точкам правки. Второе: помнить, что StaticJsonDocument<256> в седьмой версии — уже не статика: цифра двести пятьдесят шесть игнорируется целиком, никакого лимита нет. Код выглядит как раньше, а места пул берёт ровно столько, сколько съел разбор.
Старая цифра из скобок не стала бессмысленной — она перестала быть лимитом, и разницу стоит проговорить числами. StaticJsonDocument<256> из старого скетча держал запас в шесть с лишним раз против печати пакета выше, а на Uno с двумя килобайтами ОЗУ такая декларация — восьмая часть всей памяти, оплаченная на угадывание. Пулу седьмой версии под тот же состав хватает полутора-двух размеров печати — меньше восьмидесяти байт, — и обе цифры лежат на ладони: measureJson(doc) показывает, сколько пакет едет по проводу, doc.memoryUsage() — сколько пул занял под него. На живом узле я печатаю вторую в журнал раз в час: стоит на месте — бюджет сошёлся; ползёт от разбора к разбору — ищите размножающиеся String-буферы по соседству, а не библиотеку.
Что во что превратилось
| Было в v6 | Стало в v7 | Что это меняет в памяти |
|---|---|---|
| StaticJsonDocument<256> doc; | JsonDocument doc; | вырез из статики уходит: место берётся у кучи по потребности, ни байта до первого узла |
| DynamicJsonDocument doc(256); | JsonDocument doc; | malloc всей ёмкости разом заменён ростом кусочками: NoMemory из-за размера декларации не бывает |
| doc.memoryUsage() — занятое от заявленной ёмкости | то же имя, фактический расход кучи | очистили документ — показание обнулилось; ориентир для бюджета, а не лимит |
| NoMemory: «увеличь цифру в скобках» | overflowed() и рост до свободной кучи | срыв сборки дерева ловится флагом до печати, а не обрезкой пакета |
Моё правило обновления звучит непопулярно: рабочую прошивку с посчитанными ёмкостями я не трогаю. Если скетч на Uno годами живёт на StaticJsonDocument со статикой, шестой версии нечего стыдиться: статическая область не фрагментируется, верхняя граница известна с компиляции, а выигрыш от роста кусочками там, где дерево и так влезает в ёмкость, — нулевой. Переезд имеет смысл, когда ответ сервера плавает по длине, когда нужны чужие примеры на v7 или когда проект живой и будет расти. Если ваш пакет всегда одинаковый и память сходится до байта — все мои доводы за седьмую повиснут в воздухе.
Отдельный разговор — ESP8266. Восемьдесят килобайт ОЗУ, из которых скетчу реально достаётся сорок-пятьдесят, остальное держит Wi-Fi-стек, уже порезавший кучу на куски разного размера. Шестая версия со статическими документами на этой плате предсказуемее: она не добавляет фрагментации, тогда как v7 при каждом росте ищет в битой куче место под очередную порцию. Рядом живёт и String приёма, перевыделяющаяся на каждом росте, — её повадки мы сравнивали в уроке про склейку строк. На ESP8266 со стабильной прошивкой я остаюсь на v6 и потери не чувствую; на свежем проекте — сразу v7, без этапа шестой.
Одна ловушка переезда живёт не в коде, а в Library Manager: библиотека на машине одна, и кнопка «обновить» пересаживает на седьмую семантику сразу все скетчи — старые имена работают, но пул уже в куче и растёт сам, а смешать шестую и седьмую в одном проекте нельзя: заголовок один. Поэтому я обновляю не по кнопке, а по списку: какие скетчи реально ходят в JSON, каждый — на контрольном пакете, рядом со старой ёмкостью — memoryUsage(), длины печати до и после совпадают до байта. Скетчи без JSON к переезду отношения не имеют, и вечера на них тратить незачем. Мой график: живые проекты — сразу на седьмую, рабочие прошивки — по расписанию обслуживания и не раньше.
Рост вживую
Движок книги понимает оба написания — и JsonDocument без ёмкости, и DynamicJsonDocument со скобками, — поэтому в симуляторе ниже седьмая декларация разбирает три поколения пакета одного и того же сервиса. Честная оговорка: ёмкость движок не эмулирует, переполнения вы в симе не увидите. На настоящей плате шестой версии StaticJsonDocument<128> на третьем пакете упал бы с NoMemory — и это тот случай, когда угаданная впритык ёмкость подписывает вас на тихий отказ через полгода, как только сервер добавит поле.
Попробуйте по очереди все три строки из подсказки: печать measureJson растёт на глазах, декларация одна и та же, ни одна цифра в скобках не пересматривалась. LED на D12 загорается, когда в пакете нашлось поле uv — примета свежего поколения; кнопка D2 чистит документ, и куски пула возвращаются куче. На плате та же логика выглядит скучнее: объявили один раз — дальше сколько ни шлёт сервер, растёт сам. Установка обеих версий разобрана в уроке про установку v7, а если захочется строить показания в историю — пригодится и урок про суточный журнал.
- 1. Что занимает декларация JsonDocument doc; в седьмой версии до первого узла дерева?
- 2. StaticJsonDocument<256> в седьмой версии — что происходит с цифрой?
- 3. Старый скетч с DynamicJsonDocument собирают на седьмой версии. Что произойдёт?
- 4. Когда я оставляю проект на шестой версии?
- 5. Что показывает memoryUsage() в седьмой версии?
- Пул седьмой версии живёт в куче и растёт кусочками: JsonDocument doc; не резервирует ни байта до первого узла
- StaticJsonDocument и DynamicJsonDocument работают как псевдонимы с DeprecationWarning: сборка не падает, цифра ёмкости игнорируется
- memoryUsage() показывает фактический расход кучи, overflowed() ловит срыв сборки до печати
- На стабильных прошивках с посчитанными ёмкостями — особенно на ESP8266 с потревоженной Wi-Fi кучей — v6 держу сознательно
- Контроль переезда: список предупреждений, memoryUsage рядом со старой ёмкостью, длины печати до и после совпадают до байта
Частые вопросы по теме урока
- Что занимает декларация JsonDocument doc; в седьмой версии до первого узла дерева?
- Первый кусок пула выделяется под первый узел, дальше — по потребности. Декларация ничего не резервирует: это обычная переменная, а не обещание плате. Ни байта: пул растёт кусочками по мере роста дерева
- StaticJsonDocument<256> в седьмой версии — что происходит с цифрой?
- Устаревшее имя стало псевдонимом JsonDocument: ёмкость не читается вовсе. Код выглядит как раньше, но лимита за цифрой больше нет. Она игнорируется целиком
- Старый скетч с DynamicJsonDocument собирают на седьмой версии. Что произойдёт?
- Устаревшие имена работают через псевдонимы: предупреждение — подсказка, а не приговор. Список warning-строк удобно использовать как чек-лист переезда. Соберётся с DeprecationWarning и заработает
- Когда я оставляю проект на шестой версии?
- Рабочую прошивку с честными ёмкостями трогать незачем: v6-статика не фрагментирует кучу. Порог переезда — плавающая длина ответов, чужие примеры v7 или живой проект. Когда прошивка стабильна на посчитанных ёмкостях, а куча и так потревожена — например ESP8266 с Wi-Fi-стеком
- Что показывает memoryUsage() в седьмой версии?
- Ёмкости больше нет — показание стало фактическим: сколько пул занял под текущий состав. Очистка документа возвращает цифру к нулю. Фактический расход кучи пулом в момент вызова