Как стать автором
Обновить

Комментарии 14

Хорошая статья. Написана простым языком - вроде читаешь про сложные вещи, но всё понятно.

Давно хотел узнать как в Swift происходит всё это, и теперь есть понимание.

Для себя подметил важный момент про то, что даже если объект уничтожен, память под него может быть выделена ещё, долгое время.

Буду ждать продолжения :)

Спасибо, наша команда старалась сделать этот материал простым для понимая! Насчет памяти - слишком затратно затирать ее нулями. Поэтому после того, как объект уже деаллоцирован, его “отпечаток” остается. Но затем этот кусок памяти может быть выделен снова. И только после этого в него записываются уже новые данные.

Прямо как на HDD.

Скорее всего SideTableMark, для каких-нибудь инструментов/анализаторов памяти.

Спасибо за статью!

В состоянии freed любое обращение к weak переменной вернет nil

Подскажите, как WeakReference узнает что происходит с объектом, если память освобождена и может быть перезаписана?

Спасибо! В состоянии freed указатель на объект, который хранится в WeakReference, затирается нулями. Дальше при обращении к weak переменной рантайм проверяет, что указатель нулевой и возвращает nil. Хорошее замечание, добавлю ответ в статью!

Статья мощная. Но остались нераскрытые вопросы.

А зачем же нам счетчики weak и unowned ссылок? Со strong понятно -- это счетчик жизни объекта, по нему мы понимаем, жив объект или уже мертв. А зачем считать weak и особенно unowned. Допустим, что weak нужен просто для валидации, что мы нашли в памяти нужное число ссылок для их обнуления. Хотя зачем может быть нужна такая валидация? А вот про unowned вообще не могу предположить, зачем...

Есть у Вас ответы на эти вопросы?

Спасибо за вопрос! Weak счетчик отмеряет время жизни side table. Все обращения к weak переменным происходят через side table и нужно понимать, когда можно удалить ее из памяти.

А unowned счетчик отмеряет время жизни памяти, выделенной под объект. Так как счетчик ссылок лежит внутри объекта и рантайм постоянно с ним работает, то память нельзя освобождать до момента, пока не останется никаких ссылок.
По сути, после обнуления weak счетчика можно спокойно уничтожать side table - к ней уже точно не будет обращений. А обнуление unowned счетчика гарантирует то, что больше не будет обращений к памяти объекта и ее можно очищать.
Можно после обнуление strong счетчика сразу освобождать память под объект. Но потом при обращении к unonwed переменной мы не получим исключение от рантайма, с информацией о том, к какой переменной мы обратились неправильно. Вместо этого мы будем видеть критическую ошибку обращения к освобожденной памяти от операционной системы. Как, собственно, сделано с unwoned переменными в Objective-C.

Надеюсь, мой комментарий помог вам понять необходимость наличия трех ссылок. Интересно, конечно, было бы спросить самих авторов этого алгоритма посчета.

Но погодите. У меня что-то не сходится.

HeapObject - это и есть выделенный нами объект, правильно? Вернее обязательная часть нашего объекта, содержащая isa и счетчик ссылок (ну или ссылку на side table). Верно? дальше за этой обязательной частью лежат хранимые переменные нашего объекта. И вот это все вместе - наш объект. Правильно?

Теперь, если у нас счетчик сильных ссылок стал равен 0, то мы можем освободить всю эту конструкцию из памяти, т.к.она нам не нужна. Но получается, что мы ее не можем освободить, т.к. нам нужна side table, по которой мы сможем сказать, что вот такой-то объект больше не существует. Т.е. получается, что объект живет в памяти не до тех пор, пока на него не осталось сильных ссылок, а он живет пока на него есть вообще хоть одна ссылка или хотя бы пока на него не осталось даже unowned ссылок? Ваша цитата: "А unowned счетчик отмеряет время жизни памяти, выделенной под объект." говорит об этом же?

Но погодите, тогда это полностью противоречит тому, что нам дает Apple. Точно ошибки в понимании логики нигде не произошло?

Да, HeapObject это и есть объект. И да, память под объект удаляется только после того, как unowned счетчик равен нулю. Вот ссылка на кейс в компиляторе, когда обвнуляется счетчик unowned ссылок: https://github.com/apple/swift/blob/17358e4d98f1cf0c5e8b2e8fd7bb740f663e6149/stdlib/public/runtime/HeapObject.cpp#L481. .

Для пользователя выглядит так, что объекта даже физически нет в памяти. Но чтобы обращаться к счетчику ссылок в рантайме, нужно держать память под него. Иначе счетчик пришлось бы выносить куда-то за пределы объекта. И в этом случае уже можно спокойно освобождать память под объект.

Но, это же будет противоречить и опыту и описанию Apple'а. Я четко вижу в своих приложениях, как память уменьшается, когда у меня еще есть unowned ссылки, но уже нет strong ссылок.

Это как-то можно объяснить?

Я не знаю, как Apple считает память в инструментах Xcode. Возможно, внутри себя инструменты смотрят на то, что на объект нет сильных ссылок и считают память под него освобожденной. Но в саму операционную систему она еще не вернулась.

Или представьте такой случай: strong счетчик обнулился и мы освободили память. При этом еще есть unowned ссылки и нужно отслеживать некорректное обращение к ним. И в следующий момент операционная система может что угодно записать в память, где лежит счетчик ссылок. Тогда алгоритм подсчета ссылок сломается и если где-то в коде будет обращение к деинициализированной unowned переменной, рантайм не сообщит нам точно об этом. Просто операционная система увидит, что мы обращаемся к освобожденной памяти и выдаст EXC_BAD_ACCESS.

Когда я исследовал алгоритм работы счетчика, я тоже не мог представить, что объект живет так долго) И таких неочевидных моментов в рантайме языка, я думаю, еще много.

И второй вопрос. Какие прикладные задачи теперь можно решать с этим знанием в финтех-проекте? Вы же не просто ради спортивного интереса реверс-инжинирили компилятор? :)

Лично я уже давно интересуюсь темой компиляторов, но никогда об этом не писал на хабр. А насчет применения в проекте - иногда приходят креши с очень странными стектрейсами. У нас был случай, когда в одной структуре мы использовали безобидный property wrapper и на проде приложение падало. Я собрал релизную сборку на своей машине и поймал этот креш. И уже через пару часов выяснилось, что компилятор сгенерировал вызов swift_release для поля структуры с типом String. Причем, над этим полем не было property wrapper и на сборке в оптимизациями все работает отлично. Тогда мы просто убрали этот property wrapper и перестали падать.

Именно понимание процесса подсчета ссылок помогло мне разобраться с этим крешем. Возможно, эта статья поможет еще кому-то справится с подобной проблемой. Да и просто интересно покапаться в реализации магических штуковин внутри Swift)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий