iOS-runtime详解(一)消息传递中的各种结构

文章目录

  • 理解消息传递。
    • objc_msgSend的作用
      • 边界情况
    • Self & Super
    • Object & Class & Meta Clas
    • iSA详解
      • 小插曲,来看看什么是关联对象
        • 实例
      • 回到iSA
        • ISA()
      • 消息传递中如何找寻方法。
  • Method
    • class_data_bits_t bits;
    • lldb打印验证class_ro_t *的结构
    • realizeClassWithoutSwift
        • 小总结
  • 参考文章

理解消息传递。

objc_msgSend的作用

在OC中对象调用方法,叫做传递消息。消息有name名称或selector选择子。
如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的c语言函数,然而对象收到消息之后,该调用哪个方法是完全由运行期决定。
标准的c语言函数调用

void objc_msgSend(id self, SEL cmd,...)

这是一个参数可变的函数,能接收俩个或者俩个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这俩个词经常交替使用。
objc_mesSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转,如果最终还是没有找到,就执行消息转发操作。
objc_msgSend会将匹配结果缓存在快速映射表(first map)里面,每个类都有这样一块缓存,如果稍后执行的消息一致,那调用就快了。
这种快速执行路径(fast path)还是不如静态绑定的函数调用快。

边界情况

objc_msgSend_stret 如果待发送的消息要返回结构体,那么可交给这个函数处理。如果返回的结构体太大了,那么由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
objc_msgSend_fpret 如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的cpu中调用函数时,需要对“浮点数寄存器”做特殊处理。这是处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况
objc_msgSendSuper.如果要给超类发消息,调用此函数。

Self & Super

@implementation Son : Father

- (id)init

{

    self = [super init];

    if (self)

    {

        NSLog(@"%@", NSStringFromClass([self class]));

        NSLog(@"%@", NSStringFromClass([super class]));

    }

    return self;

}

@end

在Xcode上运行结果:

class:Son
2020-07-20 09:11:12.611308+0800 MsgSendStudy[94454:11993849] superClass:Son

self 和super的指向的消息接收者都是当前类的实例,不同之处是,用self发送消息会从当前类的方法列表里找,super会从父类的方法列表里找。上面的例子中,self和super都指向Son*s这个对象。
转为c++代码

static instancetype _I_Son_init(Son * self, SEL _cmd) {
    self = ((Son *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yj_0r3rd_kx0s12576v0wz9lp9h0000gn_T_Son_69c9a6_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yj_0r3rd_kx0s12576v0wz9lp9h0000gn_T_Son_69c9a6_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"))));
    }
    return self;
}

他们调用了msgSend、msgSendSuper俩个方法。
id objc_msgSendSuper(struct objc_super *super, SEL op, …)
这个方法的第一个参数不是self,是objc_super这样一个结构体
struct objc_super {

__unsafe_unretained id receiver;

__unsafe_unretained Class super_class;

};
俩个参数,一个是消息接收者,一个是其父类,提取刚刚的c++代码发现:

objc_msgSend((id)self, sel_registerName("class"));
objc_msgSendSuper((__rw_objc_super){
(id)self, 
(id)class_getSuperclass(objc_getClass("Son"))
}, sel_registerName("class"))

消息接收者是self,父类是son的父类
而class这个方法

- (Class)class {

    return object_getClass(self);

}

[self class]就是通过msgSend向自己发消息,在本类的方法列表里找class方法。
[super class]调用msgSend的时候,先构造了结构体,结构体第一个成员是self就是son的实例,第二个成员是Father
这个结构体

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 */
};
#endif

iOS-runtime详解(一)消息传递中的各种结构_第1张图片
也就是说,是从父类的方法列表里找 class方法。
而son和father都没有class方法,所以调用的是NSObject中的class方法。

- (Class)class {

    return object_getClass(self);

}

消息接收者是self
这里的self都是Son*;所以最后输出结果一致。
super 关键字帮我们做了什么

 BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];

        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];

        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];

        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

        NSLog(@"%d %d %d %d", res1, res2, res3, res4);

源码

