iOS中方法调用的隐藏参数和结构体压栈

隐藏参数

首先由下面这个常见的面试题入手。MyObject是自定义的一个类,父类是NSObject。接下来的问题是:下面的代码中打印的结果是什么?为什么?

@implementation MyObject

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@,%@", [self class], [super class]);
    }
    return self;
}
@end

我们都知道打印的结果是一样的,都是MyObject。但是为什么呢?接下来我们通过xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc MyObject.m -o MyObject.cpp编译成c++查看底层原理:

static instancetype _I_MyObject_init(MyObject * self, SEL _cmd) {
    self = ((MyObject *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyObject"))}, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_91163dcd57j_zw_xyry904bc0000gn_T_main_da396f_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")), ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyObject"))}, sel_registerName("class")));
    }
    return self;
}

这里我们看到[self class]和[super class]在底层分别变成了:

objc_msgSend((id)self, sel_registerName("class"));
objc_msgSendSuper((__rw_objc_super, sel_registerName("class"));

__rw_objc_super的数据结构是objc_super:

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

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

这里我们就可以发现[super class]中的super并不是消息的接收者,它的实际接收者是self,在调用过程中会生成一个结构体objc_super,objc_super中的receiver就是self。所以实际上是跟[self class]一样的像self发送消息。super只是调用了objc_msgSendSuper,绕过本类方法列表,直接去父类寻找方法实现。其实这里想说是,不管[self class]还是[super class],我们代码中并没有加入任何参数,但是转化成objc_msgSend和objc_msgSendSuper确多了两个参数,这其实就是所谓的隐藏参数

隐藏参数和指针平移

OC中,当我们调用方法时,在底层会转化成消息发送的形式,调用objc_msgSend向这个调用者发送消息。这里的消息就是我们调用的方法,而消息的接收者就是调用的对象。它们都转化成了objc_msgSend的参数(objc_msgSendSuper也是同理):

objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

接下来看下如下demo,我们创建一个自定义类,并定义一个属性name和一个实例方法printName,demo如下:

@interface MyObject : NSObject

@property (nonatomic, copy) NSString *name;

- (void)printName;

@end

@implementation MyObject

- (instancetype)init
{
    self = [super init];
    if (self) {
    }
    return self;
}

- (void)printName
{
    NSLog(@"%s:%@", __func__, self.name);
}

@end

然后我们进行如下操作:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Class cls = [MyObject class];
    void *objc = &cls;
    [(__bridge id)objc printName];
    MyObject *objc1 = [[MyObject alloc] init];
    [objc1 printName];
}

运行,最终打印结果为:

2021-08-12 22:29:57.308903+0800 Superclass&isaDemo[8273:557040] name:
2021-08-12 22:29:57.309032+0800 Superclass&isaDemo[8273:557040] name:(null)

这里有两个问题:

1、objc为什么能调用printName方法?
2、为什么objc调用printName打印的name是,而objc1打印的是null?

解答1:这里objc之所以能调用printName方法,是因为objc是一个指针,对象本身也是通过指针访问的,而且对象底层结构的首地址是isa指针,指向的是它的类,而这里objc指向的地址正好MyObject这个类结构的地址,objc会被认为是MyObject一个对象,当我们通过 [(__bridge id)objc printName]这种方式调用时,会进入消息发送流程,通过objc的isa指针(这里正好是objc指针指向的地址)找到它的类MyObject,并从这个类的方法列表里面找到printName这个方法调用。
2、由1可知,objc是一个指向类结构的指针,因此在访问name属性时,它是通过指针平移的方式读取,而实际上它不是一个真正的对象,而因为隐藏参数的原因和函数压栈的原因,它平移之后刚好访问到函数隐藏参数self。而objc1是经过类结构创建的真实对象,它的name属性本来就没有赋值,所以为空。关于函数压栈详情请看下文:

什么是函数压栈?

为了了解函数压栈,我们进行如下demo进行试验。msg_send模拟的是objc_msgSend。它的两个参数objc和sel在调用msg_send时会被按循序压栈:

void msg_send(id objc, SEL sel)
{
    NSLog(@"msg_send中objc地址:%p", &objc);
    NSLog(@"msg_send中objc地址:%p", &sel);
}

打印结果:

2021-08-12 23:04:59.176589+0800 Superclass&isaDemo[8429:566293] msg_send中objc地址:0x16d979a28
2021-08-12 23:04:59.176663+0800 Superclass&isaDemo[8429:566293] msg_send中objc地址:0x16d979a20

可以看出,它们地址是连续的,地址由高到低。而且相差8位。压栈不只是函数参数,函数里的临时变量的地址都会被进行压栈。为了进一步了解函数压栈,继续往下看。

结构体压栈

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Class cls = [MyObject class];
    void *objc = &cls;
    [(__bridge id)objc printName];
    MyObject *objc1 = [[MyObject alloc] init];  

    void *start = (void *)&self;
    void *end = (void *)&objc1;
    long count = (start - end)/8;
    for (int i = 0; i < count; i++) {
        void *addr = start - 8*i;
         if (i == 1) {
            NSLog(@"第%d个地址:%p, value:%s", i+1, addr, *(void **)addr);
        }else{
            NSLog(@"第%d个地址:%p, value:%@", i+1, addr, *(void **)addr);
        }
    }

    NSLog(@"self:%@", self);
    NSLog(@"cls的地址:%p", &cls);
    NSLog(@"objc的地址:%p", &objc);
}

打印结果:

第1个地址:0x16d10dac8, value:
第2个地址:0x16d10dac0, value:viewDidLoad
第3个地址:0x16d10dab8, value:ViewController
第4个地址:0x16d10dab0, value:
第5个地址:0x16d10daa8, value:MyObject
第6个地址:0x16d10daa0, value:

self:
cls的地址:0x16d10daa8
objc的地址:0x16d10daa0

从上面的打印结果中可以看出第1个地址和第4个地址都是当前ViewCntroller即self,第2个是方法viewDidLoad,第3个是当前self的类ViewController,第5个是cls,第6个是objc的地址。所以这个方法里面压栈的顺序是:

self->(SEL)viewDidLoad->ViewController->self->cls->objc,而且地址是由高到低的。

这里就可以回答2的问题了。因为objc被认为是MyObject类的对象,所以会按照MyObject的结构去访问属性name。访问的方式是通过指针平移,而MyObject的第一个属性是isa,这也是objc能调用MyObject的方法的原因。但是当objc继续访问name时,它要平移8个字节(指针大小为8个字节),而由打印结果可知objc的地址0x16d10daa0,所以objc平移8个字节之后刚好越过cls,直接到达上面的第四个地址0x16d10dab0的区域,这个地址刚好指向的是self的内存,所以在这里objc本来是访问name,实际确访问了self。

如果前面的代码改成在cls前面定义别的临时变量,name访问的数据将会改变:

- (void)viewDidLoad {
    [super viewDidLoad];
   // int a = 123;
    Class cls2 = [MyObject class];
    Class cls = [MyObject class];
    void *objc = &cls;
    [(__bridge id)objc printName];
    MyObject *objc1 = [[MyObject alloc] init];
    [objc1 printName];
}

如果demo中cls前面加个cls2,那么这个这回objc访问的name指向的应该是cls2,打印的是MyObject, 而如果前面换成加了一个整形变量int a,那么这回访问的name将是一个异常的,因为name是8字节的,a是4字节的,结果就是访问的地址出现偏差,程序crash。

你可能感兴趣的:(iOS中方法调用的隐藏参数和结构体压栈)