Quick answer: IL2CPP converts your C# code to C++ ahead of time. iOS does not allow JIT compilation, so any code path that relies on runtime code generation (such as certain generic type instantiations or reflection-heavy patterns) will crash.

Here is how to fix Unity il2cpp build crash on iOS. Your Unity project runs perfectly in the Editor and even builds cleanly for iOS — but the moment you launch on a real device, it crashes within seconds. The Xcode console shows an ExecutionEngineException or the app simply terminates with no managed exception at all. This is one of the most frustrating categories of Unity bugs because the crash only exists in IL2CPP builds on device, never in the Editor. The root cause is almost always related to code stripping, AOT generic limitations, or both.

The Symptom

You build your project with the IL2CPP scripting backend targeting iOS. The build completes without errors. You deploy to a physical iPhone or iPad through Xcode. The app launches, shows the splash screen, and then either crashes immediately on startup or crashes when you reach a specific feature — opening a menu, loading a save file, or instantiating a particular prefab.

In the Xcode device log, you see one of several error patterns. The most common is ExecutionEngineException: Attempting to call method 'SomeClass::SomeMethod' for which no ahead of time (AOT) code was generated. Other variants include TypeLoadException, MissingMethodException, or a raw EXC_BAD_ACCESS signal with no managed stack trace at all.

The critical detail is that none of this happens in the Unity Editor, which uses Mono with JIT compilation. The Editor can generate code on the fly for any generic instantiation or reflected type. iOS cannot — Apple does not allow JIT compilation on the platform, so every code path must be compiled ahead of time by IL2CPP.

What Causes This

There are three primary causes that account for the vast majority of IL2CPP iOS crashes, and they often compound each other.

1. Aggressive managed code stripping. Unity’s linker analyzes your code at build time and removes any types, methods, or entire assemblies it believes are unreachable. When the stripping level is set to High, the linker is very aggressive. If your game accesses types through reflection, string-based serialization (like JSON deserialization into generic containers), or Activator.CreateInstance, the linker cannot see those access patterns statically and will strip the code. On device, the app crashes the moment it tries to use the stripped type.

2. Missing AOT generic instantiations. IL2CPP must generate C++ code for every concrete generic type combination your code uses. If you have List<MyCustomStruct>, IL2CPP needs to see that specific instantiation referenced somewhere in your code. Value-type generics are particularly problematic because IL2CPP cannot share implementations across value types the way it can with reference types. If a generic instantiation only appears through reflection or is constructed dynamically at runtime, IL2CPP will not generate code for it.

3. Third-party plugin incompatibility. Many Unity plugins were originally written for Mono and rely on patterns that do not survive IL2CPP compilation. Plugins that use System.Reflection.Emit, runtime proxy generation, or dynamic assembly loading will fail on iOS. Some plugins include their own link.xml files but miss edge cases specific to your usage.

The Fix

Step 1: Lower the stripping level and create a link.xml file. Start by setting the managed stripping level to Minimal in Player Settings. This alone fixes many crashes. Then create a link.xml file in your Assets folder to explicitly preserve types you know are needed at runtime.

<!-- Assets/link.xml -->
<!-- Preserve types that are accessed via reflection or serialization -->
<linker>
  <!-- Preserve an entire assembly -->
  <assembly fullname="System.Core" preserve="all"/>

  <!-- Preserve specific types in your game assembly -->
  <assembly fullname="Assembly-CSharp">
    <type fullname="SaveSystem.SaveData" preserve="all"/>
    <type fullname="Inventory.ItemDefinition" preserve="all"/>
    <type fullname="Analytics.EventPayload" preserve="all"/>
  </assembly>

  <!-- Preserve JSON serialization support -->
  <assembly fullname="Newtonsoft.Json" preserve="all"/>
</linker>

The preserve="all" attribute tells the linker to keep every type and method in the assembly or type, even if it cannot find a static reference to them. Use this surgically — preserving everything increases build size, so prefer preserving specific types when you can identify which ones are being stripped.

Step 2: Force AOT compilation of generic types. Create a static method that explicitly references every generic type combination your code needs at runtime. This method never needs to be called — it just needs to exist so IL2CPP can see the instantiations.

using System.Collections.Generic;
using UnityEngine;

// This class forces IL2CPP to generate AOT code for generic types
// that are only referenced via reflection or deserialization.
public static class AotTypeEnforcer
{
    // This method is never called. Its existence forces IL2CPP
    // to compile these generic instantiations ahead of time.
    private static void EnsureGenericTypes()
    {
        // Value-type generics that your serializer uses
        var a = new List<int>();
        var b = new List<float>();
        var c = new List<Vector3>();
        var d = new Dictionary<string, int>();
        var e = new Dictionary<int, List<Vector3>>();

        // Custom struct generics used in your save system
        var f = new List<SaveSystem.SaveData>();
        var g = new Dictionary<string, Inventory.ItemDefinition>();

        // Nullable value types
        System.Nullable<int> h = null;
        System.Nullable<float> i = null;
        System.Nullable<Vector2> j = null;
    }
}

Pay particular attention to nested generics and value-type generics. Dictionary<string, List<int>> is a different instantiation from Dictionary<string, int> and from Dictionary<int, string>. Each combination needs to be referenced explicitly. Reference types like List<string> are less likely to cause issues because IL2CPP can share the implementation, but value types cannot be shared.

Step 3: Read the Xcode device log to identify the exact crash. Before guessing which types are affected, connect your device and read the crash log. This tells you exactly which type or method IL2CPP failed to resolve.

// In Unity, add this early in your startup sequence to catch
// IL2CPP exceptions before they crash the app silently.
using UnityEngine;

public class CrashDiagnostics : MonoBehaviour
{
    void Awake()
    {
        Application.logMessageReceived += OnLogMessage;
    }

    private void OnLogMessage(string condition, string stackTrace,
                              LogType type)
    {
        if (type == LogType.Exception || type == LogType.Error)
        {
            // Write to a file the player can send you, or
            // send to your crash reporting service.
            Debug.Log($"[CRASH DIAG] {type}: {condition}");
            Debug.Log($"[CRASH DIAG] Stack: {stackTrace}");

            // Flush to disk so it survives the crash
            System.IO.File.AppendAllText(
                Application.persistentDataPath + "/crash.log",
                $"{System.DateTime.Now}: {condition}\n{stackTrace}\n\n"
            );
        }
    }
}

In Xcode, open Window > Devices and Simulators, select your device, and click Open Console. Run the app and watch for lines containing ExecutionEngineException, TypeLoadException, or MissingMethodException. The message typically includes the full type name and method signature that failed, which you can then add to your link.xml or AOT enforcer class.

Why This Works

Each fix targets a different stage of the IL2CPP compilation pipeline.

Lowering stripping and using link.xml prevents the Unity linker from removing code during the bytecode analysis phase. The linker runs before IL2CPP converts your C# to C++. By marking types and assemblies as preserved, you ensure they survive into the C++ generation stage. Without preservation, the linker sees no static call path to a type used only through reflection and removes it entirely.

Forcing generic instantiations works because IL2CPP scans all reachable code for generic type usages during compilation. Even an uncalled method counts as reachable code if the class is preserved. When IL2CPP sees new Dictionary<int, List<Vector3>>() in your AOT enforcer, it generates the specialized C++ implementation for that exact type combination. Without that reference, the runtime attempts to construct the type and finds no compiled implementation, triggering the ExecutionEngineException.

Reading the device log turns a mysterious crash into an actionable fix. The exception message from IL2CPP is specific — it names the exact method or type that failed. Without this information, you are guessing which types to preserve, which can lead to either preserving too little (still crashing) or preserving everything (doubling your build size).

Common Pitfalls

JSON serialization libraries are the single largest source of IL2CPP crashes in indie games. Libraries like Newtonsoft.Json use extensive reflection to construct objects from JSON strings. Every class you deserialize must be preserved in link.xml. Consider using Unity’s built-in JsonUtility for simple cases, which does not rely on reflection and survives stripping without any configuration.

Third-party analytics and ad SDKs sometimes use reflection internally in ways you cannot easily predict. If you crash inside a plugin’s namespace, check whether the plugin ships its own link.xml. If it does not, add the plugin’s assembly to your link.xml with preserve="all" as a starting point, then narrow it down once stable.

Incremental builds can mask the problem. If you fix a crash by adding a type to link.xml but the crash persists, do a clean build. IL2CPP caches intermediate C++ files and sometimes does not regenerate them when link.xml changes. Delete the Library/il2cpp_cache folder in your project and rebuild.

// Quick validation script: run this on device startup to verify
// critical types survived stripping.
using System;
using UnityEngine;

public class StrippingValidator : MonoBehaviour
{
    void Start()
    {
        ValidateType("SaveSystem.SaveData");
        ValidateType("Inventory.ItemDefinition");
        ValidateType("Newtonsoft.Json.JsonConvert");
    }

    private void ValidateType(string typeName)
    {
        Type t = Type.GetType(typeName);
        if (t == null)
        {
            Debug.LogError($"STRIPPED: {typeName} was removed by the linker!");
        }
        else
        {
            Debug.Log($"OK: {typeName} is present.");
        }
    }
}

Related Issues

If your IL2CPP build compiles but you get runtime errors around System.Reflection.Emit, the issue is not stripping but a fundamental API limitation on iOS. No amount of link.xml configuration will fix that — you need to replace the emit-based code with a different approach. If your build fails during the C++ compilation stage rather than on device, check the Xcode build output for C++ compiler errors, which indicate an IL2CPP bug rather than a stripping issue.

When in doubt, read the Xcode log. The exception always names the guilty type.