1. Class structure changes

Before iOS 14, a Class on disk looked something like this:

This class object contains the most commonly used information: a cache pointing to metaclasses, superclasses, and methods. It also has a pointer to more additional information class_ro_t, where ro means read only. This information is read-only and contains information about class names, methods, protocols, instance variables, and properties. Both the Swift and Objective-C classes use this structure.

When classes are first loaded from disk into memory, they start out looking like this. But once a class is used, something changes.

To understand what happens next, we first need to understand what Clean Memory and Dirty Memory are.

  • Clean Memory: memory that does not change after being loaded. For example,class_ro_tisClean MemoryBecause it is read-only.
  • Dirty Memory: Memory that changes while a process is running. Once used, a class structure is Dirty Memory because new data is written at runtime, such as its method cache part.

Dirty Memory is more expensive than Clean Memory because it needs to be retained for the entire duration of a process. Clean Memory frees up space for other things because the system can always easily reload it from disk when we need it.

MacOS can handle Memory swapping, but iOS doesn’t support this technology, so Dirty Memory can be more expensive. Dirty Memory is why the class structure is divided into these two parts. Of course, it would be better if we could have more Clean Memory. By separating out data that doesn’t change, we can keep most of the class data Clean Memory.

Once the class is used, the runtime allocates extra space to store this data, class_rw_t, where Rw stands for read write. In this structure, we only store data generated at run time.

  • The First Subclass and Next Sibling Class Pointers allow the runtime to iterate through all the classes currently in use.
  • Methods, Properties, Protocols, which can also be modified at run time. In practice, only about 10% of the class’s methods change, so this memory can be optimized to free up some space.
  • Demangled Name will only be used by the Swift class and will not even be used unless you need to get their Objective-C names.

So we can split the last two parts that are not commonly used:

So I split class_rw_t into two parts. We only allocate memory for this part of the class_rw_ext_t structure if we really need it. About 90% of the classes don’t need this extra data, and the system saves about 14MB of memory.

Using the original structure requires about 30MB of memory, and splitting it saves about 14MB.

Tests on macOS Big Sur’s mail App found that more than 9,000 classes used the class_rw_T structure, while only about 10%, or more than 900 classes, used the class_rw_ext_t structure.

And we can do a simple calculation,class_rw_tHalf the size of the structure, then useThat’s the memory we saved. Mail alone saves about 15% of memory, and with this optimization, the entire system will be significantly reducedDirty Memory

If the original code accesses the class_rw_T structure directly, a crash may occur due to a change in the structure’s memory layout. Apple recommends using the runtime API so they take care of the low-level details.

2. The list of related methods changes

Each class has a list of methods. When you write a method, that method is added to the list of methods. The runtime uses these lists to parse messages sent to objects.

Each method contains three pieces of information.

  • Name, or selector, for exampleinit.
  • Encoding of method parameter types, for example@ @ 0:8 16.
  • Method IMP, objective-C methods will eventually compile to a C function.

All of this information is Pointers, which can take up 24 bytes on a 64-bit system.

Our list of methods exists in the image, and the image can be loaded anywhere in memory, depending on the dynamic linker choice. That is, the linker needs to resolve the Pointers in the image to fix where they point to real memory. This will incur additional expenditure.

And since the methods in the mirror are fixed, they don’t run to other mirrors. We don’t need a 64-bit addressed pointer, just 32 bits.

This has several benefits:

  • This offset is fixed relative to the images, regardless of where they are loaded, and should not be fixed when they are loaded from disk.
  • Since there is no repair to be done, this data can be stored in Clean Memory, which is also safer.
  • On 64-bit systems, the pointer size drops from 24 bytes in 64-bit to 12 bytes in 32-bit. Based on actual measurements, the method list takes up about 80MB of memory, but cutting it in half can save 40MB.

We want to keep this part of the data read-only, but what if we use Method Swizzling?

Apple maps the implementation of the exchange in a global table. Since swapping is not a very common operation, this global table is not particularly large.

In addition, in previous implementations, swapping methods would cause the entire paging Page to become Dirty Memory. That is, a single swap can cause thousands of bytes of Dirty Memory, which is not cost-effective.

If we had handled these low-level details directly in our code, but not properly, we might have had a 64-bit pointer to read two 32-bit pointer values. It makes no sense and will cause a crash. Again, Apple recommends using runtime apis so that they take care of the low-level details.

3. Mark pointer structure changes

First, what is a Tagged Pointer?

In this pointer, only the middle highlight is used to represent a real object pointer.

The low value is always 0 because of byte alignment; Since we don’t really address all 64, some of the high level will always be 0.

  • Intel processor

    A low value of 0 indicates a real pointer and a low value of 1 indicates a marker pointer.

    The first three bits are tag numbers, indicating their type. For example, 3 indicates NSNumber and 6 indicates NSDate.

    A tag number of 7 represents an extended tag that uses an additional 8 bits to represent the type, but the meaningful data length is shorter, such as UIColor or NSIndexSet.

    In general, only apples can add the type of marker pointer. But if you’re a Swift developer, you can create your own marker pointer. If you’ve ever used an enumeration that has a value associated with a class instance object, that’s like a marker pointer.

  • ARM64

    • IOS14 The following operating systems are available

      In ARM64, the whole thing is reversed, with the first 1 representing the tag pointer and the last 3 representing the tag number.

      This high-low flip is mainly due to a minor optimization of objc_msgSend. Apple needs to deal with Pointers to objc_msgSend as quickly as possible, which are usually plain Pointers, with marker Pointers and nil less common. Using a comparison to directly determine whether the tag is true or nil makes it easier to get into common logic.

      #define likely(x) __builtin_expect(!! (x), 1)
      #define unlikely(x) __builtin_expect(!! (x), 0)
      Copy the code

      Similarly, a tag number of 7 represents an extended tag that uses an additional 8 bits to represent the type.

    • iOS14

      In iOS 14, the tag number has been moved to low. For existing tools such as dynamic linkers, ARM’s feature, Top Biyte Ignore, is ignored for the high 8 bits of Pointers. Apple put the extension in the section where Top Biyte Ignore works. For byte-aligned Pointers, the lower three bits are always 0, just three bits below the tag number. And finally, one of the interesting effects of this is that you can put a normal pointer in a marked pointer payload. This allows a marker pointer to point to a constant, such as a string or other data structure that might hog Dirty Memory.

      If there is code in the project that involves this part, it may crash in the future. Again, Apple recommends using runtime apis so that they take care of the low-level details.

4. To summarize

After iOS14, apple brought us three runtime optimizations:

  • Smaller class data structures.
  • A smaller list of methods.
  • Marks pointer changes.

Apple recommends using the runtime API so they take care of the low-level details.

Reference 5.

  • WWDC2020 10163

If you found this article helpful, give me a thumbs up