+ (BOOL)isMemberOfClass:(Class)cls {
    
    return self->ISA() == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isKindOfClass给出的定义是: Returns a Boolean value that indicates whether the receiver is an instance of given class or an instance of any class that inherits from that class
isMemberOfClass给出的定义是: Returns a Boolean value that indicates whether the receiver is an instance of a given class
打印结果是 1 0 0 0;

Object & Class & Meta Clas

在objc.h中,对象和类结构体是这样的:

#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

注意最上面的注释,OBJC_TYPES_DEFINED这个宏定义在objc_private.h中被定义为1
可以推测,这里的objc_class、objc_object只是暴露在外面的、方便理解的结构。
我们下载了objc-779.1来看看objc_private中的结构体。

struct objc_object {
private:
    isa_t isa;

public:
  //省略方法


};

而class的结构体是继承它的

这个结构体特别大,这里放一部分
iOS-runtime详解(一)消息传递中的各种结构_第2张图片
省略了其他方法。
iOS-runtime详解(一)消息传递中的各种结构_第3张图片

通过这俩个方法我们得着,类结构体继承自对象结构体,所以类也是对象。而此处独占一行的注释意思是,这里是isa指针,也就是从object结构体继承来的isa。
而类结构体里还有superclass的指针。它的类型——Class也就是当前这个结构体类型。
一张闭环图
iOS-runtime详解(一)消息传递中的各种结构_第4张图片
这张图来自唐巧的博客,他是通过objc.h中类和对象的结构体来表示这张图的。
可以看到:对象的isa指向对象所属的类,类的isa指向类所属的元类。
而对象的元类isa指向根类的元类,也就是NSObject的元类。NSObject的元类isa指向它自己。
NSObject提供的方法并不能返回iSA(),
于是我动了手脚
//这里有点傻逼了,可以用另外的方法

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

iOS-runtime详解(一)消息传递中的各种结构_第5张图片
iOS-runtime详解(一)消息传递中的各种结构_第6张图片
如果没有改动,访问class的superclass的话
iOS-runtime详解(一)消息传递中的各种结构_第7张图片
可以看到,确实如闭环图描述的那样,NSObject的父类为空,其他类的元类iSA最终指向了NSObject的元类,但是我们无法验证,NSObject的元类的父类指向NSObject,因为调用superclass的话只会发生循环,我们更改了superclass,它只能调用ISA(),而NSObject元类的iSA是指向自己的,没有办法访问superclass。

由上面的源码可知,objc-private里定义的结构体的内容与暴露在外部的objc.h的内容意思一致。下面我们来看看结构体中的成员,

iSA详解

前面的ARC和MRC博客都简单介绍了iSA指针,下面我们来看看源码

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

uintptr_t 在宏定义里是typedef unsigned long uintptr_t;
ISA_BITFIELDx86 64下是这样的

#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

顺便介绍 RC_ONE和RC_HALF。1ULL的意思是unsigned longlong 类型的1。
isa_t 是一个 union 类型的结构体,也就是说其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。而 isa 总共会占据 64 位的内存空间(决定于其中的结构体)

小插曲,来看看什么是关联对象

有时候,类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。objective-C中有一项强大的特性可以解决此问题,这就是“关联对象”(Associated Object)。
可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候。可以指名“存储策略”(storage policy),用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义。
我们可以把某对象想象成NSDictionary,把关联对象的值理解为字典中的条目,于是,存取关联对象的值就相当于NSDictionary对象上调用[object setObject: value forKey:key]与[object objectForKey:key]方法。然而俩者之间有个重要差别:设置关联对象时用的键(key)是个不透明的指针。如果在俩个键上调用’isEqual:'方法的返回值是YES,那么NSDictionary就认为俩者相等;然而在设置关联对象值时,若想令俩个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。
关于关联对象的实现,我们在下一篇博客讨论,这里主要讲一下它的用法。它就是用来添加属性的。

实例

当用户按下按钮关闭该视图时,需要使用委托协议来处理此动作,但是要想设置好这个委托机制,就得把创建警告视图和处理按钮动作的代码分开。由于代码分俩块,读起来有点乱。比方说,我们在使用UIAlertView时,一般会这么写:

- (void)askUserAQuestion {
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you to do?" delegate:self cancelButtonTitle:@"Cancel"
	otherButtonTitle:@"Continue", nil];
	[alert show];
}
//UIAlertViewDelegate protocol Method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
	if (buttonIndex = 0) {
		[self doCancel];
	} else {
		[self doContinue];
	}
}

