类的结构

在上一个章节,讲到了isa,然后直接引申到类,那么究竟什么是类呢,跟源码,看结构发现,Class实际上就是objc_class这么一个结构体。
屏幕快照 2020-03-23 18.23.09.png
objc_class又继承自objc_object。为什么objc_class中的第一个属性ISA会被注释?这个就是提醒开发者,这是一个隐藏属性,继承自objc_object的光环,看下objc_object你就知道了。
屏幕快照 2020-03-23 18.25.23.png
是不是一目了然?回到objc_class,类的结构有这四个属性:

  1. // Class ISA;
  2. Class superclass;
  3. cache_t cache; // formerly cache pointer and vtable
  4. class_data_bits_t bits;

这四个属性拆开来看,
ISA:Class类型:typedef struct objc_class *Class;指针;
superclass:类型同上,顾名思义,父类;
cache_t:这个是一个结构体,用于缓存方法的imp,看下cache_t的结构:
屏幕快照 2020-03-23 18.30.07.png
class_data_bits_t:这个属性是一个struct结构体类型,我们所熟知的属性啊,成员变量就存储在这里。

类的创建时机

类的创建,这可能有点陌生。开发中一般都是创建个类的实例对象,比如创建个Person类的实例对象,然后怎样怎样。但是具体类是什么时候创建的呢?
类是在编译期就已经创建并加载到内存了。下面我们通过两种方式来验证:

lldb打印调试验证

屏幕快照 2020-03-25 17.52.15.png
如上图:将断点打在Person *person = [Person.class alloc];这一行,首先明确,断点到这儿的时候,这一样代码还没执行。person对象为nil。
好,开始lldb调试(这些指令和0x0000000ffffffff8的含义在这篇文章提到过):
直接x/4xg 打印Person类,发现已经可以打印出数据,通过他的第一个属性isa也已经成功找到Person这个元类。所以说在创建Person对象之前,Person这个类就已经存在于内存之中。

MachOView查看验证

如果说上面的方法运行程序了,不足以说明类是在编译期创建的,那么这个方法来看;直接command + b 编译一下项目,然后在Products文件夹下边找到这个待执行文件,拖到MachOView中,
屏幕快照 2020-03-25 17.59.53.png
屏幕快照 2020-03-25 18.03.00.png
我们仅仅command + b 编译了一下工程,就已经可以在内存中找到这个Person类,这更加明确的验证了“类是在编译期就已经创建并加载到内存了。”

objc_object和NSObject

在没有翻源码之前的认知里,万物皆对象万物皆NSObject,类就是Class。但是翻了源码发现,Class其实是个objc_class的结构体,并且继承自objc_object。那么,这一一对应的两者objc_object和NSObject是什么关系呢?
其实OC的底层是由c实现的,objc_object可以说是用c来写的,但是我们开发中用的是OC啊,所以可以认为NSObject是objc_object的一种仿写,他们的本质其实是一样的。

类定义的那些属性和成员变量

类中定义一个属性,比如title,编译器会自动生成setget方法,并且会创建一个带下划线的_title成员变量;类中定义一个成员变量,不会生成setget方法。下面验证一下:

编译成cpp文件查看验证

在Person.m文件中添加一个属性title和成员变量nickName
屏幕快照 2020-03-30 14.25.48.png
然后,将终端进入Person.m文件所在目录,执行 clang -rewrite-objc Person.m -o Person.cpp 指令,即可在指定的目录下看到编译好的Person.cpp文件,全局搜索(command + f)Person_IMPL 看到下面这个结构体:
屏幕快照 2020-03-30 14.32.05.png
Person_IMPL 就是Person类通过编译之后的样子,第一个参数NSObject_IMPL我暂且称他为父类NSObject的成员变量(我猜的,所有集成NSObject的类编译完之后,第一个参数都是他),
nickName就是我们之前定义好的成员变量,_title就是编译自动生成的对应title属性的成员变量。往下看,有一个ivar_list,同样可以看到这两个成员变量:
屏幕快照 2020-03-30 14.39.49.png
继续往下找,method_list:
屏幕快照 2020-03-30 14.33.36.png
红框圈出来的正是title属性的get和set方法,至于@16@0:8的意思请参考iOS符号

Runtime方法打印验证

