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:<ViewController: 0x105e06300>
2021-08-12 22:29:57.309032+0800 Superclass&isaDemo[8273:557040] name:(null)

这里有两个问题:

1、objc为什么能调用printName方法?
2、为什么objc调用printName打印的name是<ViewController: 0x105e06300>,而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:<ViewController: 0x113d07af0>
第2个地址:0x16d10dac0, value:viewDidLoad
第3个地址:0x16d10dab8, value:ViewController
第4个地址:0x16d10dab0, value:<ViewController: 0x113d07af0>
第5个地址:0x16d10daa8, value:MyObject
第6个地址:0x16d10daa0, value:<MyObject: 0x16d10daa8>

self:<ViewController: 0x113d07af0>
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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容