美文网首页
iOS中方法调用的隐藏参数和结构体压栈

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

作者: 希尔罗斯沃德_董 | 来源:发表于2021-08-13 12:09 被阅读0次

隐藏参数

首先由下面这个常见的面试题入手。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。

相关文章

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

    隐藏参数 首先由下面这个常见的面试题入手。MyObject是自定义的一个类,父类是NSObject。接下来的问题是...

  • OpenGL 栈概念及金字塔构建

    一、压栈与出栈的简介 1.压栈函数(PushMatrix()): 和数据结构中的栈类似,调用这个方法的时候,若传入...

  • 运行时 对我们的Stack都做了什么

    前言 stack(栈)区, 对一个方法的调用的影响 在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调...

  • JVM内存结构

    栈帧结构 一个栈帧对应一个方法的一次调用,存放方法调用和执行信息的数据结构。 存储: 局部变量表 存放方法参数和局...

  • 函数调用约定

    1. c调用约定 _cdecl 调用方将参数从右面到左压栈,被调用函数完成后,调用方负责从栈中清除参数。 2. s...

  • 虚拟机的方法调用和字节码执行

    目录 一、运行时栈帧结构二、方法调用三、方法执行 一、运行时栈帧结构 栈帧是用于支持虚拟机进行 方法调用 和 方法...

  • 调用约定

    调用约定 调用约定参数压栈顺序平衡堆栈EAXAXAH - ALECXCXCH-CLEDXDXDH-DLEBXBXB...

  • iOS动态调用类方法

    iOS动态调用类方法(不带参数) iOS动态调用类方法(带参数) iOS动态调用类方法(有返回值) 优点 弱化连接...

  • C# 值参数和引用参数

    值参数 概念:方法中的值参数传递的类型可以包括“值类型”和“引用类型”。结论:被调用方法的参数在栈上分配内存, 值...

  • JAVA面试问题

    1.Java中堆和栈有什么不同? 每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变...

网友评论

      本文标题:iOS中方法调用的隐藏参数和结构体压栈

      本文链接:https://www.haomeiwen.com/subject/xzakbltx.html