This article is about the actual operation principle of super. If you still have doubts about the distinction between super and self, please refer to the interview question “Recruiting a reliable iOS” by ChenYilong.

What exactly is Super doing?

Official mention of the super keyword?

Open the Apple API documentation and search for objc_msgSendSuper (supplement rumTime for those unfamiliar with this function).

Super official Explanation

It explicitly states that sending messages using the super keyword will be converted by the compiler to calling objc_msgSendSuper and related functions (depending on the return value).

Let’s look again at the definition of this function (which is defined in the documentation) :

id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ;Copy the code

The super is no longer the super we wrote when we called [super init], it’s the struct objc_super structure pointer. The documentation makes it clear that this structure needs to contain instances of receiving messages as well as the parent class from which to start looking for method implementations:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained Class super_class;
    /* super_class is the first class to search */
};Copy the code

Objc_super structure

Now that you know how super is called, let’s try to implement a super ourselves.

Implement the super keyword manually

Let’s define two classes first:

This is the parent class: Father

// Father.h
@interface Father : NSObject

- (void)eat;

@end

// Father.m
@implementation Father

- (void)eat {
    NSLog(@"Father eat");
}

@endCopy the code

This is the subclass: Son

// Son.h
@interface Son : Father

- (void)eat;

@end

// Son.m
@implementation Son

- (void)eat {
    [super eat];
}

@endCopy the code

In this case, our Son class overrides the parent’s eat method to do only one thing: call the parent’s eat method.

Let’s start testing in Main:

int main(int argc, char * argv[]) { Son *son = [Son new]; [son eat]; } // Output: 2017-05-14 22:44:00.208931+0800 TestSuper[7407:3788932] Father eatCopy the code

So far, there is nothing wrong with this. A Son object calls the eat method (internally calling the parent’s eat) and prints the result.

1. Now, let’s implement it ourselvessuperThe effect of:

Rewrite the Son. M:

// Son.m

- (void)eat {
//    [super eat];

    struct objc_super superReceiver = {
        self,
        [self superclass]
    };
    objc_msgSendSuper(&superReceiver, _cmd);    
}Copy the code

Run our main function:

109379+0800 TestSuper[7417:3790621] Father eat 2017-05-14 22:47:00.109379+0800 TestSuper[7417:3790621] Father eatCopy the code

No problem, we implement the super effect according to the official document.

Is super really like this?

Let’s take a skeptical look at this example:

Here we have the Grandson subclass: the Grandson class

// Grandson.h
@interface Grandson : Son

@end

// Grandson.m
@implementation Grandson

@endCopy the code

This class implements nothing and inherits purely from Son.

Then let’s rewrite main:

int main(int argc, char * argv[]) {
    Grandson *grandson = [Grandson new];
    [grandson eat];
}Copy the code

Run, after a while crash, as shown:

Crash prompt

Now look at the method calls in the relevant threads:

Crash method call

This is an infinite loop, so the system forces the code to stop. But why is there a loop here? Let’s break it down:

  1. The eat method was not implemented in The Grandson, so the execution of the eat method in the instance of Grandson in the main function was as follows: Search from bottom to bottom according to the class inheritance relationship, find the eat method in the Son class of Grandson’s parent class and call it.
  2. In the implementation of Son’s EAT method, we build onesuperReceiverThe structure, inside, containsselfAs well as[self superclass]. In this case, self is referring to the instance of Grandson, the variable Grandson, so[self superclass]The return value of this method is the Son class.
  3. According to the analysis in Point 2, and in our documentation at the beginning of this article, Apple points out thatsuperReceiverThe parent class in is the parent class that we started looking for the method implementation, and we can figure out what the parent class is at this pointobjc_msgSendSuper(&superReceiver, _cmd)The method implementation of the function call is in the Son classeatMethod implementation. That is, it forms a recursion.

Since we can’t use the superclass method here, how do we implement super ourselves?

We are the authors of this code, so we can do this:

Son.m - (void)eat {// [super eat]; struct objc_super superReceiver = { self, objc_getClass("Father") }; objc_msgSendSuper(&superReceiver, _cmd); } // 2017-05-14 23:16:49.232375+0800 TestSuper[7440:3798009] Father eatCopy the code

We directly specify the parent class in the superReceiver that we are looking for in the method implementation: Father. Wouldn’t every call to [super XXXX] require the parent class to be specified directly?

“To specify” means to write the class directly in the code, for example: [Father class] or objc_getClass(“Father”), where Father and Father are dead in the code.

Leaving that question aside, let’s examine this code:

  1. The eat method was not implemented in The Grandson, so the execution of the eat method in the instance of Grandson in the main function was as follows: Search from bottom to bottom according to the class inheritance relationship, find the eat method in the Son class of Grandson’s parent class and call it.
  2. In the implementation of Son’s EAT method, we build onesuperReceiverThe structure, inside, containsselfAs well asFatherThis class.
  3. objc_msgSendSuperThe function goes directly to the Father classeatMethod, and execute (output).

This code is now executed in normal logic.

2. [super xxxx]Do you really want to specify the parent directly?

We used the rewrite directive in clang to rewrite son.m:

clang -rewrite-objc Son.mCopy the code

The generated son.cpp file:

static void _I_Son_eat(Son * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("eat"));
}Copy the code

