谈谈我对Objective-C对象本质的理解

1,649 阅读8分钟
原文链接: www.jianshu.com

【原创博文,转载请注明出处!】
Objective-C的本质
我们平时编写的Objective-C代码,底层实现其实都是C、C++代码,所以Objective-C的面向对象是基于C、C++的数据结构实现的。
思考:Objective-C的对象、类主要是基于C、C++的什么数据结构实现的呢?
因为对象或类可以有各种类型的实例(NSString *, CGFloat, NSArray *),能存放不同类型的数据结构,无非就是结构体了。显然,Objective-C的对象、类主要是基于C、C++的结构体实现的。
为了证明这一点,我们试图将OC代码转成更为底层一点的代码来一探究竟。
因为底层C++代码对C是完全兼容的,所以暂且将OC转成C++代码
借助终端使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp来实现。

解释一下:
xc就是Xcode的缩写。
xcrun是Xcode的一种工具。
-sdk iphoneos规定sdk需要运行在iOS系统上面。
clang是Xcode内置的llvm编译器前端,也是编译器的一种。
-arch xxx(arm64、i386、armv7...)指出iOS设备的架构。
参数 -rewrite-objc xx.m 是重写objc代码的指令(即重写xx.m文件) 。
-o newFileName.cpp 表示输出新的.cpp文件。
终端指令成功生成.cpp文件.png
NSObject对象的实现.png
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end

将系统NSObject的定义简化为:

@interface NSObject <NSObject> {
      Class isa ;
}
@end

点击Class后进入objc.h,Class 定义为 : typedef struct objc_class *Class; 也就是说Class是个结构体指针,因此系统的NSObject类仅有唯一的一个成员变量,即isa指针。

一个NSObject对象占用多少内存?
了解了OC对象的基本结构之后,再讨论一下对象所占用的内存情况。

<objc/runtime.h>文件提供class_getInstanceSize(Class _Nullable cls)方法,返回我们一个OC对象的实例所占用的内存大小;
<malloc/malloc.h>文件提供 size_t malloc_size(const void *ptr)方法返回系统为这个对象分配的内存大小。

测试:

  • 看一个没有成员变量的类的实例(以NSObject为例)
NSObject *obj = [[NSObject alloc] init];
    NSLog(@"NSObject实例大小--> %zd",class_getInstanceSize([obj class]));
    NSLog(@"obj实际分配的内存%zd",malloc_size((const void *)obj));

//    2018-07-18 00:30:28.033 LJMahjong-mobile[41409:3330831] NSObject实例大小--> 8
//    2018-07-18 00:30:28.034 LJMahjong-mobile[41409:3330831] obj实际分配的内存16
  • 再来看看一个普通的类的实例,并且实例有自己的成员变量(新建一个Student类,为其添加属性age、name等)
@interface Student: NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) BOOL male;

@end;

@implementation  Student

@end;


  Student *stu = [Student new];
  stu.name = @"Rephontil.ZHou";
  stu.age = 25;
  stu.male = YES;
  NSLog(@"Student实例大小--> %zd",class_getInstanceSize([stu class]));
  NSLog(@"stu实际分配的内存%zd",malloc_size((const void *)stu));
//    2018-07-18 00:30:28.035 LJMahjong-mobile[41409:3330831] Student实例大小--> 32
//    2018-07-18 00:30:28.035 LJMahjong-mobile[41409:3330831] stu实际分配的内存32
  • 再来看看一个普通的类的实例,并且实例有且仅有唯一的成员变量(如Student只有一个name属性)
@interface Student: NSObject

@property (nonatomic, copy) NSString *name;
//@property (nonatomic, assign) NSInteger age;
//@property (nonatomic, assign) BOOL male;

@end;

@implementation  Student

@end;


  Student *stu = [Student new];
  stu.name = @"Rephontil.ZHou";
  //  stu.age = 25;
  //  stu.male = YES;
  NSLog(@"Student实例大小--> %zd",class_getInstanceSize([stu class]));
  NSLog(@"stu实际分配的内存%zd",malloc_size((const void *)stu));
//    2018-07-18 00:50:18.106 LJMahjong-mobile[41842:3456140] Student实例大小--> 16
//    2018-07-18 00:50:18.107 LJMahjong-mobile[41842:3456140] stu实际分配的内存16

由以上三次次结果的不同可推测:一个OC对象所占用的内存取决于这个对象成员变量的多少。但是同时,系统为其分配内存时,默认会分配最少16个字节的大小。

有人对指针的长度不清楚,对不同位数系统的机器总是记忆混淆☹️,还有些人说系统就是那么规定的,一句话就能囊括宇宙!其实有些事情真的有很清楚的理由让你搞明白,比如你看看我的解释:
在64bit系统上,指针占用8个字节。
在32bit系统上,指针占用4个字节。
这个很好理解,指针说白了也就是内存的地址,在64bit计算机上,内存地址显然是由64位“0”或“1”这样的二进制数组成的,因为1byte = 8bit,所以64位计算机,地址内存地址为8byte,32位计算机内存地址也就是4byte。

