美文网首页
类-初探

类-初探

作者: 生产八哥 | 来源:发表于2021-01-29 17:07 被阅读0次

alloc干了什么

在探究这个问题前,需要现有一份可编译的源码

对象的本质是什么:结构体

通过源码,我们发现新创建的对象alloc流程大致如下

alloc流程.png

核心共三步:

  1. 计算对象所需空间:对于继承自NSObject没有任何属性的自定义类而言,默认有个isa结构体。占8字节。最低16字节起,if (size < 16) size = 16;,采用16字节内存对齐原则(具体可查看源码,其中还包括结构体/联合体isa_t的对齐规则)。
  2. 开辟计算的空间,返回地址指针
  3. 创建isa指针,将class和isa指针关联并返回

所以一个对象是由其isa指针其他属性,当这个对象只是alloc的时候,只需要开辟isa空间。当对这个对象的属性赋值的时候,对象就能打印出属性值了。

(lldb) p/x p
(Person *) $0 = 0x0000000100690760
(lldb) x/4gx 0x0000000100690760
0x100690760: 0x011d800100008535 0x0000000000000000
0x100690770: 0x0000000000000000 0x0000000000000000

// 跳过一个断点给对象赋值属性后
(lldb) x/4gx 0x0000000100690760
0x100690760: 0x011d800100008535 0x0000000000000000
0x100690770: 0x0000000100004078 0x0000000000000000
(lldb) po 0x0000000100004078
属性赋值了

alloc创建的对象实际的引用计数为0,其引用计数打印结果为1,是因为在底层rootRetainCount方法中,引用计数默认+1了,但是这里只有对引用计数的读取操作,是没有写入操作的,简单来说就是:为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然为0

init干了什么

//类方法
+ (id)init {
    return (id)self;
}
//实例方法
id
_objc_rootInit(id obj)
{
    return obj;
}

可见他们都是返回了传入的self本身。

new干了什么

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

new其实就是调用了alloc init

但还是推荐用init方法初始化对象,因为你可以自定义实例化方法和传参,比new拓展性高,更加规范。

isa是什么

isa在底层是一个isa_t的联合体。暴露给外界的时候,isa_t需要强转成Class类型暴露给开发人员,这样是为了让开发人员更清晰的看到isa的职责就是保存类信息等。

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    //Class _Nonnull isa ;//8字节 说明类还有isa指针,指向了`元类`
    Class _Nullable super_class;//8字节
    cache_t cache;             //计算cache_t内部属性大小,16字节 
    class_data_bits_t bits;   //属性、方法列表等信息 所以要获取bits,需要把内存地址从首地址移动0x20位就能拿到bits里面的数据
   class_rw_t *data() const {
        return bits.data();
    }
    ....
}

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
};
// isa中真正存储类信息的结构
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t has_cxx_dtor      : 1;                                       \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19

objc2.0 中,所有的对象和类都会包含一个 isa_t 类型的结构体。同时,因为 objc_class 继承自 objc_object,所以所有的类也包含这样一个 isa。在优化之前,isa 只是一个指向类或元类的指针,而优化之后,采取了联合体结构,同样是占用8字节空间,但存储了更多的内容。

联合体相较于结构体,其所有成员占用同一段内存,且占用的内存大小等于最大成员占用的内存

通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,在arm64环境下,前三位为布尔值,从第四位开始存储shiftcls类信息,共33位。

isa指针分为纯指针和nonpointer_isa两种情况,nonpointer即开启了指针优化,其不只是类对象地址,isa中包含了类信息、对象的引用计数等。其中主要的类信息存储在isa的bits结构体的shiftcls字段里,而shiftcls的值是cls经过位域运算得到的即shiftcls = (uintptr_t)newCls >> 3,即跳过nonpointerhas_assochas_cxx_dtor,均为布尔类型,各占一位;。代码中要想拿到类信息,需要进行位域运算isa & ISA_MASK,即找到shiftcls在其所在的结构体中的位置,arm64中ISA_MASK的值为0x0000000ffffffff8ULL

元类

通过对代码进行汇编成C++或对源码调试类结构代码,会得出下图结构。上个大神图,理解了这个图就理解了类结构。


isa.png

为了加深对这张图的理解,这里举个🌰

NSObject的分类分别声明同名的实例方法和类方法,然后只实现实例方法。接着去调用这个类方法,结果是执行到了实例方法里。

