IL2CPP Stripping Removes Reflection‑Invoked Methods in Unity 2022.3 LTS Builds
0 reputation · 23 Nov 2021, 23:07 UTC
0 reputation · 23 Nov 2021, 23:07 UTC
Ensure that methods accessed only via reflection remain present in IL2CPP‑built players while keeping the build size impact minimal.
The Unity Editor (Mono) does not apply code stripping, so the same reflection calls succeed locally. In IL2CPP builds for Android/iOS, the linker treats reflection‑only members as unused and removes them unless explicitly preserved. Preservation mechanisms (link.xml, [Preserve] attribute, dummy static reference) must be correctly placed inside the Assets folder and scoped to the exact members needed; over‑preserving inflates the binary and defeats stripping benefits.
Questions
35275 reputation · 24 Nov 2021, 09:26 UTC
The likely cause is managed code stripping in IL2CPP builds. The Unity Editor runs Mono without stripping, so reflection works. IL2CPP runs the Unity linker, which removes types and members that have no static references. Reflection by string is invisible to that analysis. Confirmed: the supported fixes are a link.xml under Assets/ or [UnityEngine.Scripting.Preserve] on the exact type/member. Minimal preservation means preserve the declaring type and the specific reflected member, not the whole assembly or type with preserve='all'.
Use the narrowest entry that covers the reflection call. For a method invoked via Type.GetType + GetMethod + Invoke:
<linker>
<assembly fullname='Assembly-CSharp'>
<type fullname='MyGame.ReflectionTarget'>
<method name='Run'/>
</type>
</assembly>
</linker>For fields, use <field name='Value'/>. For constructors or property accessors, preserve the constructor or getter/setter method; if your Unity version's linker does not accept the member-level form, preserve='methods' on the type is the narrower fallback. If overloads are involved, use the signature form accepted by your Unity version, or preserve methods on the type. Mark this for review against your exact 2022.3 patch.
If you own the code, [Preserve] is often easier:
using UnityEngine.Scripting;
[Preserve]
public class ReflectionTarget
{
[Preserve] public void Run() { }
}Use link.xml for third-party assemblies you cannot edit. Do not use preserve='all' unless the type is small or you genuinely need every member; it also preserves nested types and inflates the binary.
Two silent failure modes matter. A link.xml outside Assets/, or with a wrong assembly/type full name, is ignored. [Preserve] applies only to assemblies processed by the Unity build pipeline; it will not preserve code in an external assembly you load dynamically. Check the assembly full name and namespace spelling.
No editor-only check can prove an IL2CPP build will keep a member, because the Editor does not run the linker. A practical pre-build gate is a reflection contract plus a coverage check:
link.xml under Assets/ and checks each registry entry for a matching assembly/type/member entry or [Preserve] attribute.Type.GetType, GetMethod, GetField, GetProperty, Activator.CreateInstance, and MakeGenericMethod. Treat this as a heuristic: concatenated or dynamic names will be missed.IPreprocessBuildWithReport when an entry is uncovered.The reliable runtime check is a development IL2CPP build with stripping enabled and a smoke test that exercises the reflection path, watching for MissingMethodException or TypeLoadException. That is after the build, but it confirms the preservation actually worked.
In Unity 2022.3 LTS, the link.xml and [Preserve] model is stable. The required entry per member does not change in newer LTS releases. What can change is the default Managed Stripping Level, engine code stripping, and linker diagnostics. A newer LTS with more aggressive defaults can expose missing entries that were previously hidden. Verify Player Settings > Other Settings > Optimization > Managed Stripping Level for each LTS you ship, and test the same reflection paths on each target.
If you share the exact reflection call—Type.GetType + GetMethod versus Activator.CreateInstance versus a serializer over properties—the minimal entry can be narrowed further. The type/member pattern above is the general rule; the exact call determines whether you need a method, field, constructor, or property accessor entry.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.