如果想在同一个类处理多条警告视图信息,代码将变得复杂,我们必须在delegate方法中检查传入的alertView参数,并写出相应的逻辑。
可以通过关联对象来做,在创建视图的时候吧每个按钮的逻辑都处理好。

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you to do?" delegate:self cancelButtonTitle:@"Cancel"
	otherButtonTitle:@"Continue", nil];
	[alert show];
}
void (^block)(NSInteger) = ^(NSInterger buttonIndex) {
	if (buttonIndex = 0) {
		[self doCancel];
	} else {
		[self doContinue];
	}
};
objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
[alert show];

////UIAlertViewDelegate protocol Method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
	void (^block)(NSinteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
	button(buttonIndex);

}

这里为 alertView增加了一个blcok属性,在需要的时候把它取出来。

回到iSA

union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

	union
    联合体和结构体“联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间,一个结构体变量的总长度大于等于各成员长度之和。而在“联合”中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。应该说明的是,这里所谓的共享不是指把多个成员同时装入一个联合变量内,而是指该联合变量可被赋予任一成员值,但每次只能赋一种值,赋入新值则冲去旧值。如下面介绍的“单位”变量,如定义为一个可装入“班级”或“教研室”的联合后,就允许赋予整型值(班级)或字符型(教研室)。要么赋予整型值,要么赋予字符型,不能把两者同时赋予它。联合类型的定义和联合变量的说明:一个联合类型必须经过定义之后,才能把变量说明为该联合类型。
isa_t(uintptr_t value) : bits(value) { } 中,“:”的作用
位域位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域详解;
这是arm64架构的isa_t,不同架构只是各成员所占位数不同。

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
          uintptr_t nonpointer        : 1;                         是否使用优化的指针                \
      uintptr_t has_assoc         : 1;                                       是否有关联对象  \
      uintptr_t has_cxx_dtor      : 1;                                         是否有析构函数\
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \存储类地址
      uintptr_t magic             : 6;                                         判断初始化完成\
      uintptr_t weakly_referenced : 1; // 对象是否曾经或正在被弱引用,如果没有,可以快速释放内存  
      uintptr_t deallocating      : 1;                      是否正在被释放                   \
      uintptr_t has_sidetable_rc  : 1;                                         是否用散列表记录引用计数\
      uintptr_t extra_rc          : 8               引用计数数量,调用方法查看计数时会显示此值的数目+1.
    };
   };

我们从初始化方法来验证每一位的作用。


inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

双冒号的作用:

2.双冒号(::)用法

(1)表示“域操作符”
例:声明了一个类A,类A里声明了一个成员函数void f(),但没有在类的声明里给出f的定义,那么在类外定义f时, 
就要写成void A::f(),表示这个f()函数是类A的成员函数。

(2)直接用在全局函数前,表示是全局函数 
例:在VC里,你可以在调用API 函数里,在API函数名前加::

(3)表示引用成员函数及变量,作用域成员运算符

所以,这个initisa是objc_object的函数。
assert是iOS断言,如果传入的条件表达式为假则终止。

//简化后的代码
 isa.bits = ISA_MAGIC_VALUE;
    isa.has_cxx_dtor = hasCxxDtor;
    isa.shiftcls = (uintptr_t)cls >> 3;
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

因为这是一个联合体,所有成员共用一段内存,给bits赋值,相当于给结构体赋值.
我们将0x001d800000000001ULL对应到每一位。
如图所示
iOS-runtime详解(一)消息传递中的各种结构_第8张图片
他们的地址从低到高排列
iOS-runtime详解(一)消息传递中的各种结构_第9张图片
所以有值的是:nonpointer和magic。
如果nonpointer为0,代表raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。
如果为1,代表它不是指针,是已经优化的isa,关于类的信息存储在shiftcls中。
magic为0x3b用于调试器判断当前对象是真的对象还是没有初始化的空间。
之后会初始化has_cxx_dtor的值,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。
最后我们就要将当前对象对应的类指针存入 isa 结构体中了。

