小码哥iOS学习笔记第一天: Objective-C的本质

·  阅读 2346

一、一个NSObject对象占用多少内存?

  • 针对问题, 我们创建一个项目工程, 并创建一个NSObject对象

  • 我们平时编写的Objective-C代码, 底层实现其实都是C\C++代码

  • 所以Objective-C的面向对象都是基于C\C++的数据结构实现的

思考: Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?

结构体: Objective-C中类的属性多样, 只有结构体能承载

  • 将Objective-C代码转换为C\C++代码, 使用Mac终端进行转换, C++的文件格式是.cpp
$ clang -rewrite-objc main.m -o main.cpp

  • 使用Xcode查看main.app文件, 可以看到转换后的文件有9万多行代码, 并且我们创建NSObject对象的代码就在文件的最后面

  • 注意: 在不同平台上, 支持的代码是不一样的, 比如Windows, Mac, iOS等平台

  • 所以在转换代码的时候, 可以指定某一个平台后再转换, 终端指令如下

# 指定: iOS操作系统, arm64架构, 将 main.m 文件转为 main-arm64.cpp 文件
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

  • 使用Xcode查看main-arm64.cpp文件, 此时转换后的C++代码只有3万多行, 创建NSObject对象的代码同样在最底部

  • 此时的项目工程有10个错误, 这些错误全都是main-arm64.cpp文件带来的, 所以可以使其不参与编译, 将错误消除

  • main-arm64.cpp文件中, 找到下面的代码

  • NSObject_IMPL实际意思是NSObject Implementation, 即: NSObject类的实现

  • 我们可以使用command + 鼠标右键点击NSObject类, 进入NSObject.h文件中查看NSObject类的定义

通过这种方法, 可以从侧面证明 Objective-C 中的类, 在底层是C\C++中的结构体类型

所以NSObject对象, 在内存中就是一个结构体对象

  • 思考: 一个OC对象在内存中是如何布局的?

  • NSObject的底层实现如下:

  • 由图可知, 一个NSObject_IMPL结构体中, 只有一个成员变量Class isa, 我们使用command + 鼠标右键点击Class类型查看一下

  • 由图可知, Class类型是一个指针, 所以NSObject_IMPL结构体中只包含一个指针变量

  • arm64架构中, 一个地址占用8个字节, 所以isa指针占用8个字节的内存空间

  • 可以使用runtime中的class_getInstanceSize(Class _Nullable cls)方法, 查看一个对象中, 所有成员变量占用的内存大小

  • 那么一个NSObject对象, 到底占用多少的内存空间呢?

  • 可以使用malloc框架中的malloc_size(const void *ptr)方法查看

  • 疑问: 为什么NSObject对象在内存中只使用了8个字节存储isa指针, 却分配了16个字节的内存空间呢?

  • 官网中, 找到Objc4开源代码

  • 下载最新代码

  • 使用Xcode打开Objc4, 查找alloc方法的底层方法_objc_rootAllocWithZone()

  • 查看_objc_rootAllocWithZone()方法的实现, 可以找到class_createInstance()方法, 这个就是创建对象调用的方法

  • 继续查看class_createInstance()方法的实现, 方法中调用了_class_createInstanceFromZon()方法

  • 继续查看_class_createInstanceFromZon()方法的实现, 此时可以看到创建对象时调用calloc()函数, calloc()函数传入的第二个参数size, 就是分配内存的大小, size是通过instanceSize()方法获取的

  • 继续查看instanceSize()方法, 可以知道, size的最小是就是16

  • 这就是为什么NSObject明明只有一个指针变量, 却占用了16个字节内存的原因

一个NSObject对象占用多少内存?
系统分配了16个字节给NSObject对象 (通过malloc_size函数获得)
但NSObject对象内存只使用了8个字节的空间(64bit环境下, 可以通过class_getInstanceSize函数获得)

二、窥探NSObject的内存

  • 运行程序, 打断点, 打印obj对象, 获取obj对象的地址

  • 查看内存

  • 输入上面找到的obj地址, 查看obj对象在内存中的信息, 其中isa使用了8个字节, 剩余8个字节没有被使用

三、常用LLDB指令

1、printp: 打印值

2、po: 打印对象

3、memory read/数量格式字节数 内存地址: 读取内存, 有缩略写法: x

  • 读取内存的格式和字节数如下:
格式: 
x: 16进制       f: 浮点数       d: 十进制

字节大小:
b: byte: 1字节          h: half word: 2字节
w: word: 4字节          g: giant word: 8字节
  • 指定条件读取内存