下面通过runtime api 提供的方法来打印一下ivar、property和method:
先贴出来runtime的方法:

  1. // 获取class的所有成员变量
  2. void testObjc_copyIvar(Class class){
  3. unsigned int count = 0;
  4. Ivar *ivars = class_copyIvarList(class, &count);
  5. for (unsigned int i=0; i < count; i++) {
  6. Ivar const ivar = ivars[i];
  7. //获取实例变量名
  8. const char*cName = ivar_getName(ivar);
  9. NSString *ivarName = [NSString stringWithUTF8String:cName];
  10. NSLog(@"class_copyIvarList:%@",ivarName);
  11. }
  12. free(ivars);
  13. }
  14. // 获取class的所有属性
  15. void testObjc_copyProperies(Class class){
  16. unsigned int pCount = 0;
  17. objc_property_t *properties = class_copyPropertyList(class, &pCount);
  18. for (unsigned int i=0; i < pCount; i++) {
  19. objc_property_t const property = properties[i];
  20. //获取属性名
  21. NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
  22. //获取属性值
  23. NSLog(@"class_copyProperiesList:%@",propertyName);
  24. }
  25. free(properties);
  26. }
  27. // 获取class的所有方法
  28. void testObjc_copyMethodList(Class class){
  29. unsigned int count = 0;
  30. Method *methods = class_copyMethodList(class, &count);
  31. for (unsigned int i=0; i < count; i++) {
  32. Method const method = methods[i];
  33. //获取方法名
  34. NSString *key = NSStringFromSelector(method_getName(method));
  35. NSLog(@"Method, name: %@", key);
  36. }
  37. free(methods);
  38. }

Tips:以上函数都是在main.m文件中写的,所有函数的格式符合C语言语法,调用的时候也直接C函数调用,还有函数应该写在main函数上边,或者先在main函数上边声名再在下边实现也可以。(应该是这样,C语法记不太清)

看一下打印结果:
屏幕快照 2020-03-30 15.13.30.png
和上边的编译看出来的结果是一样的,很完美,后面再看一下,实例方法和类方法,分别存在哪儿。

实例方法和类方法

存在位置

上边,在testObjc_copyMethodList方法中,打印出了编译过程中,系统自动生成的getset方法,这是在传来的class也就是Person类中通过runtime提供的class_copyMethodList方法获取的方法列表,去掉属性,分别定义一个对象方法和类方法,在通过上边testObjc_copyMethodList方法打印一下:
屏幕快照 2020-03-31 10.57.47.png
执行结果:
屏幕快照 2020-03-31 10.59.43.png
testObjc_copyMethodList方法接收的参数是Person类,打印发现,只有输出一个personA这个实例方法,那personB这个类方法呢,他为什么打印不出来?
原因:实例方法存在类中,类方法存在元类中
通过object_getClass(class)获取Person类的元类,作为参数传入testObjc_copyMethodList方法中打印:
屏幕快照 2020-03-31 11.03.54.png
这次,在元类Person中打印找到了personB这个类方法。
叨叨一句:之前提到的类对象,可以就看成元类的对象,而类方法存在元类中,也可以称之为元类的“实例方法”。

class_getInstanceMethod和class_getClassMethod

看下下边的案例,personA为实例方法,personB为类方法:

  1. void testInstanceMethod_classToMetaclass(Class pClass){
  2. const char *className = class_getName(pClass);
  3. Class metaClass = objc_getMetaClass(className);
  4. Method method1 = class_getInstanceMethod(pClass, @selector(personA));
  5. Method method2 = class_getInstanceMethod(metaClass, @selector(personA));
  6. Method method3 = class_getInstanceMethod(pClass, @selector(personB));
  7. Method method4 = class_getInstanceMethod(metaClass, @selector(personB));
  8. NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
  9. NSLog(@"%s",__func__);
  10. }
  11. void testClassMethod_classToMetaclass(Class pClass){
  12. const char *className = class_getName(pClass);
  13. Class metaClass = objc_getMetaClass(className);
  14. Method method1 = class_getClassMethod(pClass, @selector(personA));
  15. Method method2 = class_getClassMethod(metaClass, @selector(personA));
  16. Method method3 = class_getClassMethod(pClass, @selector(personB));
  17. Method method4 = class_getClassMethod(metaClass, @selector(personB));
  18. NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
  19. NSLog(@"%s",__func__);
  20. }

如果method有值就能打印出来指针,跑一下看看:
屏幕快照 2020-03-31 14.53.16.png
objc_getMetaClass(className):获取元类
来解释一下,首先class_getInstanceMethod(class,sel):获取class的sel实例方法
personA是Person类的实例方法,所以前两个结果为1、0(1表示找到method,0表示没找到),personB为Person类的类方法,存在Person元类中,可以视为personB为Person元类的实例方法,所以后边两个为0、1;

再来看class_getClassMethod(class,sel):获取类方法:
看一下class_getClassMethod的源码:
屏幕快照 2020-03-31 15.04.20.png
getMeta()源码:
屏幕快照 2020-03-31 15.06.07.png
一目了然啊,里边竟然就是通过class获取到元类,然后将这个元类sel作为参数去调用class_getInstanceMethod方法,
终极解释:personA为实例方法存在Person类中,在Person元类肯定找不到,前两个为0;personB为类方法,存储在Person元类中,在Person元类中能找到,所以后边为1,没毛病吧?
关于class_getInstanceMethod里边的实现原理,请参考方法的查找流程