isa.shiftcls = (uintptr_t)cls >> 3;

为什么64位机器指针打印地址却是12位16进制48位呢?
因为虚拟地址48位已经足够用,其余高16位必须与第47位保持一致
为什么要右移3位呢?
我看了几篇文章

将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清楚减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。

绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。

最好的解释是,右移动3位赋值的时候相对应。
为什么48位的地址右移动3位能相对应呢?不应该是4位吗?
打印一下isa

 struct objc_object *object =(__bridge struct objc_object *)[NSObject new];
    NSUInteger x = object->isa;
    NSString *string = @"";
    while(x > 0) {
        string = [[NSString stringWithFormat:@"%lu", x&1] stringByAppendingString:string];
        x=x>>1;
    }
    NSLog(@"%@ %lu", string, (unsigned long)[string length]);
    
    
    
    NSString *string2 =@"";
    NSUInteger b = (uintptr_t)[NSObject class];
       while(b > 0) {
           string2 = [[NSString stringWithFormat:@"%lu", b&1] stringByAppendingString:string2];
           b=b>>1;
       }
    NSLog(@"%@ %lu", string2,[string2 length]);

对象的地址和类的地址对比:

01011101111111111111111110010010110111101101000100011001 
        011111111111111110010010110111101101000100011000 

将类的地址右移三位,和对象中shiftcls的地址相同。
不手动打印无法发现其中的蹊跷,可以看到上面shiftcls的注释中,最大值是0x7fffffe00000,转为2进制为47位,这里是最迷惑的。因为最高位为0,也就是说44位为shiftcls的地址,而最高位始终是0,这时候,整个地址为45位,需要进行对齐补足3位0来对齐,所以我们偏移3位来获取正确的地址。
所以说OC的指针只有47位。通常不管高位的0,它被省略,所以16进制打印是12位,2进制打印是47位。
如何获取类对象的地址呢?

ISA()

iOS-runtime详解(一)消息传递中的各种结构_第10张图片
可以看到,白色部分是不走的,因为编译后就确定了宏定义的值。
这里通过 isa.bits &ISA_MASK来获取isa的值。
关于isa就到此为止。

消息传递中如何找寻方法。

因为在OC中,对象方法并没有存储在没个对象的实例当中(如果每个对象都存储了实例方法,那么内存将会一直增加),所以所有的对象方法都存储在类中,而我们调用实例方法的时候其实就是根据isa找到自己的类,从类中取到实例方法,如果类中没有实例方法,就调用superclass继续查找;那么类方法又是存储在哪呢?没错,就是存储在刚刚所说的元类中
那么类是如何存储方法的呢?
我们先看看方法是什么样的

Method

struct method_t {
SEL name;
const char *types;
IMP imp;
};
iOS-runtime详解(一)消息传递中的各种结构_第11张图片

iOS-runtime详解(一)消息传递中的各种结构_第12张图片
方法就放在class_data_bits_t bits这里。
isa 是指向元类的指针,不了解元类的可以看 Classes and Metaclasses
super_class 指向当前类的父类
cache 用于缓存指针和 vtable,加速方法的调用
bits 就是存储类的方法、属性、遵循的协议等信息的地方

class_data_bits_t bits;

又是一个熟悉的bits。

我们查看它的结构

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
    //省略方法

在objc_class的注释里写到 // class_rw_t * plus custom rr/alloc flags
翻译一下。class_rw_t *加上自定义rr/alloc标志。
为了获取这个结构体里的bits,我们调用data()方法。
发现返回的是class_rw_t *结构体类型
查看这个方法。

class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    #define FAST_DATA_MASK          0x00007ffffffffff8UL

将 bits 与 FAST_DATA_MASK 进行位运算,只取其中的 [3, 47] 位转换成 class_rw_t * 返回。
所以剩下的3位可以另作它用

#define FAST_IS_SWIFT           (1UL<<0)
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
#define FAST_DATA_MASK          0x00007ffffffffff8UL

