preface
To understand different blocks of knowledge individually cannot help us to understand and memorize, nor can we connect the knowledge points in series to achieve mastery.
Choose a main line from the actual scene, to understand and learn the knowledge points encountered by the main line, in order to better connect the different blocks of the series, deepen the understanding.
From object creation, explore the nature of the object, the process of creation, encounter ISA, object -> class -> metaclass, cache_t, memory alignment, classification, taggedPoint, method caching, method lookup, message forwarding, Memory management and so on.
By doing so, we can not only master these concepts, but also understand them, and get to the root of why Apple designs the way it does.
This article starts from the object creation, combs the object creation process, explores each knowledge point encountered.
Documents:
Objc source code.
Objc4-756.2 latest source code compiler debugging.
OC object creation exploration
How objects are created, most commonly alloc init, or new.
New project preparation code:
NSObject * obj = [NSObject alloc];
Copy the code
Add a breakpoint and run the project by clicking Step into.
objc
objc_alloc
Actually run the case on objC 756.2, add breakpoints to alloc and objc_alloc respectively, and you’ll see that objc_alloc goes first.
1, objc_alloc and alloc
But looking at the source code, we see that NSObject has an alloc class method. So why doesn’t our external [NSObject alloc] call the alloc class method instead of going into objc_alloc?
This part of the author through part of the source code combined with MachO file to view the speculation is as follows:
Xcode 10
Will go right intoalloc
,Xcode 11
Will go right intoobjc_alloc
Because inXcode11
The compiledalloc
The corresponding symbol will be set toobjc_alloc
.- while
Xcode 10
Not really. We can use itMachOView
Look at the projects compiled in these two environments separatelyMach-O
In the__DATA
Section,__la_symbol_ptr
Section.
The following are the author’s test results.
In addition, some codes can be found in objC source code as follows:
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc; // This is what happens when the symbol is bound.
} else if (msg->sel == SEL_allocWithZone) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == SEL_retain) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == SEL_release) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == SEL_autorelease) {
msg->imp = (IMP)&objc_autorelease;
} else{ msg->imp = &objc_msgSend_fixedup; }}/ *... * /
}
Copy the code
(Read the Hook/fishHook principle and symbol table and the dyLD loading process from scratch for more information about symbol binding.)
◈ alloc class method source is as follows:
+ (id)alloc {
return _objc_rootAlloc(self);
}
Copy the code
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/.true/*allocWithZone*/);
}
Copy the code
The ◈ objc_alloc function is as follows:
id objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/.false/*allocWithZone*/);
}
Copy the code
We can see that both alloc and objc_alloc go into callAlloc, but the last two arguments are passed differently. So let’s move on.
◈ –> Tips:
Xcode 11 calls [NSObject alloc] to objc_alloc, and internally calls [CLS alloc] directly to alloc. I haven’t found the exact information to confirm this. Guess if there’s any fully open source code for symbol binding and fixup. If you know, welcome to exchange.
2、 callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if(slowpath(checkNil && ! cls))return nil;
#if __OBJC2__
if(fastpath(! cls->ISA()->hasCustomAWZ())) {if (fastpath(cls->canAllocFast())) {
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if(slowpath(! obj))return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
id obj = class_createInstance(cls, 0);
if(slowpath(! obj))return callBadAllocHandler(cls);
returnobj; }}#endif
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
Copy the code
First we notice two macro-defined functions: fastPath and slowPath.
// x is probably not 0, and the compiler is expected to optimize it
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x is likely to be 0 and the compiler is expected to optimize
#define slowpath(x) (__builtin_expect(bool(x), 0))
Copy the code
So let’s just mention that in passing.
2.1. Fastpath and SlowPath
Removing fastPath and slowPath does not affect any functionality at all. The reason fastPath and slowpath are included in the if statement is to tell the compiler:
The condition in if is fastpath or slowpath event
This allows the compiler to optimize the code.
So how do you tell the compiler, or how does the compiler target processing and optimization?
For example 🌰 :
if (x)
return 2;
else
return 1;
Copy the code
Reading:
-
1️ : Since the computer does not read one instruction at a time, but multiple instructions, return 2 will be read in when it reads an IF statement. If x is 0, return 1 is re-read, and reread instructions are relatively time-consuming.
-
2️ discount: If x has a very high probability of being 0, the return 2 instruction will inevitably be read every time and has almost no chance to execute, resulting in unnecessary command re-stress.
-
3️ discount: Thus, of the two macros defined by Apple, FastPath (x) still returns x, only telling the compiler that the value of x is generally not 0 so that compilation can be optimized. Similarly, slowpath(x) means that the value of x is likely to be 0 and the compiler is expected to optimize.
The explanation of this example comes from bestsswifter’s in-depth understanding OF GCD, you can have a look.
So in callAlloc, step one
if(slowpath(checkNil && ! cls))return nil;
Copy the code
It’s basically telling the compiler that CLS is most likely to have a value, and the compiler should handle it accordingly.
CLS ->ISA()->hasCustomAWZ().
2.2, hasCustomAWZ
AllocWithZone = AllocWithZone = AllocWithZone; This is determined by the hasCustomAWZ method in the objC_class structure of the class.
bool hasCustomAWZ(a) {
return ! bits.hasDefaultAWZ();
}
Copy the code
HasDefaultAWZ is implemented as follows:
bool hasDefaultAWZ(a) {
return data()->flags & RW_HAS_DEFAULT_AWZ;
}
void setHasDefaultAWZ(a) {
data()->setFlags(RW_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ(a) {
data()->clearFlags(RW_HAS_DEFAULT_AWZ);
}
Copy the code
It’s actually a tag in the RW that indicates whether the user has implemented allocWithZone.
Since classes are lazily loaded, when the first message is sent to the class, the class is not loaded. Therefore, when the class first receives alloc and enters hasCustomAWZ, there is no DefaultAWZ. So hasCustomAWZ is true and therefore goes directly to [CLS alloc];
We can test it as follows:
LBPerson *objc = [[LBPerson alloc] init];
LBPerson *objc1 = [[LBPerson alloc] init];
Copy the code
When objc enters callAlloc, it enters [CLS alloc] below, and when objC1 enters, it directly enters if (fastPath (! CLS ->ISA()->hasCustomAWZ())) {internal.
Tip:
- 1️ retail: Familiar to us
initialize
When the class receives the first messageobjc_msgSend
The process is invoked by the trigger.- 2️ discount: The above result is
Xcode 11
Environment,Xcode 10
Environmental direct entryalloc
That isobjc_msgSend
So it goes straight inif
Set up process.- 3 ️ ⃣ : about
allocWithZone
All we need to know for the moment is that it’s another method that the object opens up, and if you override it, when you alloc, it’s going to go into user definedallocWithZone
Flow. This is also what we deal with when we write singletonsallocWithZone
The reason why.
The handling done to Initialize in lookUpImpOrForward.
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
/ *... * /
if(initialize && ! cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock(); }/ *... * /
}
Copy the code
As for the structure of the class and the specific content of ISA, I will write two articles exclusively about it because there are many contents. First, I will put a picture for the convenience of having a general understanding.
When first entered [CLS alloc]; , let’s look at the source code implementation:
+ (id)alloc {
return _objc_rootAlloc(self);
}
Copy the code
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/.true/*allocWithZone*/);
}
Copy the code
Again in callAlloc, because [CLS alloc]; The message sending mechanism is triggered, DefaultAWZ is true, hasCustomAWZ is false, and so on to the next process.
2.3, canAllocFast
The source code is as follows:
bool canAllocFast(a) { assert(! isFuture());return bits.canAllocFast();
}
#if! __LP64__
/ * * /
#elif 1
#else
#define FAST_ALLOC (1UL<<2)
#if FAST_ALLOC
#else
bool canAllocFast(a) {
return false;
}
#endif
Copy the code
You can clearly see that the return is false. So callAlloc comes in
id obj = class_createInstance(cls, 0);
if(slowpath(! obj))return callBadAllocHandler(cls);
return obj;
Copy the code
The reason for doing this is that under 32-bit systems, there are additional processes that 64-bit systems no longer use, so macro definitions are used to handle compatibility.
2.4, class_createInstance
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
Copy the code
static __attribute__((always_inline))
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true.size_t *outAllocatedSize = nil)
{
if(! cls)return nil;
assert(cls->isRealized());
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if(! zone && fast) { obj = (id)calloc(1, size);
if(! obj)return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if(! obj)return nil;
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
Copy the code
By looking at the function name and return value, we know that we have reached the point where we are creating objects and allocating memory.
The first is hasCxxCtor and hasCxxDtor.
Since the reference:
- -fobjc-call-cxx-cdtors
- IOS: “. Cxx_destruct “-a hidden selector in my class
2.4.1 hasCxxCtor and hasCxxDtor
Let’s take a look at the object release process.
- 1️ one: on the subject
dealloc
, will judge whether can be released, based on five main criteria:NONPointer_ISA // whether isa isa non-pointer type weakly_reference // Whether there is a reference has_assoc // Whether there is an associated object has_cxx_dtor // is there any c++ related content has_sidetable_rc // Whether to use sidetable Copy the code
- 2️ one: if not before5In any of these cases, the release operation can be performed,CFunction of the
free()
Otherwise, the system entersobject_dispose
- 3 ️ ⃣ :
object_dispose
- Direct call
objc_destructInstance()
. - After the callCFunction of the
free()
.
- Direct call
- 4 ️ ⃣ :
objc_destructInstance
- To determine
hasCxxDtor
, if you havec++
Related content to callobject_cxxDestruct()
Destroy c++ related content. - To determine
hasAssociatedObjects
, if there is an associated objectobject_remove_associations()
To destroy the associated object. - And then call
clearDeallocating()
. - The execution is complete.
- To determine
- 5 ️ ⃣ :
clearDeallocating()
Calling process
- To perform first
sideTable_clearDeallocating()
. - To perform
waek_clear_no_lock
, sets the weak reference pointer to the object tonil
. - Next to execute
table.refcnts.eraser()
To erase the object’s reference count from the reference count table. - To this end,
dealloc
The execution process is complete.
- To perform first
Both of these were originally used in objc++ to handle construction and destruct of c++ member variables, and later.cxx_destruct was used to handle memory freed under ARC.
-
When using MRC, the developer must manually write dealloc to ensure that all references to all objects it reserves are released. This is manual and error prone.
-
When ARC was introduced, the code to perform tasks equivalent to these manual distributions had to be implemented in every object with all but simple attributes. Relying on developers to implement dealloc routines manually will not solve this problem.
-
Thus a pre-existing mechanism of Objective-C ++ is used, namely a selector called a hidden selector, which (.cxx_destruct) is automatically called by the Objective C runtime before objects are released, and they are automatically generated by the compiler.
So hasCxxCtor and hasCxxDtor, just to flag the presence of these two selectors.
As some of you may have noticed, there is.cxx_destruct when we get the list of class methods.
Testing:
void testObjc_copyMethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
// Get the method name
NSString *key = NSStringFromSelector(method_getName(method));
NSLog(@"Method, name: %@", key);
}
free(methods);
}
Copy the code
Print the following:
For this reason,.cxx_destruct is also often called a hidden selector.
Going back to class_createInstance, the next step is canAllocNonpointer, which is covered in more detail in ISA. Size_t size = CLS ->instanceSize(extraBytes);
2.4.2, instanceSize
This is where we begin to calculate the amount of memory required, which involves the oft-mentioned memory alignment.
The principle of OC objects occupying memory is also described in detail in this article.
First look at the source code:
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires a minimum of 16 bytes for all objects.
if (size < 16) size = 16;
return size;
}
// Class's ivar size rounded to a pointer-size boundary
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
Copy the code
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
Copy the code
InstanceSize passes the parameter extraBytes as 0, and the attributes are aligned 8 bytes in 64 bits and 4 bytes in 32 bits.
Use (x + WORD_MASK) & ~WORD_MASK; It’s the same effect as moving the bits three places to the left and three places to the right. Data ()-> RO ->instanceSize (unalignedInstanceSize)
Minimum 16 bytes (if (size < 16) size = 16).
Then, since the zone passed in is NULL and supports Nonpointer ISA. So we go to the if satisfy statement.
id obj;
if(! zone && fast) { obj = (id)calloc(1, size);if(! obj)return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
Copy the code
2.4.3, calloc
Click to find calloc source code in malloc.
void * calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
Copy the code
Tip: if you can’t follow the source code, you can follow the following methods
And finally here:
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
/ * * * / has been eliminated
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
Copy the code
Segregated_size_to_fit is as follows:
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM;// multiply by power of two quanta size
*pKey = k - 1;// Zero-based!
return slot_bytes;
}
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) / / 16
Copy the code
Slot_bytes corresponds to (size + (16-1)) >> 4 << 4, which is 16-byte alignment, so calloc() allocates object memory according to 16-byte alignment criteria.
So calloc opens up memory and returns a pointer to that memory address. Back to libobjc, _class_createInstanceFromZone next.
obj->initInstanceIsa(cls, hasCxxDtor);
Copy the code
2.4.4, initInstanceIsa
inline void
objc_object::initInstanceIsa(Class cls, boolhasCxxDtor) { assert(! cls->instancesRequireRawIsa()); assert(hasCxxDtor == cls->hasCxxDtor()); initIsa(cls,true, hasCxxDtor);
}
Copy the code
inline void
objc_object::initIsa(Class cls, bool nonpointer, boolhasCxxDtor) { assert(! isTaggedPointer());if(! nonpointer) { isa.cls = cls; }else{ assert(! DisableNonpointerIsa); assert(! cls->instancesRequireRawIsa());isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
/*arm64 does not go here */
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endifisa = newisa; }}Copy the code
Shiftcls = (uintptr_t) CLS >> 3; More on this in a follow-up ISA article.
At this point, object creation has been explored. The release process has been covered a little bit.
3, the init
Look at the init
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj)
{
return obj;
}
Copy the code
As you can see, init returns the method caller by default. This is designed for engineering purposes, so that you can do some initialization or assignment when you initialize an object.
4, new
+ (id)new {
return [callAlloc(self.false/*checkNil*/) init];
}
Copy the code
New is equivalent to alloc + init. But using new doesn’t call the various init factory methods we’ve overridden.
There is a rumor that it is for Java and other language developers to join the habit problem, listen to it, not true.
share
Finally, Sunnyxx gave 4 questions in an offline sharing session. You can check it out and talk about it, say your answer, and share an analysis article if necessary.