iOS类的原理探索分析

322 阅读2分钟

前言

之前了解到isa的结构,可以看到isa里shiftcls存放的是类指针的值。这篇文章就来探索下类的结构。

isa走向

创建一个对象

DDAnimal *a = [[DDAnimal alloc] init];

通过LLDB调试,获取对象的isa

image.png

通过这几步操作,我们可以看到对象的isa是指向了DDAnimal。都说万物皆是对象,那我们来看看DDAnimal的isa是指向了哪里?

image.png

果然拿到类的数据了,我们先来看看类的结构是怎么样的。

image.png image.png image.png

一波操作,可以看出Class就是objc_class,而objc_class继承自objc_object,objc_object里面的有且仅有一个成员就是isa,那么0x0000000100008718就是DDAnimal的isa。

image.png

继续操作,拿到类的地址里的数据,通过isa与上掩码,最终得到的数据是DDAnimal类又指向了一个DDAnimal,这是为什么呢,先不管了,我们再往下看看,这个DDAnimal的isa又是指向哪里的。

image.png

这里指向的类又不同了,这一个DDAnimal的isa指向变成了NSObject,再往下看看,这个NSObject的isa还指向了哪里。

image.png

通过上面的调试,我们看了$8打印出来的isa是指向了自己,都是0x000000010036a0f0。再用这个isa去与上掩码得到结果仍然是一样的,这里可以看出已经形成了闭环。

这一波操作我们可以看出 对象a的isa指向了类DDAnimal,DDAnimal的isa指向了又一个DDAnimal,第二个DDAnimal的isa指向了NSObject,NSObject的isa指向了自己。
那么问题是:对象a的isa指向了DDAnimal了,为什么DDAnimal的isa又指向了一个DDAnimal?用烂苹果(MachOView)看看有什么.

image.png

在烂苹果里搜索DDAnimal,可以看到_OBJC_CLASS_$_DDAnimal就是我们创建的DDAnimal,发现上面还多了一个_OBJC_METACLASS_$_DDAnimal类型的DDAnimal,可以看出系统自动给我们创建了一个metaclass类型,这个就是元类了。

同样的NSObject也有一个元类,如图:

image.png 那么我们获取的NSObject是_OBJC_CLASS_$_NSObject还是_OBJC_METACLASS_$_NSObject呢,再来验证一下。

image.png

可以看出NSObject类的地址和上面得到的不一样,而NSObject类的isa指向的地址和上面得到的一样,都是0x000000010036a0f0。这里验证出来我们DDAnimal最终指向的应该是_OBJC_METACLASS_$_DDAnimal,也是一个元类,NSObject是根类,那么_OBJC_METACLASS_$_NSObject就叫做根元类。

总结一下,对象的isa -> 类(DDAnimal)类的isa -> (DDAnimal的)元类元类的isa -> (NSObject)根元类根元类的isa -> 根元类(自己)

继承关系

这里直接用代码来看,更加清晰。

// DDAnimal实例对象
DDAnimal *object1 = [DDAnimal alloc];
// DDAnimal类
Class class = object_getClass(object1);
// DDAnimal元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// DDCat元类
Class cMetaClass = object_getClass(DDCat.class);
Class csuperClass = class_getSuperclass(cMetaClass);
NSLog(@"%@ - %p",csuperClass,csuperClass);
// DDAnimal元类
Class aMetaClass = object_getClass(DDAnimal.class);
Class asuperClass = class_getSuperclass(aMetaClass);
NSLog(@"%@ - %p",asuperClass,asuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(object_getClass(aMetaClass));
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
// NSObject 根类
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);

打印结果

> 0x100607420    // 实例对象  
> 0x1000086e0    // 类  
> 0x1000086b8    // 元类  
> 0x7fff88a6bd60 // 根元类  
> 0x7fff88a6bd60 // 根根元类  
> 
> DDAnimal - 0x1000086b8     // 元类
> NSObject - 0x7fff88a6bd60  // 根元类
> NSObject - 0x7fff88a6bd88  // 根类
> (null) - 0x0

从上半部分的打印可以印证上面所得的isa走向图。下面部分则表示了类的继承关系DDCat元类继承自DDAnimal元类DDAnimal元类继承自根元类根元类继承自NSObjectNSObject继承nil
接下来看一个走势图,这是苹果官方的资料,图上的走势和指向我们都得到了验证。

isa流程图.png

类的结构

先把我们创建的类贴出来

@interface DDAnimal : NSObject{
    NSInteger _weight;
    NSString *sex;
}

@property (nonatomic, copy) NSString *cate;    ///<
@property (nonatomic, copy) NSString *age;    ///<
@property (nonatomic, assign) NSInteger height;    ///<

- (void)jump;
+ (void)run;

@end

接下来查看类的源码

struct objc_class : objc_object {
    // Class ISA;              // isa
    Class superclass;          // 父类
    cache_t cache;             // 缓存
    class_data_bits_t bits;    
}

通过查看类的结构源码,这里只贴出了成员的代码,其他的代码都是一些方法,有点多,感兴趣的可以自己去看看。如上所示,主要有四个成员,不难看出,前三个成员就是存储一些其他信息,第四个成员应该是存储了当前类的信息。要怎么拿到bits呢,这里我们就要用到内存偏移,要内存偏移就要知道偏移多少,前面就三个成员,isa是8个字节,superclass是8个字节,那么cache有多少字节呢?

cache_t字节大小

通过查看cache_t源码,是一个200多行的结构,看到头就大,但是仔细一看,会发现里面大多都是函数方法,函数方法不占用结构体内存,除开函数方法,其他还有很多的static,存在静态区,也不占用结构体内存,那么就只剩下

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;  // 8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;    // 4字节
#if __LP64__
            uint16_t                   _flags;        // 2字节