在这里插入图片描述

  • isSwift()
    FAST_IS_SWIFT 用于判断 Swift 类
  • hasDefaultRR()
    FAST_HAS_DEFAULT_RR 当前类或者父类含有默认的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
  • requiresRawIsa()
    FAST_REQUIRES_RAW_ISA 当前类的实例需要 raw isa

执行 class_data_bits_t 结构体中的 data() 方法或者调用 objc_class 中的 data() 方法会返回同一个 class_rw_t * 指针,因为 objc_class 中的方法只是对 class_data_bits_t 中对应方法的封装

struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t version;
    uint16_t witness;

    const class_ro_t *ro; //,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif

//省略方法

iOS-runtime详解(一)消息传递中的各种结构_第13张图片


//class_ro_t结构体 
//其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    }
    //省略方法

在编译期间类的结构中的 class_data_bits_t *data 指向的是一个 class_ro_t * 指针
iOS-runtime详解(一)消息传递中的各种结构_第14张图片

通过研究class的创建可以发现这一点。
查看方法:

/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
* realizeClassWithoutSwift
*首次对类cls进行初始化,
*包括分配读写数据。
*不执行任何快速端初始化。
*返回类的真实类结构。
*锁定:runtimeLock必须由调用者写锁
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;
//部分代码

又注释可知,这个方法就是首次初始化类的。

ro = (const class_ro_t *)cls->data();
 // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);

从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
初始化一个 class_rw_t 结构体
设置结构体 ro 的值以及 flag
最后设置正确的 data。
iOS-runtime详解(一)消息传递中的各种结构_第15张图片
但是,在这段代码运行之后 class_rw_t 中的方法,属性以及协议列表均为空。这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。

realizeClassWithoutSwift方法最后调用了methodizeClass
 // Attach categories
    methodizeClass(cls, previously);

    return cls;
}

lldb打印验证class_ro_t *的结构

#import 

@interface TestObject : NSObject
- (void)hello;
@end

#import "TestObject.h"

@implementation TestObject

- (void)hello {
    NSLog(@"hello");
}

@end

主程序代码

#import "TestObject.h"
int main(int argc, const char * argv[]) {

    @autoreleasepool {
        Class cls = [TestObject class];
        NSLog(@"%p", cls);
        return 0;
    }
}

类在内存的地址在编译时就决定

0x100002100

+32 就是class_data_bits_t bits的地址,0x100002120

接下来,在整个 ObjC 运行时初始化之前,也就是 _objc_init 方法中加入一个断点:

然后用lldb调试

(lldb) p (objc_class *)0x100002100
warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.
(objc_class *) $0 = 0x0000000100002100
(lldb) p (class_data_bits_t *)0x100002120
(class_data_bits_t *) $1 = 0x0000000100002120
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100002068
(lldb) p (class_ro_t *)$2  //将class_rw_t强制转换为class_ro_t
(class_ro_t *) $3 = 0x0000000100002068
(lldb) p *$3
(class_ro_t) $4 = {
  flags = 0
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100000f91 "TestObject"
  baseMethodList = 0x00000001000020b0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) 

现在我们获取了类经过编译器处理后的只读属性 class_ro_t:

(class_ro_t) $4 = {
  flags = 0
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100000f91 "TestObject"
  baseMethodList = 0x00000001000020b0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}

可以看到这里面只有 baseMethodList 和 name 是有值的,其它的 ivarLayout、 baseProtocols、 ivars、weakIvarLayout 和 baseProperties 都指向了空指针,因为类中没有实例变量,协议以及属性。所以这里的结构体符合我们的预期。
我们看看方法列表

 p $4.baseMethodList