关于“在绝大多数情况下:OC对象所占用内存大小与对象所拥有的属性或成员变量的大小之和不相等”这一事实,可以参考内存对齐方面的知识。
关于什么是内存对齐❓以及内存对齐对CPU效率的影响❓可以参考这篇文章内存对齐

在iOS里面,创建一个OC对象,系统分配的内存大小都是16byte(字节)的倍数,最大为256byte(字节)。具体为什么这样分配,参考多方资料得出的结论就是:iOS系统下,这种内存分配方式可以使CPU工作效率最高。

OC对象的分类

OC对象分为3类 :
1 (instance对象)实例对象 ;
2 (class对象)类对象 ;
3(meta-class对象)元类对象。

instance对象就是通过alloc出来的,一个类可以通过alloc init得到多个instance对象。instance对象在内存中存储的信息包括:该实例对象的成员变量(包括isa指针),不包含方法。
所有的实例对象里面都有isa指针。因为几乎所有的OC对象都继承自NSObject,所以包含isa指针。

相比较类的instance对象,每个类的类对象和元类对象都只有一个。
类对象存放的内容包含:
isa指针;
superClass指针;
这个类的对象方法(也就是"-"开头的实例方法);
类的属性信息和成员变量信息(NSString、int、float…);
类的协议等信息。
元类对象存放的信息包含:isa指针 、superclass指针以及类的类方法(“+”方法)。

A.对一个类或一个类的实例对象执行class方法、或对一个类的实例对象调用object_getClass()方法,都可以得到这个类的类对象;
如:
1、Class objectClass1 = [NSObject class];
2、NSObject *object = [[NSObject alloc] init];
Class objectClass2 = [object class];
3、 Class objectClass2 = object_getClass(object);
1 与 2都可以获取NSObject的类对象,且同一个类的类对象地址相同(仅有一份)。
B.对“类对象”执行object_getClass方法,就可以得到这个类的元类对象。
Class metaClass = object_getClass(objectClass2);

isa指针

关于isa指针指向关系.png

instance(实例)对象的isa指向class(类对象)。当调用对象方法时,通过instance的isa指针找到class,最后找到对象方法的实现进行调用。
class(类对象)的isa指向meta-class(元类)。当调用类方法时。通过class的isa指针找到meta-class,最后找到类方法的实现进行调用。

isa与superclass葵花宝典.jpg

这幅图我在初学iOS的时候就看到过,那时候头脑中还没有isa的概念☹️,此图几乎每一本iOS进阶书籍必备。通过MJ大神的网络视频,本人对instance、class、meta-class三者之间的isa与superclass关系有了更近一部的理解。

图中左上角一虚一实两箭头表明:该关系网中,所有虚线箭头指向都是isa指针指向的关系,实线箭头指向都是superclass指针指向的关系。
因此对于拥有isa变量的对象(instance、class、meta-class):
-instance对象的isa指向class
-class对象的isa指向meta-class
-meta-class对象的isa指向基类(Root class)的meta-class

对于拥有superclass变量的对象(class、meta-class):
-class对象的superclass指针指向父类的class对象。如果父类不存在,则superclass指针指向nil。
-meta-class对象的superclass指针指向父类的meta-class。基类的meta-class的superclass指向基类的class对象。(说人话就是:基类的元类对象的superclass指向基类的类对象!!!很多人会说瓦日吧😁)。

补充一点:从64bit系统开始,isa指针需要进行一次位运算才能算出其指向的对象的实际地址。(也就是说:之前讲的instance的isa值与instance的class对象的地址并不一定相等,instance的isa需要进行一次位运算才可以得到class的地址)


isa实际指向的地址与class、meta-class地址不一定相等.png

通过上面对“isa与superclass葵花宝典.jpg!
”这幅图中设计的逻辑关系的讲解,因此理解起来子类调用父类的方法就很简单了。对于子类调用父类的实例方法与类方法:
1、调用实例方法;
isa找到class,方法不存在,就通过class的superclass找到其父类的类对象,查看父类的class(类对象)里面是否有实例方法。一层层向上。
2、调用类方法。
isa找到meta-class,如果没有类方法,就通过meta-class的superclass指针找到父类的meta-class对象,查看父类的meta-class里面是否有对应的类方法。一层层向上。

奖励🎁:苹果objc源码开源下载地址,点进去会看到一堆objc4-208.tar.gz格式的文件,-208表示序号。苹果官方也在不断地更新这套源码,序号越大表示源码越新,下载查看的时候课留意一下。

图片发自简书App