#endif
            uint16_t                   _occupied;     // 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
}

从上图不难算出cache_t的内存大小是16个字节。
结合上面的16个字节,那么总计是32个字节大小,需要偏移32个字节。

class_data_bits_t

通过查看源码可以看出bits里包含了属性、方法和协议的值,我们尝试能不能获取到这些值。

// 获取到类的首地址
(lldb) x/4gx DDAnimal.class
0x100008740: 0x0000000100008718 0x000000010036a140
0x100008750: 0x000000010112d810 0x0001803800000003
// 内存偏移32个字节,也就是0x20
(lldb) p/x 0x100008740 + 0x20
(long) $1 = 0x0000000100008760
// 强转成class_data_bits_t
(lldb) p/x (class_data_bits_t *)0x0000000100008760
(class_data_bits_t *) $2 = 0x0000000100008760
// 查看源码得知属性和方法存在class_rw_t中,并且可以通过data()方法获取。
(lldb) p/x $2->data()
(class_rw_t *) $3 = 0x000000010112d7c0

获取到了class_rw_t数据,接下来就可以查看属性和方法了

// 先来获取属性
(lldb) p/x $3->properties()
(const property_array_t) $4 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100008588
      }
      arrayAndFlag = 0x0000000100008588
    }
  }
}
// 通过查看property_array_t的代码,它是继承自list_array_tt,在list_array_tt里看到元素是存储在ptr里,接着获取ptr,并打印地址里的数据
(lldb) p/x $4.list.ptr
(property_list_t *const) $5 = 0x0000000100008588
(lldb) p/x *$5
(property_list_t) $6 = {
  entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 0x00000010, count = 0x00000003)
}
// count = 3,里面存了三个属性,在list_array_tt里看到了iterator迭代器,那么就可以通过get(i)的方式获取,操作走起
(lldb) p/x $6.get(0)
(property_t) $7 = (name = "cate", attributes = "T@\"NSString\",C,N,V_cate")
(lldb) p/x $6.get(1)
(property_t) $8 = (name = "age", attributes = "T@\"NSString\",C,N,V_age")
(lldb) p/x $6.get(2)
(property_t) $9 = (name = "height", attributes = "Tq,N,V_height")

从上面可以看出只获取到了属性,并没有获取到成员变量,并且rw里也没有看到成员变量。如法炮制,再获取方法。

(lldb) p/x $3->methods()
(const method_array_t) $10 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100008430
      }
      arrayAndFlag = 0x0000000100008430
    }
  }
}
(lldb) p/x $10.list.ptr
(method_list_t *const) $11 = 0x0000000100008430
(lldb) p/x *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 0x0000001b, count = 0x00000007)
}
(lldb) p/x $12.get(0).big().name
(SEL) $13 = 0x0000000100003f36 "cate"
(lldb) p/x $12.get(1).big().name
(SEL) $14 = 0x0000000100003f3b "setCate:"
(lldb) p/x $12.get(2).big().name
(SEL) $15 = 0x00007fff7c54dbd5 "setHeight:"
(lldb) p/x $12.get(3).big().name
(SEL) $16 = 0x00007fff7c55165c "height"
(lldb) p/x $12.get(4).big().name
(SEL) $17 = 0x00007fff7c769392 "age"
(lldb) p/x $12.get(5).big().name
(SEL) $18 = 0x00007fff7c769396 "setAge:"
(lldb) p/x $12.get(6).big().name
(SEL) $19 = 0x00007fff7ce5fba8 "jump"
(lldb) 

从上面可以看出这里获取到了所有的方法,除了类方法,同样我们也没有找到类方法的获取方式。

Tips:这里获取方法的方法和获取属性的有一点区别,直接使用get(),打印出来的是空数据,是因为method_t里自定义了方法,查看代码,了解到需要使用big()来获取。 接下来要去查询成员变量和类方法,接着看代码,找到了class_ro_t,在ro里找到了ivarsbaseMethods,如法炮制,通过class_rw_t里的ro()获取到了class_ro_t,在ivars里面找到成员变量,但是在baseMethods里还是没有看到类方法。
通过复盘可知,该找的地方都找了,那么类方法会不会不是存在类里面的呢,如果不是在类里面,还会在哪里呢?对了,类还有一个元类,我们到元类里去找找,LLDB走起。

(lldb) p/x 0x0000000100008718 & 0x00007ffffffffff8  // 拿到元类的地址
(long) $21 = 0x0000000100008718
(lldb) p/x (class_data_bits_t *)0x0000000100008738  // 这里直接在0x0000000100008718的基础上加了0x20,也就相当于是在isa的地址上加0x20
(class_data_bits_t *) $23 = 0x0000000100008738
(lldb) p/x $23->data()
(class_rw_t *) $24 = 0x000000010112d7a0
(lldb) p/x $24->methods()
(const method_array_t) $25 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x00000001000083c8
      }
      arrayAndFlag = 0x00000001000083c8
    }
  }
}
(lldb) p/x $25.list.ptr
(method_list_t *const) $26 = 0x00000001000083c8
(lldb) p/x *$26
(method_list_t) $27 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 0x0000001b, count = 0x00000001)
}
(lldb) p/x $27.get(0).big().name
(SEL) $28 = 0x00007fff7c52dabc "run"

耶思,获取到了类方法。

总结

  1. 对象的isa走位和继承的走位很重要,理解透彻了,会对学习底层理解底层有很大帮助。
  2. 类的属性和属性方法存在类的class_rw_t中,类的成员变量存在类的class_ro_t中,类的类方法存在元类的class_rw_t中。