The code at the end of this line is very unreadable, so let’s break it down a little bit (we made a few syntax changes to get it through compilation because of syntax problems, and it actually works the same as in the original CPP) :

static void _I_Son_eat(Son * self, SEL _cmd) {
    __rw_objc_super superReceiver = (__rw_objc_super){
        (__bridge struct objc_object *)(id)self,
        (__bridge struct objc_object *)(id)class_getSuperclass(objc_getClass("Son"))};

    typedef void *Func(__rw_objc_super *, SEL);
    Func *func = (void *)objc_msgSendSuper;

    func(&superReceiver, sel_registerName("eat"));
}Copy the code

First modify son.m to run:

// Son.m - (void)eat { // [super eat]; //_I_Son_eat is the overridden function _I_Son_eat(self, _cmd); } // 2017-05-15 00:08:37.782519+0800 TestSuper[7460:3810248] Father eatCopy the code

Nothing wrong with it.

The overwritten code builds an __rw_objc_super structure, defined as follows:

struct __rw_objc_super { struct objc_object *object; struct objc_object *superClass; __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}};Copy the code

This structure is consistent with struct objc_super. We then call the objc_msgSendSuper function as func with the specified parameters. Note here the second value in __rw_objc_super superReceiver: class_getSuperclass(objc_getClass(“Son”)).

The class that this code specifies directly is this class: the Son class. But the superClass in __rw_objc_super is not this class. It is the parent class found by the Runtime. This is the same result as our own implementation of specifying Father as the super_class value of the objc_super structure.

Therefore, [super XXXX] must specify a class, which can be a parent or parent class, in order to call the parent method correctly! It’s just “specify”, and the compiler will do it for us, and we’ll just say super.

Clang rewrite is unreliable

Why is Clang unreliable

The rewrite function in Clang provided code that was not transformed by the compiler (LLVM), which now generates LLVM Intermediate Representation (LLVM IR) when Xcode turns on bitcode. This code consolidates most high-level languages upward and supports cpus of many different architectures downward, as detailed in the LLVM documentation. So our goal is to find out from IR code exactly what Super is doing!

View IR code

Go to the directory where the son. m file is stored and run:

clang -emit-llvm Son.m -S -o son.llCopy the code

There are a lot of IR codes generated, so let’s select the key ones to check:

%0 = type opaque

// Son eat method
define internal void @"\01-[Son eat]"(%0*, i8*) #0{%3 = alloca %0*, align 8    // Allocate memory for a pointer, 8 bytes aligned (declare a pointer variable)
  %4 = alloca i8*, align 8    // Allocate memory for a char * pointer.
  %5 = alloca %struct._objc_super, align 8    // Allocate memory to _objc_super (declare a struct._objc_super variable)
  store %0* %0%,0* * %3, align 8    // Write the first argument, id self, to the memory allocated by %3
  store i8* %1, i8** %4, align 8    // write _cmd to the memory area allocated by %4
  %6 = load %0* %0* * %3, align 8   // Read data from %3 into the temporary variable %6.
  %7 = bitcast %0* %6 to i8*        // Convert the type of the %6 variable to a char * pointer to self
  %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0    Struct. _objc_super (%5); // take the 0th element in struct._objc_super (%5) and declare it as %8
  store i8* %7, i8** %8, align 8    // Store %7 in the variable %8, that is, store i8* self in the 0th element of the structure
  %9 = load %struct._class_t* %struct._class_t* * @"OBJC_CLASSLIST_SUP_REFS_$_", align 8    Struct._class_t*, @"OBJC_CLASSLIST_SUP_REFS_$_"
  %10 = bitcast %struct._class_t* %9 to i8*   // Force the variable %9 to char *
  %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1   Struct. _objc_super (%5); // take the first element in struct._objc_super (%5) and declare it as %11
  store i8* %10, i8** %11, align 8    // store the %9 variable @"OBJC_CLASSLIST_SUP_REFS_$_" into the first element of the structure
  %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !7    // Put a reference to @selector(eat) in a char * %12 variable

  Struct. _objc_super and @selector(eat), calling objc_msgSendSuper2
  call void bitcast (i8* (%struct._objc_super*, i8*, ...) * @objc_msgSendSuper2 tovoid (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12)
  ret void
}


@"OBJC_CLASS_$_Son" = global %struct._class_t{%struct._class_t* @"OBJC_METACLASS_$_Son"%,struct._class_t* @"OBJC_CLASS_$_Father"%,struct._objc_cache* @_objc_empty_cache, 
                                                i8* (i8*, i8*)** null,
                                                %struct._class_ro_t* @"\01l_OBJC_CLASS_RO_$_Son" 
                                              }, section "__DATA, __objc_data", align 8

Struct._objc_super: @"OBJC_CLASS_$_Son";
@"OBJC_CLASSLIST_SUP_REFS_$_" = private global %struct._class_t* @"OBJC_CLASS_$_Son", section "__DATA, __objc_superrefs, regular, no_dead_strip", align 8Copy the code

