NSDictionary 內部結構、實現原理
The Class
Plenty of Foundation classes are class clusters and NSDictionary
is no exception. For quite a long time NSDictionary
used CFDictionary
as its default implementation, however, starting with iOS 6.0 things have changed:
1 2 |
|
Similarly to __NSArrayM
__NSDictionaryI
rests within the CoreFoundation framework, in spite of being publicly presented as a part of Foundation. Running the library through class-dump generates the following ivar layout:1 2 3 4 5 |
|
It’s surprisingly short. There doesn’t seem to be any pointer to either keys or objects storage. As we will soon see, __NSDictionary
literally keeps its storage to itself.
The Storage
Instance Creation
To understand where __NSDictionaryI
keeps its contents, let’s take a quick tour through the instance creation process. There is just one class method that’s responsible for spawning new instances of __NSDictionaryI
|
It takes five arguments, of which only the first one is named. Seriously, if you were to use it in a @selector
statement it would have a form of @selector(__new:::::)
. The first three arguments are easily inferred by setting a breakpoint on this method and peeking into the contents of x2
, x3
and x4
registers which contain the array of keys, array of objects and number of keys (objects) respectively. Notice, that keys and objects arrays are swapped in comparison to the public facing API which takes a form of:
|
It doesn’t matter whether an argument is defined as const id *
or const id []
since arrays decay into pointers when passed as function arguments.
With three arguments covered we’re left with the two unidentified boolean parameters. I’ve done some assembly digging with the following results: the fourth argument governs whether the keys should be copied, and the last one decides whether the arguments should not be retained. We can now rewrite the method with named parameters:
|
Unfortunately, we don’t have explicit access to this private method, so by using the regular means of allocation the last two arguments are always set to YES
and NO
respectively. It is nonetheless interesting that __NSDictionaryI
is capable of a more sophisticated keys and objects control.
Indexed ivars
Skimming through the disassembly of + __new:::::
reveals that both malloc
and calloc
are nowhere to be found. Instead, the method calls into __CFAllocateObject2
passing the __NSDictionaryI
class as first argument and requested storage size as a second. Stepping down into the sea of ARM64 shows that the first thing __CFAllocateObject2
does is call into class_createInstance
with the exact same arguments.
Fortunately, at this point we have access to the source code of Objective-C runtime which makes further investigation much easier.
The class_createInstance(Class cls, size_t extraBytes)
function merely calls into _class_createInstanceFromZone
passing nil
as a zone, but this is the final step of object allocation. While the function itself has many additional checks for different various circumstances, its gist can be covered with just three lines:
|
The extraBytes
argument couldn’t have been more descriptive. It’s literally the number of extra bytes that inflate the default instance size. As an added bonus, notice that it’s the calloc
call that ensures all the ivars are zeroed out when the object gets allocated.
The indexed ivars section is nothing more than an additional space that sits at the end of regular ivars:
Allocating objectsAllocating space on its own doesn’t sound very thrilling so the runtime publishes an accessor:
|
There is no magic whatsoever in this function, it just returns a pointer to the beginning of indexed ivars section:
Indexed ivars sectionThere are few cool things about indexed ivars. First of all, each instance can have different amo