为什么会这样?首先说明几个知识点:

  1. 实例方法存储在类中,类方法存储在元类中
  2. 方法的查找是顺着继承链查找的
  3. isa的指向和继承链是不同的,内存中只存在存在一份根元类NSObject,根元类的元类是指向它自己。
    所以执行一个不存在的类方法,会顺着上面这个图找到根元类NSObject,根元类找不到此方法,进而去寻找指向的NSobject根类,而在这一步,查找类方法就变成了查找实例方法
类方法在元类里是实例方法
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Class getMeta() 
{
    if (isMetaClass()) return (Class)this; //如果是元类,则返回元类本身
    else return this->ISA(); //否则返回类的isa指向,即元类。
}

调用getClassMethod,传入类,则查找其元类。传入类方法,则查找实例方法。
Method method1 = class_getClassMethod(pClass, @selector(instanceMethod));//查找顺序 元类 --> 根元类 --> 根类 --> nil,查找(类的)实例方法,找不到,返回NULL
Method method2 = class_getClassMethod(metaClass, @selector(instanceMethod));//在元类查找(类的)实例方法,找不到,返回NULL
Method method3 = class_getClassMethod(pClass, @selector(classMethod));//传入类,即在cls->getMeta()元类中查找实例方法,可以找到
Method method4 = class_getClassMethod(metaClass, @selector(classMethod)); //同理可以找到

获取类方法的底层实现就是直接拿实例方法,而class_getClassMethod 传参(类或元类,类方法) 均能获取到。 在类里获取类方法可以理解(由于系统处理,相当于是在元类里获取实例方法),但为什么在元类里获取类方法也能获取到,类方法在元类里不是实例方法吗。 原因在于其底层实现的cls->getMeta(),即 对类或者元类取类方法,都变成了是在对元类取实例方法

实例方法和类方法本质都是函数

在底层查找不到方法即将抛出错误unrecognizedMethod的前一步,方法的+号还是-号是这么判断的。class_isMetaClass(objc_getClass(self)) ? '+' : '-‘;这里用了一个三目运算。 即判定是否是元类。即不管是类方法还是实例方法,在底层都是函数形式。

在什么情况下定义类方法,在什么情况下定义实例方法

比如,一个型号的车是一个类。那么,同一型号的车就是车这个类的不同对象,他们有着相同的初始属性(比如型号、发动机等)。
如果我们把洗车看作是一个方法,那么,洗车是一个实例方法。为什么?因为洗车对应的是其中一辆车,而不是车这一个类。
如果我们把统计一共生产了多少辆该型号的车看作一个方法的话。这个方法就是类方法,因为它针对的是车这一个类,而不是具体的哪一辆车。
综上,如果我们定义的方法中需要修改实例的属性的话,就要定义成实例方法,如果不需要用到具体实例中的属性,就定义成类方法

isKindOfClass和isMemberOfClass

isKindOfClass 和 isMemberOfClass,都是拿receiver的isa(即对象则比较类,类则比较元类)进行比较,一个会沿着继承链进行比较,一个不会,这样就很好记了。

为什么设计元类

这个问题不同的阶段的人应该会有不同的理解。

个人理解:首先iOS是基于消息机制这个大前提下,且objc_msgSend的调用流程是一定要isa指针和方法名。没有元类的话,重名的实例方法和类方法就没有办法区分。如果实例方法和类方法都放在类对象上,那类对象的isa指针只能指向自己了,那一旦类方法和实例方法重名,就没法搞了!所以是为了区分实例方法和类方法的声明与调用。但前提是建立在msgSend这套体系上的。如果想把元类干掉,把类方法和实例方法放在类的不同数组中,可以是可以(分类就是这么干的,因为没有分元类这一说,分类结构体中分别存储了实例方法和类方法),但相当于要判断这个方法是实例还是类方法,那么从查找缓存到执行都要判断,相当于msgSend除了self,_cmd,还要加个标识参数,这样效率会更低。
通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,职责分离,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。元类的独立让类的结构更简洁,实例所需内存空间更小,执行效率更高

调试步骤

(lldb) p/x p
(Person *) $0 = 0x00000001006ac450
(lldb) x/4gx 0x00000001006ac450
0x1006ac450: 0x011d800100008535 0x0000000000000000
0x1006ac460: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x011d800100008535 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008530 //类地址 和Person.class得到的相同
(lldb) x/4gx 0x0000000100008530
0x100008530: 0x0000000100008508 0x0000000100357140
0x100008540: 0x000000010034f360 0x0000802c00000000