(method_list_t *) $5 = 0x00000001000020b0
(lldb) p $5->get(0)
(method_t) $6 = {
  name = "hello"
  types = 0x0000000100000fa2 "v16@0:8"
  imp = 0x0000000100000f10 (KCObjcTest`-[TestObject hello])
}
(lldb) p $5->get(1)
Assertion failed: (i < count), function get, file /Users/wangtianliang/Desktop/RuntimeAnalyze/objc4-779.1/runtime/objc-runtime-new.h, line 432.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

可以打印的方法信息与方法结构对应。

realizeClassWithoutSwift

这个方法替代了原来的 realizeClass。它的作用是:

  • 分配可读写数据空间
  • 返回真正的类结构
    下面我们来打断点来测试
    iOS-runtime详解(一)消息传递中的各种结构_第16张图片

iOS-runtime详解(一)消息传递中的各种结构_第17张图片

等运行完初始化代码
//第二个断点
在用lldb打印

(lldb) p (objc_class *)cls //打印类指针
(objc_class *) $1191 = 0x0000000100002100
(lldb) p (class_data_bits_t *)0x0000000100002120 //在类指针上偏移32位是bits的位置
(class_data_bits_t *) $1192 = 0x0000000100002120
(lldb) p *$1192  //访问 class_data_bits_t 指针的内容
(class_data_bits_t) $1193 = (bits = 4370482800)
(lldb) p $1193.data()  //通过data方法返回class_rw_t
(class_rw_t *) $1194 = 0x0000000104804670
(lldb) p *$1194 //访问 class_rw_t 指针的内容,发现它的 ro 已经设置好了
(class_rw_t) $1195 = {
  flags = 2148007936
  version = 0
  witness = 0
  ro = 0x0000000100002068 //这里的地址与runtime之前的调试打印地址相同
  methods = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  properties = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = nil
  demangledName = 0x0000000000000000
}
(lldb) 

继续查看 ro

(lldb) p $1194.ro
(const class_ro_t *) $1196 = 0x0000000100002068
  Fix-it applied, fixed expression was: 
    $1194->ro
(lldb) p *$1196
(const class_ro_t) $1197 = {
  flags = 0
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100000f91 "TestObject"
  baseMethodList = 0x00000001000020b0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}

在上述的代码运行之后,类的只读指针 class_ro_t 以及可读写指针 class_rw_t 都被正确的设置了。但是到这里,其 class_rw_t 部分的方法等成员都指针均为空,这些会在 methodizeClass 中进行设置:
iOS-runtime详解(一)消息传递中的各种结构_第18张图片
可以看到这里方法列表的地址和runtime之前ro里的一样。
iOS-runtime详解(一)消息传递中的各种结构_第19张图片
在这里调用了 method_array_t 的 attachLists 方法,将 baseMethods 中的方法添加到 methods 数组之后。我们访问 methods 才会获取当前类的实例方法。

小总结

  • 编写代码运行后,开始类的方法,成员变量 属性 协议等信息都存放在 const class_ro_t中,运行过程中,会将信息整合,动态创建 class_rw_t,然后会将 class_ro_t中的内容(类的原始信息:方法 属性 成员变量 协议信息) 和 分类的方法 属性 协议 成员变量的信息 存储到 class_rw_t中.并通过数组进行排序,分类方法放在数组的前端,原类信息放在数组的后端.运行初始 objc-class中的 bits是指向 class_ro_t的,bits中的data取值是从class_ro_t中获得,而后创建 class_rw_t,class_rw_t中的 class_ro_t从初始的 class_ro_t中取值,class_rw_t初始化完成后,修改 objc_class中的bits指针指向class_rw_t
    所以ro中存储的是编译时就已经决定的原数据,rw才是运行时动态修改的数据

iOS-runtime详解(一)消息传递中的各种结构_第20张图片
方法调用流程

CHPerson *person = [[CHPerson alloc] init];
[person test]

首先通过isa找到person对应的CHPerson类,先查看其中的cache_t里的缓存方法
如果找不到该test方法,就去查看bits中的rw的methods查找方法【如果找到了,调用,且添加到cache_t中】
如果在CHPerson里找不到,会查看CHPerson的父类,同样是先cache_t,后bits,如果查找到了,是存放在自己的cache里,不是父类的【注意!】
最后一直查看父类会到达NSObject根类,如果依然找不到就消息转发。

参考文章

初识isa
用 isa 承载对象的类信息
刨根问底Objective-C Runtime(唐巧)
深入解析objc中的方法
runtime中基本结构体

你可能感兴趣的:(iOS-runtime详解(一)消息传递中的各种结构)