В этой статье мы поговорим о взаимодействии среды выполнения IL2CPP со сборщиком мусора и увидим, каким образом корни сборщика мусора в управляемом коде связываются с нативным сборщиком мусора.
Предыдущие материалы темы:
IL2CPP: Обертки P/Invoke для типов и методов
IL2CPP: вызовы методов
В этой статье, как и в предыдущих публикациях серии, мы раскроем детали реализации отдельного компонента IL2CPP, которые могут быть изменены в будущем. Рассмотрим некоторые внутренние API, используемые кодом среды выполнения для взаимодействия со сборщиком мусора. Эти API не являются публичными, так что не следует пытаться вызывать их из кода какого-нибудь реального проекта.
Сборка мусора
Я не буду приводить здесь общую информацию о сборке мусора, так как это довольно широкая тема, которой посвящено много исследований и публикаций. Для краткости представим сборщик мусора в виде алгоритма, который занимается построением направленных графов из ссылок на объекты. Если объект Child используется объектом Parent посредством указателя в нативном коде, то граф будет иметь такой вид:
Сборщик мусора ищет в памяти, используемой процессом, объекты без родительского объекта. Если такой объект найден, то занимаемая им память может быть освобождена и повторно использована с другой целью.
Конечно, большинство объектов должны иметь какой-нибудь родительский объект. Поэтому сборщику мусора нужно уметь отличать особые родительские объекты. Я предпочитаю думать о последних, как об объектах, используемых программой. В терминологии сборщика мусора они называются «корнями». Ниже приведен пример родительского объекта без корня.
В данном случае у объекта Parent 2 нет корня, поэтому сборщик мусора может повторно использовать память, занимаемую объектами Parent 2 и Child 2. В свою очередь, Parent 1 и Child 1 имеют корень – а значит они используются программой, и сборщик мусора не будет повторно использовать их память, так как программа всё еще использует их для определенной цели.
В .NET используются корни трех видов:
- локальные переменные в стеке любого потока, выполняющего управляемый код;
- статические переменные;
- объекты GCHandle.
Мы рассмотрим взаимоотношения IL2CPP со сборщиком мусора при работе с корнями всех перечисленных выше видов.
Подготовка к работе
Я работаю в Unity версии 5.1.0p1 на OSX, и буду осуществлять сборку для платформы iOS. Это позволит нам использовать Xcode для наблюдения за взаимодействием IL2CPP со сборщиком мусора. Как и в предыдущих примерах, мы будем использовать проект, содержащий один скрипт:
using System; using System.Runtime.InteropServices; using System.Threading; using UnityEngine; public class AnyClass {} public class HelloWorld : MonoBehaviour { private static AnyClass staticAnyClass = new AnyClass(); void Start () { var thread = new Thread(AnotherThread); thread.Start(); thread.Join(); var anyClassForGCHandle = new AnyClass(); var gcHandle = GCHandle.Alloc(anyClassForGCHandle); } private static void AnotherThread() { var anyClassLocal = new AnyClass(); } }
Я отметил опцию Development Build в окне Build Settings, и выбрал Debug напротив Run in Xcode as. В сгенерированном проекте Xcode прежде всего найдите строку Start_m. Вы должны увидеть сгенерированный код для метода Start класса HelloWorld под названием HelloWorld_Start_m3.
Добавляем потоковые локальные переменные в качестве корней
Добавим точку останова в функции HelloWorld_Start_m3 на строке, где вызывается Thread_Start_m9. Этот метод создает новый управляемый поток, который будет добавлен в качестве корня в сборщик мусора. Этот процесс можно отследить в заголовочных файлах libil2cpp, поставляемых вместе с Unity. В директории установки Unity откройте файл Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h. Он содержит ряд методов с префиксом il2cpp_gc_ и является частью API между средой выполнения libil2cpp и сборщиком мусора. Но помните, что этот API общедоступен, потому данные методы не следует вызывать из кода реального проекта. К тому же они могут быть изменены в новой версии без уведомления.
Добавим точку останова в функции il2cpp_gc_register_thread в Xcode. Для этого нужно выбрать Debug > Breakpoints > Create Symbolic Breakpoint.
Эта точка достигается практически мгновенно после запуска проекта в Xcode. В данном случае мы не видим исходного кода, так как он построен в статической библиотеке среды libil2cpp, однако из стека вызовов ясно, что этот поток создается в методе InitializeScriptingBackend, который выполняется при запуске.
Мы увидим, что эта точка будет достигаться несколько раз по мере создания управляемых потоков для внутреннего использования. Пока что ее можно отключить в Xcode и продолжить выполнение проекта без нее. Мы должны достичь точку останова, которая была добавлена ранее в методе HelloWorld_Start_m3.
Теперь я собираюсь запустить управляемый поток, созданный нашим скриптовым кодом, поэтому нужно снова включить точку останова на il2cpp_gc_register_thread. Достигнув ее, мы увидим, что первый поток ожидает присоединения к созданному потоку, но стек вызовов для созданного потока показывает, что мы только запускаем его:
Когда новый поток связывается со сборщиком мусора, последний интерпретирует все объекты в локальном стеке этого потока как корни. Взглянем на сгенерированный код для метода HelloWorld_AnotherThread_m4:
AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var); AnyClass__ctor_m0(L_0, /*hidden argument*/NULL); V_0 = L_0;
Мы видим одну локальную переменную L_0, которую сборщик мусора должен интерпретировать как корневую. В течение недолгого времени, пока существует этот поток, данный экземпляр объекта AnyClass и любые другие объекты, на которые он ссылается, не могут быть повторно использованы сборщиком мусора. Определенные в стеке переменные – это наиболее распространенный тип корня, так как объекты в программе преимущественно начинаются с локальной переменной в методе, исполняемом в управляемом потоке.
При завершении потока вызывается функция il2cpp_gc_unregister_thread, которая указывает сборщику мусора больше не интерпретировать объекты стека потока как корни. После этого сборщик мусора сможет повторно использовать память, занимаемую объектом класса AnyClass, который представлен в нативном коде как L_0.
Статические переменные
Некоторые переменные не зависят от потоковых стеков вызовов. Речь идет о статических переменных, и они тоже должны интерпретироваться сборщиком мусора как корневые.
Когда IL2CPP создает нативное отображение класса, все статические поля группируются в структуру C++, отличную от экземпляров полей в классе. Перейдем к определению класса HelloWorld_t2 в Xcode:
struct  HelloWorld_t2  : public MonoBehaviour_t3 { }; struct HelloWorld_t2_StaticFields{ // AnyClass HelloWorld::staticAnyClass AnyClass_t1 * ___staticAnyClass_2; };
Обратите внимание, что технология IL2CPP не использует ключевое слово C++ static, поскольку она должна постоянно контролировать размещение статических полей, а также выделение памяти для них, чтобы нужным образом взаимодействовать со сборщиком мусора. Когда определенный тип впервые используется в среде выполнения, код libil2cpp выполнит инициализацию типа. Такая инициализация включает в себя выделение памяти для структуры HelloWorld_t2_StaticFields. Память выделяется с помощью специального вызова к сборщику мусора: il2cpp_gc_alloc_fixed (его можно увидеть в файле gc-internal.h).
После данного вызова сборщик мусора будет принимать выделенную память за корень до окончания процесса. Можно установить точку останова на функции il2cpp_gc_alloc_fixed в Xcode, но она вызывается довольно часто (даже в таком простом проекте, как наш), потому не будет очень полезной.
Объекты GCHandle
В некоторых случаях нежелательно использовать статические переменные, но при этом необходимо контролировать, когда именно сборщик мусора может повторно использовать выделенную для объекта память. К примеру, вам нужно передать указатель управляемого объекта из управляемого кода в нативный. Если нативный код получает возможность распоряжаться этим объектом, нам нужно сообщить сборщику мусора, что нативный код является сейчас корнем в его объектном графе. Для этого используется специальный управляемый объект GCHandle.
При создании объекта GCHandle код среды обработки начинает интерпретировать выбранный управляемый объект как корень в сборщике мусора, чтобы не допустить повторное использование памяти ни этого объекта, ни любого другого, на который он ссылается. В IL2CPP мы видим, как низкоуровневый API выполняет это в файле Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h. Опять же, напоминаю, что этот API не является публичным. Добавим точку останова на функцию GCHandle::New. Если мы продолжим выполнение проекта, должен появиться такой стек вызовов:
Сгенерированный код для метода Start вызывает метод GCHandle_Alloc_m11, который в итоге создает объект GCHandle и уведомляет сборщик мусора о новом корневом объекте.
Вывод
Тема интеграции сборщика мусора в IL2CPP по-прежнему далеко не исчерпана. Я настоятельно рекомендую читателям самостоятельно изучать больше материала о взаимодействии IL2CPP и сборщика мусора.
ссылка на оригинал статьи https://habrahabr.ru/post/325034/
Добавить комментарий