What does mixing Swift and Objective-C cost, and how do you keep the cost down?
Predict first
A hot loop calls a small Objective-C method a million times from Swift. What is the best first fix?
// A: add more @objc attributes
// B: move the loop to one side so it crosses the boundary once
// C: mark the method dynamic
- A
- B
- C
Mental model. Every border crossing costs a queue and a stamp. One trip with a full suitcase beats a hundred trips with a postcard.
Costs arrive in four places. Runtime: a call that crosses into Objective-C goes through objc_msgSend and retain and autorelease traffic, and bridging String, Array and Dictionary to their Foundation forms can allocate or copy. Binary size: every @objc member gets a thunk and runtime metadata, so a blanket @objcMembers adds weight you may never use. Optimisation: the compiler cannot inline or devirtualise across dynamic and Objective-C boundaries. Build time: mixed targets regenerate headers, and a large bridging header invalidates a lot of Swift on each change.
Keep it down by keeping the seam coarse and narrow: expose a few well-typed entry points rather than many chatty calls, keep hot loops entirely in one language, avoid NSObject as a default base class, mark only what must be @objc, and convert to Swift types once at the boundary instead of repeatedly inside loops. Measure before and after: compare binary size with and without @objcMembers, and look for objc_msgSend, objc_retain and bridging calls in Time Profiler.
Crossing the border once, with a coarse call, removes per-call message sends and retain traffic. More attributes or dynamic would add cost.
Common traps
- Calling an Objective-C getter inside a tight Swift loop. Each call pays message-send and retain costs.
- Making everything
@objcMembersfor convenience. It inflates size and slows dispatch for code that never needed it.
They might follow up with
- How do you check binary size impact? Compare the linkmap or app thinning report with and without the attribute.
- What is the cheapest design fix? A single coarse-grained method on the boundary that does the whole job on one side.
Related questions
- 4.4.1 What do @objc, @objcMembers, @objc(name), dynamic and @nonobjc each do?
- 4.8.1 How do Swift and Objective-C see each other: bridging header, umbrella header, and the -Swift.h file?
- 3.6.4 What hidden retains and autoreleases does calling Objective-C add?
← How do Swift and Objective-C see each other: bridging header… How would you plan the migration of a large Objective-C app to Swift? →