Objective-C

MRC and ARC Mixed Compilation Adaptation Rules

By Seren  |  13 Aug, 2026  |  Leave a comment


Mixing ARC and MRC in a single Xcode project sounds like a recipe for disaster, but sometimes it is the only option when inheriting legacy codebases or integrating third-party libraries. Later, when I inherited a legacy project, I discovered it contained a bunch of MRC third-party libraries, and compilation immediately failed. After researching, I learned that ARC and MRC can coexist in the same project — but you need to tell the compiler which parts use which rules. Without that, the compiler applies ARC rules to MRC code uniformly, and calls to release and autorelease cause compilation errors.

Basic Adaptation: Specifying Rules at the Compilation Stage

The core approach to mixing ARC and MRC is specifying compilation options per file. In Xcode’s Build Phases -> Compile Sources, double-click a .m file and add the compilation flag in the popup.

  • ARC project with MRC files: Add the -fno-objc-arc flag to MRC files. This tells the compiler: “Don’t use ARC for this file — process it with MRC rules.”
  • MRC project with ARC files: Add the -fobjc-arc flag to ARC files. This tells the compiler: “This file needs ARC.”

If there are many MRC files, adding flags one by one gets tedious. Consider compiling those MRC files into a static library (.a) and linking it as a binary dependency. Static libraries have their memory management already determined at compile time — the main project’s memory management mode doesn’t affect them.

Code-Level Adaptation: Conditional Compilation

Sometimes, the same code needs to compile under both ARC and MRC. You can use conditional compilation to detect the environment and execute different branches accordingly.

#if __has_feature(objc_arc)
    // Code under ARC
    #define ZX_AUTORELEASE(exp) exp
    #define ZX_RELEASE(exp) exp
    #define ZX_RETAIN(exp) exp
#else
    // Code under MRC
    #define ZX_AUTORELEASE(exp) [exp autorelease]
    #define ZX_RELEASE(exp) [exp release]
    #define ZX_RETAIN(exp) [exp retain]
#endif

With this macro, ZX_RELEASE(obj) does nothing under ARC but calls [obj release] under MRC. The same code works in both environments.

Cross-Language Bridging: ARC and Core Foundation

Beyond mixing ARC and MRC, there’s another common scenario: memory management between ARC and Core Foundation (C-style APIs). CF objects aren’t controlled by ARC and require manual reference counting. When you get a CF object in ARC code, ARC doesn’t know its reference count status — you need __bridge keywords to tell the compiler how to handle it.

// Get object from CF, ARC takes ownership
CFStringRef cfString = CFStringCreateWithCString(NULL, "Hello", kCFStringEncodingUTF8);
NSString *nsString = (__bridge_transfer NSString *)cfString;
// Now nsString is managed by ARC — no manual CFRelease needed

// Pass object from ARC to CF, ARC hands over ownership
NSString *nsString2 = @"World";
CFStringRef cfString2 = (__bridge_retained CFStringRef)nsString2;
// Now cfString2 is manually managed by the developer — must call CFRelease
// ... use cfString2 ...
CFRelease(cfString2);

__bridge simply passes the reference without changing ownership. __bridge_retained transfers ownership from ARC, giving the CF side a +1 reference count. __bridge_transfer makes ARC take ownership of the CF object and manage its release automatically.

The most critical lesson I learned from maintaining mixed ARC/MRC projects is to never assume the compiler will protect you. When I first added -fno-objc-arc to an MRC file, I thought the problem was solved. But two weeks later, a crash appeared because a category on that MRC file was still being compiled with ARC rules — Xcode applies the flag to the base file, not to its categories. I had to manually add the flag to every category file as well. Now I always search the entire project for related files before changing any compilation flag.

Another practical tip: when migrating an MRC project to ARC incrementally (which is the recommended approach), start with leaf files that don’t depend on others. Migrate one file at a time, run all tests, then move to the next. Trying to migrate everything at once is a recipe for hours of debugging. The conditional compilation macros I described earlier are invaluable during this gradual migration — they let you test the same code under both regimes without maintaining two separate branches.

Two Common Misconceptions

Misconception 1: MRC automatically converts under ARC. It doesn’t. MRC code’s release and autorelease are disabled under ARC — compiling directly without the -fno-objc-arc flag will cause compilation errors.

Misconception 2: Static libraries don’t have ARC/MRC issues. Static libraries have their memory management determined at compile time. The main project’s memory management mode doesn’t affect them.

A real-world scenario I encountered: a client’s SDK was compiled with MRC and distributed as a static library. When we linked it into our ARC project, everything compiled fine — but we got memory leaks because the SDK’s internal delegate callbacks weren’t releasing objects properly. The root cause was that the SDK developer had written the code under ARC assumptions but compiled it as MRC, creating a mismatch. The fix involved wrapping the entire SDK interface in a thin ARC adapter layer that handled all the retain/release calls manually. This taught me that even when the build system says everything is fine, you still need to verify memory behavior at runtime using Instruments’ Leaks and Allocations tools. Never trust the compiler alone when mixing memory management regimes.

For teams starting fresh, my strong recommendation is: never start a new project in MRC. Even if you’re working with legacy code, migrate incrementally rather than maintaining a mixed codebase long-term. The maintenance overhead of tracking which files need which flags, managing conditional compilation macros, and debugging ownership issues across ARC/MRC boundaries far outweighs the one-time cost of an incremental migration. Tools like the Xcode build settings analyzer and static analyzers can help identify files that are candidates for migration, making the process faster and safer than doing it manually.

Summary

Adapting ARC and MRC mixed compilation can be broken down into three layers:

In my experience, the biggest headache with ARC/MRC mixed projects is tracking down ownership bugs that only appear under specific conditions. I once had a crash that happened only on iOS 13 devices, because a third-party MRC library was returning objects without proper retain counts. The fix was marking that specific file with -fno-objc-arc and adding manual [obj retain] calls in the wrapper layer. The lesson: always wrap third-party MRC code in an ARC-compatible interface — don’t let MRC leak into your business logic.

For teams maintaining mixed projects, I recommend establishing a clear convention: all new code must be ARC, all MRC files must live in a separate directory with a README explaining why, and every MRC file must have a comment at the top indicating manual review is required. This way, new developers know exactly which files they’re not allowed to touch without understanding the MRC rules first.

  • Compilation level: Use -fno-objc-arc to mark MRC files and -fobjc-arc to mark ARC files. Packaging many MRC files into a static library is also a viable approach.
  • Code level: Use __has_feature(objc_arc) for conditional compilation, making code compatible with both memory management rules.
  • Bridging level: Use __bridge / __bridge_retained / __bridge_transfer to manage object ownership between ARC and CF.

GitHub source code address:https://github.com/2252408699/MRCARCIntegrationDemo.git

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *