Analysis of underlying principle of NSObject

Posted by dotMoe on Mon, 21 Feb 2022 12:14:35 +0100


NSObject is the root class of most class hierarchies in Objective-C. usually, when using NSObject objects, we will use [[NSObject alloc] init] or [NSObject new] to create object instances. Through this article, we will study the object creation process of NSObject together.

initialization

Call alloc mode

When we call the [NSObject alloc] method, we call_ objc_rootAlloc(self) this method passes NSObject itself as a parameter to apply for a memory.

callAlloc accepts three parameters:

  1. Class;
  2. Check whether it is empty;
  3. Whether to call allocWithZone;

#define ALWAYS_INLINE inline attribute((always_inline))
Use inline function

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

When calling the alloc method, we can clearly see that Objc4 sends information to the allocWithZone method using the Runtime mechanism through the underlying call. At this time, allocWithZone receives the message sent by the Runtime and is called [cls allocWithZone:nil].

+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

In calling the method, we can see_ class_createInstanceFromZone() discards the zone parameter. Even if developers customize the allocWithZone method, they cannot specify the zone they want to use. The object zone is actually allocated by the libmolloc library.
Initialization process:

  1. Size = CLS - > instancesize (extrabytes) the memory size required to initialize the object;
  2. Generate a memory size through obj = (id)calloc(1, size). malloc will not set the memory to zero, but calloc will set the allocated memory to zero;
  3. Call obj - > initinstanceisa (cls, hascxxdtor), initialize isa and associate with cls class;
  4. Return object;

Call init mode

Calling the [[NSObject alloc] init] method will return the obj object generated by allocWithZone.

- (id)init {
    return _objc_rootInit(self);
}

Call new mode

The [NSObject new] Call actually calls [[NSObject alloc] init], and the new method can be called directly in the default init mode.

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

Destroy

Call dealloc

- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

_ objc_isTaggedPointer determines whether to mark the current pointer as a 64bit small object pointer.

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; // The flag bit indicates whether the pointer is a tagged pointer
}

Tagged Pointer is a technology proposed by apple to store small objects on 64bit devices. It has the following characteristics:

  1. The value of the Tagged Pointer is no longer an address, but a real value. So, in fact, it is no longer an object, it is just an ordinary variable in the skin of an object.
  2. Its memory is not stored in the heap, nor does it need malloc and free. It does not follow the logic of reference counting, and is released by the system.
  3. It has three times the efficiency in memory reading, and the creation time is 106 times faster than before.
  4. You can set the environment variable objc_ DISABLE_ TAGGED_ At points, developers decide whether to use this technology.
  1. Tagged Pointer judges that when the return value is true, it indicates that the current pointer refers to the object rather than the address. It should be released by the system and returned directly to return.
  2. Judge whether the current isa has been optimized. When nonpointer is 1, it means that the current isa has been optimized;
  3. Judge whether the current ISA is weakly referenced or has been weakly referenced,! isa.weakly_referenced will release faster;
  4. Judge whether the current isa has an associated object,! isa.has_assoc will release faster;
  5. Judge whether the current isa has a C + + destructor,! isa.has_cxx_dtor will be released faster;
  6. Determine whether the current isa has an extended reference count. When the reference count of an object is relatively small, its reference count is recorded in Isa. When the reference count is greater than a certain value, sideTable will be used to help store the reference count,! isa.has_sidetable_rc will release faster;
    If the above is not satisfied, the following methods will be called to release.
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

Judge that the obj current object holds the C + + destructor and associated object, and release cxx and assoc.
At present, judge whether obj has been optimized by Isa. When slowpath(!isa.nonpointer) is true, it indicates that isa has been optimized through SlideTable. Remove from SlideTable table.

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

After completing the above operations, free(obj) to release the object.

Topics: iOS source code analysis objective-c cocoa