iOS底层-类的三顾茅庐(一)

前言

了解完对象的底层,知道isa指向的是类对象。那么类(Class)的本质究竟是什么?本文顺序isa的指向,探索类的继承链,和类对象的结构,并且尝试获取方法和变量的存放位置。

类的继承链

实例对象存储的isa指针,占8字节,并指向所属的类。这里通过3中不同的方式打印一下isa内容:

image-20220426105206333

可以看到都是同个对象,这说明类对象有且仅有一个。它是Class类型的。

isa的指向

在源码中搜索一下:实际上类对象就是objc_class这么一个结构体。

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

那么为什么说类也是个对象?你看他有isa指针,还继承了objc_object,这结构体同时也是对象的本质。

image-20220426105520234

这就说明和对象一样了,不同的是,类对象的isa指针指向哪里呢?元类对象。接下来是证明。

首先获得类对象:

image-20220426110710263

接着用x/4gx打印类对象的内存地址,以及它的isa地址并打印:发现还是原来那个类???

image-20220426111213190

这打破了前面的认知,是类对象不唯一?其实这就是FFGoods的元类。捋一下,实例对象的isa指向类对象,类对象的isa指向元类对象

元类也是一个objc_class,也有一个isa指针;继续打印看看:

image-20220426111723331

这和我们认识的NSObeject,是不是一回事呢?我再通过p/x NSObject.class打印一下地址,发现并不一样:

image-20220426111856107

继续通过 x/4gx 打印NSObject的类对象及其isa指向的类对象:

image-20220426112302569

最终这个和前面元类的地址一样,这就是对象根类NSObject的元类,也叫根元类(它还是NSObject)。

捋一捋:元类对象的isa指向根元类,根类NSObject的指针也指向根元类

那么根元类有没有isa,有的话指向哪里?没有什么是LLDB不能打印的:

image-20220426112818556

原来,根元类的isa是指向自身isa不是应该包含对象相关信息吗,这里怎么直接等于自身内存地址了?

isa是一个指针只是用来存储内存地址,根元类或者元类没有引用计数,或者是否被弱引用。所以他们两的isa不是上篇文章说到的nonPointerIsa

这样一来,isa的指向形成了一个闭环

那么每个对象都能通过4个步骤找到根元类吗?先卖个关子,往下看。

类的继承

如果创建一个类,不继承NSObject?看个栗子:

image-20220426132421495

发现对于子类FFToys来说,元类的父类 = 父类的元类?也就是说元类之间也保持继承关系

接着看一下根类和根元类有没有父类:

image-20220426132746782

根类NSObject没有父类,NSObject元类的父类竟然是NSObject的类对象。

再补充一下类对象之间的关系:子类的类对象的父类 = 父类的类对象

image-20220426135036789

走位图

由此总结一下继承链,以自定义的父子类为例:

image-20220426133851194

NSObjectobjc的根类,所以它没有父类。根元类的父类都是NSObject

再总结一下isa的走向:

image-20220426135449380

类对象的结构

已知对象的本质是objc_object结构体,那么看一下类对象:其结构体objc_class也是继承objc_object

image-20220426140127546

superclass是指向父类对象的指针,cache我打算之后的文章里单独讲。接下来打算读bits这个内存空间,这里涉及到内存平移的概念。

内存平移

开发中用到的指针,在内存中也需要有个地址来存放指针;举个栗子:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 数组指针
        int c[4] = {5,6,7,9};
        int *d = c;
        NSLog(@"数组头指针:%p", &c);
        NSLog(@"通过下标获取元素的内存地址:%p - %p - %p - %p", &c[0], &c[1], &c[2], &c[3]);
        NSLog(@"通过地址偏移获取元素的内存地址:%p - %p - %p - %p", d, d + 1, d + 2, d + 3);
      
        for (int i = 0; i < 4; i++) {
            int value = *(d + i);
            NSLog(@"%d", value);
        }
    }
    return 0;
}

一般将c[4]看做数组指针,c指向数组的首地址;

通过*(d+i)就是一种内存平移。数组里存的是指针,取出第i个指针。

image-20220426143232935

根据声明的int类型,每个指针在内存上相差4字节。

类信息的读写