IR syntax is not difficult to remember, or relatively easy to understand. Here we can just look at the comparison:

  • %1, %2, @ XXX and so on are all referential variables, which can be understood as variable names
  • I8 is an 8-bit int, that is, a 1-byte char. i8Refers to charPointer to the
  • Alloca refers to allocating memory and can be understood as declaring a variable, such as alloca i8That is a charThe variables of
  • %0 is an opaque type, so %0* refers to a universal pointer
  • Store is written to memory
  • Load: reads data from memory
  • Bitcast for type conversion
  • Getelementptr Inbounds takes the specified memory offset

The code has both assembly and high-level language flavor. The comments are basically complete, and the logic in the code is similar to the /clang rewrite we implemented ourselves above. But notice the @”OBJC_CLASSLIST_SUP_REFS_$_” variable.

“OBJC_CLASSLIST_SUP_REFS_$_” corresponds to the second element in struct objc_super: super_class. This is shown in IR code %11 and the line that follows.

@”OBJC_CLASSLIST_SUP_REFS_$_” is defined as @”OBJC_CLASS_$_Son”. The @”OBJC_CLASS_$_Son” global variable is the Son object, which contains the metaclass: @”OBJC_METACLASS_$_Son”, the parent class: @”OBJC_CLASS_$_Father”, and some other data. However, looking at this, we see that this is different from our own implementation of super, as well as the super rewritten by Clang: Struct objc_super super_class [Son class] struct objc_super super_class [Son class]]

View assembly source code

However, the only function here, @objc_msgsendsuper2, seems to have an extra 2 than the objc_msgSendSuper we saw earlier. Objc-msg-arm64.s objc-msg-arm64.s objc-msg-arm64.s objC-msG-arm64.s objC-msG-arm64.s

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START

ldp    x0, x16, [x0]        // x0 = real receiver, x16 = class
ldr    x16, [x16, #SUPERCLASS]    // x16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2Copy the code

This is a piece of assembly code, and yes, apple implements the functions related to sending messages directly in assembly to make them more efficient.

Here’s a brief analysis of the function:

  1. ldp x0, x16, [x0]: read two words from x0 into x0 and x16, according to the comment, read data should be correspondingselfwith[Son class].
  2. ldr x16, [x16, #SUPERCLASS]: Take the value of x16 + the offset of the SUPERCLASS value as the address, extract the value of the address and save it in X16. Here,SUPERCLASSIs defined as#define SUPERCLASS 8So it’s offset by 8 bits, so it should be zero@"OBJC_CLASS_$_Father"The parent class[Father class]In the x16.
  3. performCacheLookupFunction with an argument of NORMAL.

Let’s look at the definition of CacheLookup:

/******************************************************************** * * CacheLookup NORMAL|GETIMP|LOOKUP * * Locate the implementation for a selector in a class method cache. * * Takes: * x1 = selector * x16 = class to be searched * * Kills: * x9,x10,x11,x12, x17 * * On exit: (found) calls or returns IMP * with x16 = class, x17 = IMP * (not found) jumps to LCacheMiss * ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp    x10, x11, [x16, #CACHE]    // x10 = buckets, x11 = occupied|mask
    and    w12, w1, w11        // x12 = _cmd & mask
    add    x12, x10, x12, LSL #4    // x12 = buckets + ((_cmd & mask)<<4)

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel ! = _cmd)
    b.ne    2f            // scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #- 16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // wrap: x12 = first bucket, w11 = mask
    add    x12, x12, w11, UXTW #4    // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel ! = _cmd)
    b.ne    2f            // scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #- 16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // double wrap
    JumpMiss $0

.endmacroCopy the code

We’re not going to expand on the actual cache hook up here, we’re just going to focus on where we’re looking for the method. The annotation explicitly states that this was a function to “search the method cache of a class to find the method implementation,” to find the selector in X1, the class in X16 (class to be searched, that is, to start the search from that class), and to disclose the number of times that x16, Is exactly the Father class we just saved in _objc_msgSendSuper2, so the method will start looking in this class.

Overall call flow

From manual implementation -> view clang rewrite -> view IR code -> view assembly source these several process analysis down, we are finally the real super call link to make clear:

  1. The compiler specifies onestruct._objc_superThe structure, in the structureselfIs the received object,Direct pointIts class is the value of the structure’s second class type.
  2. call_objc_msgSendSuper2Function, passed abovestruct._objc_superStructure.
  3. in_objc_msgSendSuper2Function directly through the offsetDirect searchThe parent class.
  4. callCacheLookupThe function goes to the parent class to find the specified method.

conclusion

So, from the real IR code, the super keyword actually refers to the Son class directly, and then with the _objc_msgSendSuper2 function directly to find the parent class to find the method, rather than clang rewrite, refers to the class, and then through the Runtime to find the parent class.

In fact, it is not a problem to refer to this class first and then look up the parent through the Runtime. This also avoids some runtime “change the parent” situations. But there should be a reason for LLVM to do this, perhaps for performance reasons?