(lldb) po 0x0000000100008530 
Person //类信息

(lldb) p (class_data_bits_t *)0x0000000100008550 //这里要在类首地址的基础上平移32字节,即0x20才能拿到bits
(class_data_bits_t *) $4 = 0x0000000100008550
(lldb) p $4->data()
(class_rw_t *) $5 = 0x00000001006ac090
(lldb) p * $5
(class_rw_t) $6 = { //class_rw_t里存储着干净内存ro,属性,方法,协议等,旧版本会全部显示出来,新版本自动隐藏显示了
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000800
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $6.properties()
(const property_array_t) $7 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100008210
      }
      arrayAndFlag = 4295000592
    }
  }
}
(lldb) p $7.list
(const RawPtr<property_list_t>) $8 = {
  ptr = 0x0000000100008210
}
(lldb) p (property_list_t *)$8 //这样在最新版本objc818拿不到了
error: <user expression 13>:1:1: cannot cast from type 'const RawPtr<property_list_t>' (aka 'const WrappedPtr<property_list_t, PtrauthRaw>') to pointer type 'property_list_t *'
(property_list_t *)$8
^~~~~~~~~~~~~~~~~~~~~
(lldb) p $7.begin()  //这样可以
(list_array_tt<property_t, property_list_t, RawPtr>::iterator) $11 = {
  lists = 0x00000001005f9850
  listsEnd = 0x00000001005f9858
  m = {
    entsize = 16
    index = 0
    element = 0x0000000100008218
  }
  mEnd = {
    entsize = 16
    index = 3
    element = 0x0000000100008248
  }
}
(lldb) p (property_t *)0x0000000100008218
(property_t *) $12 = 0x0000000100008218
(lldb) p $12[0]
(property_t) $13 = (name = "fenlei", attributes = "T@\"NSString\",C,N")
(lldb) p $12[1]
(property_t) $14 = (name = "kuozhan", attributes = "T@\"NSString\",C,N,V_kuozhan")

类的首地址偏移16字节可以拿到缓存cache_t,偏移32字节可以拿到类信息class_data_bits_t
class_data_bits_t的data()是class_rw_t结构体
这些打印都是根据底层代码来的,所以要结合源码来看。

ivars

通过查看objc_classbits属性中存储数据的类class_rw_t的定义发现,除了methods、properties、protocols方法,还有一个ro方法,其返回类型是class_ro_t,通过查看其定义,发现其中有一个ivars属性,打印后会发现所有成员变量都在这里。

通过{}定义的成员变量,会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量.

通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性.

methods (实例方法和类方法在底层都是函数,是通过查找方法时的传参是类还是元类来判断其是实例方法还是类方法)

类的实例方法存储在bits属性中,通过类bits --> methods() --> list获取实例方法列表,类中的方法列表除了包括实例方法,还包括属性的set方法 和 get方法

类的类方法存储在元类bits属性中,通过元类bits --> methods() --> list获取类方法列表.

相关文章

  • 类-初探

    alloc干了什么 在探究这个问题前,需要现有一份可编译的源码[https://opensource.apple....

  • Unsafe类初探

    Unsafe类说明 从这个类的名字Unsafe上来说这个类就是一个不安全的类,也是不开放给用户直接使用的(当然我们...

  • Spring-Security配合OAuth2初探

    Spring-Security配合OAuth2初探 环境 SpringBoot1.5.2, Tomcat 核心类 ...

  • 类的结构分析

    类分析初探 基于isa结构分析 ,我们可以通过lldb获取对象的内存情况 创建一个Person类对象 查看类对象的...

  • iOS 类的结构分析

    1. 类的初探 在isa结构解析中,自定义LSPerson 类继承自NSObject,重写成C++代码如下 str...

  • Java集合类初探

    参考原文 一 java集合类简介 1、java集合大致可以分为Set、List、Queue、Map四类。 Set:...

  • Swift类的初探

  • 类的结构初探

    本篇文章针对类的结构进行初步的分析,对之前学习的内容做一个小小的总结。 文章的分析主要是利用lldb断点调试,通过...

  • OC类的初探

    前言 每个对象都会有一个它所属的类,这是面向对象的基本概念。但是在OC中,这对所有数据结构有效。任何数据结构,只要...

  • 苹果 ARKit 初探

    苹果 ARKit 初探 苹果 ARKit 初探

网友评论

      本文标题:类-初探

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