再看类对象的首地址,通过内存平移32字节(isa + superclass +cache的大小)就拿到bits地址(指针类型class_data_bits_t *

image-20220426150408474

看到$3的这一串数字,显然我不知道它是啥。回顾它的结构体:

image-20220426150714291

friend关键字难道是朋友关系?如果要访问一个类的私有成员,正常需要public关键字。c++提供的friend修饰符,和objc_class成为朋友。使其可以访问里面的私有数据;

参考:C++友元函数和友元类(C++ friend关键字)。友元类中的所有成员函数都是另外一个类的友元函数。例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。此时,B是objc_class,那么可以访问class_data_bits_t的所有成员。

注意:友元的关系是单向的、不能传递的。

调用一下公开的data()方法:

image-20220426152900632

要看懂这些,还得翻一下class_rw_t源码:看到methods()应该就是方法列表:

image-20220426153208934

一步步拿到方法列表的结构体,但是方法的数量对不上?属性namegetset,以及实例的init方法加起来应该是3个。

image-20220426161307355

跳转method_array_t


DECLARE_AUTHED_PTR_TEMPLATE(method_list_t)

class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
        return beginLists();
    }
    
    const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};

跳转method_list_t

image-20220426155837849

原来是通过entsize_list_tt这个模板生成的:

image-20220426164021369

在编程的世界里,是容器(container)都有迭代器(iterator),可以用来遍历这个容器。

根据上图的Element& get(uint32_t i),试试get方法,但是拿到的对象不显示具体内容:

image-20220426161528777

找到method_t,对于一个方法,应该有sel方法名,还有imp方法实现。

image-20220426161734169

这里面的big结构体正好就有我们需要的内容,接下来分别获取方法和属性列表。

image-20220426163747609

大小端

这里看到读取的big的公开方法:

image-20220426163307450

有个isSmall()的判断是什么?iOS系统是分大小端,intel电脑是大端模式,arm架构的是小端模式。

**大端:**数据的高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。

**小端:**数据的低位字节存储在内存的低地址端,高位字节存储在内存的高地址端。

举个例子,比如,我们要存储一个16进制数0x12345678,从内存地址0x1001开始存放。内存最低操作单元是字节,每个内存地址存放1字节的大小,而16进制数的2位等于1字节,所以总共要4个内存地址存放(地址低的是高位)。

// 1字节 = 8位(二进制),
// 而一位16进制转二进制是4位,例如 15 = 0xf = 1111 = 8 + 4 + 2 + 1

具体内容:

内存地址大端模式存放小端模式存放
0x10010x120x78
0x10020x340x56
0x10030x560x34
0x10040x780x12

用大端系统下的计算器表示:

image-20220426180303617

换算:

0001 0010 = 0x12; 
0011 0100 = 0x34; 
0101 0110 = 0x56; 
0111 1000 = 0x78;

实例方法

(大端设备)逐个读取方法:

image-20220426163455388

这多出来的.cxx_destruct是什么?我搜到的是析构方法,在ARC模式下用于释放成员变量的。当类拥有成员变量的时候,就会自带这个方法。包括属性自动生成的成员变量也算。

类方法

类对象里只有实例方法,类方法应该是存放在元类里。

接着就通过获取元类的class_rw_t结构中的方法列表来验证一下。

image-20220427175131638

可以看到元类的方法列表里只有这个类方法。

图中涉及的指令:

// x/6gx meta
// p/x (class_data_bits_t *)0x100008220
// p *$1
// p $2.data()
// p *$3
// p $4.methods()
// p $5.list
// p $6.ptr
// p *$7
// p $8.get(0).big()

属性

同理可得属性列表。

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};
// 跳转 property_list_t
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

调试如下:p/x $5.properties()

image-20220426165506500

超过属性数量的时候就出现Assertion failed

如果给类添加成员变量,会不会出现在属性列表里呢?修改,重试一遍:

image-20220426171358360

属性列表里数量没变,也就是没有成员变量_privateProperty。欲知后事如何,且听下回分解。iOS底层专栏

总结

isa指向

实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身。

类的继承链

对于NSObject、父类、子类,他们的类对象、元类对象都保持继承关系。

根元类的父类就是NSObject类对象。

NSObject类对象是万类之祖,没有父类。

类对象

类对象本质为objc_class结构体,有且只有一个。存储了isasuperclass(父类)、bits(属性、实例方法、协议、成员变量)、cache(方法缓存)。

class_rw_t

初步认为类的信息存储在class_rw_t结构体中,已经验证包含了属性和方法。

entsize_list_tt

entsize_list_tt 是个模板,可以实例化出method_list_t、ivar_list_t、property_list_t三种类型。

// Element:表示元素类型 List:表示容器类型 FlagMask:标记位
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>

.cxx_destruct

.cxx_destruct方法是在ARC模式下用于释放成员变量的。只有当前类拥有实例变量时这个方法才会出现,property生成的实例变量也算,且父类的实例变量不会导致子类拥有这个方法

大小端

大端的意思就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。小端就相反。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值