4、memory write 内存地址 数值: 修改内存中的值

四、自定义类型的实例, 在内存中的大小

1、以自定义类Student为例

  • 定义Student类, 继承自NSObject, 并添加两个int类型的成员变量, 具体实现如下:
@interface Student: NSObject
{
    @public
    int _no;
    int _age;
}
@end
@implementation Student
// 类的实现部分
@end
  • 此时测试代码如下:

2、根据main.m文件, 获取对应的.cpp文件, 查看Student在底层的具体实现

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
  • 查看Student的底层结构体实现:

  • Student_IMPLstruct NSObject_IMPL NSObject_IVARS;是父类NSObject的实现

  • 所以Student_IMPL的实际实现如下:
struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

3、测试Student的实例, 是不是Student_IMPL类型

  • 使用struct Student_IMPL *指针, 指向Student的一个实例, 代码如下
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;

struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL*)stu;
NSLog(@"no - %d, age - %d", stuImpl->_no, stuImpl->_age);
  • 运行后, 可以打印出_no_age的值, 就是45:
// 打印结果
no - 4, age - 5
  • 这从侧面可以验证Student底层就是Student_IMPL类型

  • 也可以在下图的位置打断点, 用来查看Student在内存中的数据

  • 可以看到内存中的Student实例:

  • 前八个字节是isa, 后八个字节是_no_age, 因为一个int类型只占4个字节
  • 在结构体中, 成员变量之间的地址是连接在一起的, 所以Student的成员变量一共占用16个字节的内存
  • 由上面可知一个NSObject的实例最少分配16个字节, 所以例子中定义的Student类型的一个实例, 在内存中会占用16个字节的内存, 并且这个16个字节全部都被使用
  • 使用class_getInstanceSizemalloc_size进行测试

五、更复杂的继承结构

  • 现有如下代码: Person继承自NSObject, 有一个成员变量_age, Student继承自Person, 有一个成员变量_no
@interface Person: NSObject
{
    int _age;
}
@end
@implementation Person
@end


@interface Student: Person
{
    int _no;
}
@end
@implementation Student
@end
  • 此时底层实现如下:
struct NSObject_IMPL {
	Class isa;
};

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
};

struct Student_IMPL {
	struct Person_IMPL Person_IVARS;
	int _no;
};
  • 可以推测出:

    • Person的实例占用内存为isa + _age = 12, 小于16, 所以Person的实例占用16字节的内存
    • Student的实例占用内存为isa + _age + _no = 16, 等于16, 所以Student的实例占用16字节的内存
  • 使用代码检测:

注意:
我们知道class_getInstanceSize函数获取到的, 是类型中所有成员变量一共占用内存的大小, 所以Person获取到的成员变量应该是isa + _age = 12个字节, 但是为什么返回16字节呢?

  • 查看一下class_getInstanceSize的源码:

  • 进入alignedInstanceSize函数:

实际上class_getInstanceSize获取到的大小, 是内存对齐之后的大小, 即所有成员变量中, 占用内存最大的那个成员变量的倍数, Person内的NSObject_IMPL有一个isa占用8个字节, _age占用4个字节, 所以class_getInstanceSize获取到的是8的倍数, 即16个字节

  • 对于Person, NSObject_IMPL虽然占有16个字节, 但是只有一个成员变量isa, 所以有8个字节是空的, 所以, 会将_age放在空的位置, 而不是继续累加至20个字节

  • 对于Student, 有两个成员变量Person_IMPL_no, Person_IMPL占用16个字节, 但是有4个字节是空的, 所以分配给了_no使用, 即Student分配内存是16个字节

1、属性会生成对应的成员变量

  • 再次给Student添加一个成员属性height
@interface Student: Person
{
    int _no;
}
@property (nonatomic, assign) int height;
@end
@implementation Student
@end
  • 此时Student的底层实现为:
struct Student_IMPL {
	struct Person_IMPL Person_IVARS;
	int _no;
	int _height;
};
  • 测试Student在内存中的大小如下:

  • 此时Student中的成员变量是isa + _age + _no + _height = 20, 最大成员变量isa占用内存为8, 所以class_getInstanceSize获取到的是24个字节

注意:
OC中给实例对象分配空间时, 是按照16, 32, 48, 64, 80, 96...按照16的倍数递增的, 所以malloc_size函数获取到的Student实例内存是32

分类:
iOS
分类:
iOS
收藏成功!
已添加到「」, 点击更改