Advancements in the Objective-C runtime

Class Data Structures

  • Classes used to be represented (in-memory) as:
  • The ro struct is read-only, and is a lot less expensive because of it.
  • The other two structures are r/w, and so can be a lot more expensive.
  • The new representation is:
  • The rw_ext struct is only required by about 10% of apps, and so is allocated only when requested.
  • System-wide memory usage improvement: 30MB.
  • The class_* methods/APIs continue to work, but code accessing these structures directly will now break.

Relative Method Lists

  • Methods contain these pieces of metadata:
    • (pointer to) name
    • (pointer to) type information
    • pointer to the implementation
  • Pointers are 64-bit; large address space to accommodate the heap, stack, and dynamically linked binaries
  • These pointers are clean memory, but not free.
  • Pointers need to be fixed/resolved at load time because of dynamic linking.
  • Importantly, a method call from one binary can’t contain pointers to another binary, so there’s really no need for these pointers to cover the entire 64-bit address space.
  • This is now improved using relative method lists: pointers that cover a 32-bit relative address space.
  • Other advantages:
    • No resolution required after dynamic linking.
    • 50% space reduction; 40MB saving on a typical iPhone.
    • “more space you can use to delight your users” 🙄
  • Swizzling
    • Swizzled methods can be implemented anywhere, not just the current binary
    • A global table is maintained, mapping these 32-bit offsets to their full 64-bit (potentially swizzled) address
    • A single swizzle creates a new table entry, which is much cheaper than dirtying an entire page.
  • A potential landmine here is when the deployment target is specified incorrectly; an older runtime will attempt to interpret these relative offsets as 64-bit pointers, almost certainly causing a crash.

Tagged Pointers

  • Object pointer layout:
    • Low bits are always zero because of alignment requirements: objects must be located at a multiple of the pointer size.
    • High bits (1-2 bytes) are always zero.
  • A pointer with the lowest bit set to 1 is not a regular pointer but a tagged pointer (this is Intel; ARM is flipped because of endianness).
  • As an example, this can be used to store a number directly in the pointer. Vaguely similar to an index-only scan.
  • Obfuscation is provided by combining these tagged pointers with a random value that’s set up at (app?) startup.
  • The “tag” field specifies the type of tag this is, such as an NSNumber. Tags can be extended up to 256 unique types (at the cost of a smaller payload).
  • Only Apple can add tagged types, but Swift enums use tagged pointers behind the scenes.
  • iOS 14 flips things around on ARM a bit more to allow a full object pointer to be stored inside a tagged pointer:
    • This allows a tagged pointer to point to static data in the binary.
    • Direct bit-twiddling against a tagged pointer structure is going to break on iOS 14 because of this.
Edit