iOS 底层 -- alloc与init
[TOC]
1、 什么是runtime
runtime 是C、C++、汇编实现的一套API,目的是为 OC增加运行时功能
2、 关于alloc与init到底在底层做了什么
看以下打印信息
JEObject *obj = [JEObject alloc];
JEObject *obj1 = [obj init];
JEObject *obj2 = [obj init];
NSLog(@"\n obj - %p\n obj1 - %p\n obj2 - -%p",obj,obj1,obj2);
打印结果为:
obj - 0x600003218320
obj1 - 0x600003218320
obj2 - -0x600003218320
结果很奇怪,三个对象的地址都完全一样。既然很奇怪,那就 来看看源码,直接在xcode中是看不到源码的,我们需要在opensource中去下载涉及到的源码 ,在MacOS中的最新版本(当前是10.14.5)
opensource_macos.png点开之后 搜索objc 得到2个搜索结果 下载objc4-xxxx(后面的代表版本号,会随着版本的更新而变化) objc4.png
这里有一个问题,在opensource中,有如此多的源码,如何知道 要下载objc的这份源码呢,这里提供2种方式
方法一、汇编:
将断点打在 JEObject *obj = [JEObject alloc];
这一行
菜单栏 -> Debug -> Debug workflow -> Always show disassembly 勾选这个选项(也就是开启汇编模式)
程序运行起来,进入断点的时候出现这样的代码:(不同版本的Xcode,内容会稍有区别)
汇编模式 Xcode 11.0
在这里我们大致可以猜一下 明显的双引号中间的红色文字,应该表示的意思就是方法名,当然在这里,这些可以不去理会。
这里需要重点关注callq
(更老版本的Xcode 这里是 bl
)
callq
表示的是调用了方法 。在当前断点下面最近的那个callq那一行打上断点 (也就是 objc_msgSend 或者 objc_alloc),继续走,来到断点时, 按住control
键, 注意看框起来的部分 框起来的那个地方发生了变化
继续点击 step into 按钮 就会出现我们需要的内容
最终结果
这样说就拿到了
方法二: 符号断点法
添加 符号断点(可能很多人只添加过全局断点,还不知道符号断点是啥😂)
步骤一:在需要探究的方法上打断点
在需要的地方打断点
步骤二:当程序运行到此断点的时候 添加符号断点
符号断点添加成功之后(可能会小卡一下)让断点直接走(下一曲图标的那个)就出现了我们需要的内容 需要的结果
有了这个结果,就可以去opensource搜索源码了。
方法三、偶然发现,不知道是否正确
进入API的系统声明,就是按住command
->jump to definition
,
箭头所指即为所需要的结果,拿这个信息去opensource搜索
⚠️:这个地方如果是文件夹,是有源码可找的,如果是framework 那么就是没有源码可看的
2.1 alloc的底层实现
打开源码文件,搜索 alloc {
(系统的方法 一般都是在方法最后加一个空格,然后才跟上大括号)进去之后 查看具体的实现,其经过了2次调用 alloc -> _objc_rootAlloc -> callAlloc
在callAlloc中得到了返回实际的对象。
2.2 init的底层实现
同样的方法, 搜索 init {
,进去之后 只调用了一个方法 init -> _objc_rootInit
在 rootInit方法中 只做了一件事情, 就是 retrun obj。
到了这里 就知道了 为何上面 alloc之后 2次init 打印的地址都是一样的了。
2.3 new的底层实现
既然都已经进来了,那我们就来验证另外一个问题 new
,我们经常说 new方法 其实就相当于是 alloc init,那么我们进入源码看看他们之间究竟是否有差别,
同样的方法, 搜索 new {
进入之后 查看实现 里面只有一次方法的实现
在new方法里面就是调用alloc的实现(callAlloc)后 进行了init操作,由此可见,[Class new] 完全等价于 [[Class alloc] init]
问题来了
上面说,init没有任何操作, 由此引出另外一个问题:既然init没有做任何操作,为何还要有init这个步骤?
要想回答这个问题,先想想,init是在什么时候会用到?
重写方法的时候! 在初始化一个类的时候,如果这个类的参数需要默认的值, 一般我们都会选择在 init方法中初始化这个值(比如封装一个倒计时按钮,一般都会在init中初始化 time = 60)。
父类不实现,交给子类根据需求去实现,
3 LLVM(编译器)
1、 补充 sieof()知识
sizeof() 是获取类型的大小
32位系统 | 64位系统 | |
---|---|---|
BOOL | 1 | 1 |
char | 1 | 1 |
short | 2 | 2 |
float | 4 | 4 |
CGFloat | 8 | 8 |
int | 4 | 4 |
long | 4 | 8 |
double | 8 | 8 |
long long | 8 | 8 |
void *(指针) |
4 | 8 |
结构体指针(struct */Class) |
4 | 8 |
结构体 | 最大属性内存的倍数 详细算法参考 | 同32位 |
结构体的sizeof() 长度 用
补、偏、长
的方式 是一个很好理解和计算的方法
- 举例1---简单型结构体:
struct JEObject_IMPL {
NSString *x; // 补0 偏0 长8
int a; // 补0 偏8 长12
char y; // 补0 偏12 长13
char t; // 补0 偏13 长14 14不是8(最长是NSString * 的8位)的倍数 内存对齐为16
};
- 举例2---复合型结构体:
struct JETemp_IMPL {
NSString *a; // 补0 偏0 长8
int b; // 补0 偏8 长12 12不是8的倍数 内存对齐为16
};
struct JEObject_IMPL {
struct JETemp_IMPL x; // 补0 偏0 长16
char y; // 补0 偏16 长17
char t; // 补0 偏17 长18
CGSize z; // 补6 偏24 长40 40是8(最长是JETemp_IMPL中NSString * 的8位)的倍数
// 这里为什么是补6 ? 因为CGSize是结构体,里面最长位是8 按照8的倍数来补齐
};
如果使用 sizeof(obj) 得到的实际只是 指针的大小 也就是 4/8
想要知道实际上obj的内存,继续下面的问题讲解
2、 malloc_size() ——sizeof() —— class_getInstanceSize()
定义这样一个对象JEObject
@interface JEObject : NSObject {
@public
int _age;
int _height;
NSString *_name;
}
@end
JEObject *obj = [JEObject alloc];
NSLog(@"%zu",class_getInstanceSize([obj class]));
NSLog(@"%lu",sizeof(obj));
注意和sizeof()的区别
问题:创建一个JEObject
- 对象内存占多少?
- 系统为其分配了多少内存
这里涉及到对象的本质,
NSobject的本质就是一个包含一个指针(这个指针是指向结构体的指针)的结构体
可以通过一个命令将OC代码转成C
cd 到待转换文件的路径
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc JEObject.m -o out.cpp
//命令解释:最后的 JEObject.m 是待转换文件的文件名, out.cpp 是输出文件的文件名
// 最后得到一个 out.cpp 的文件
在out.cpp中可以搜索JEObject_IMPL就能看到其具体结构
//NSObject
struct NSObject_IMPL {
Class isa;
// typedef struct objc_class *Class; ---> 指向结构体的指针
};
struct JEObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
NSString *_name;
};
有3个函数是获取大小的,但是具体表示的意义不一样
malloc_size()
Returns size of given ptr
系统为其分配的内存大小
sizeof()
获取对象所占内存大小
这是一个运算符 而不是方法,在编译的时候就是一个确定的数据
class_getInstanceSize()
Returns the size of instances of a class
获取类的实例对象的成员变量所占用的大小
alloc的关键步骤:
alloc关键步骤
其中 instanceSize()方法的代码
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes; // 8位对齐
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
内存对齐的算法:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
// 相当于 (x + 7) >> 3 << 3;
/* 8字节对具体算法 ,同理可得 16字节对其
详细步骤
(8 + 7) & ~7
二字节方式表述:
15: 0000 1111
7: 0000 0111
~7: 1111 1000 (取反)
~7 & 15 0000 1000 (相同为1 否则为0)
最后得到的就是 0000 1000 也就是8
‘|’ 运算 有1得1 否则为0
*/
}
3、类所占内存的大小
对于类所占用的内存要分情况来看,上面提到过,类的本质是结构体,那么类所占用的内存大小 就是这个结构体的内存大小,编译之后具体是怎样的结构体,要看具体是用什么方式声明的属性
1、 内部类的方式
@interface JEObject ()
{
int _age;
NSString *_name;
int _height;
}
@end
⬆️对于这种方式,其转换为C代码之后(前面有提到OC转C的命令)变量的顺序不变,在最前面加上了isa指针,所以用补、偏、长
的方法来计算这个JEObject的 class_getInstanceSize()
为32 malloc_size()
为32
2、用@property的方式声明属性
@interface JEObject ()
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int height;
@end
⬆️对于这种方式,其转换为C代码之后(前面有提到OC转C的命令)变量的顺序发生了变化(最前面的加上isa指针之后,<以最优的内存方式?> 、< 以单个属性从小到大的方式?>),所以用补、偏、长
的方法来计算这个JEObject的 class_getInstanceSize()
为24 malloc_size()
为32
具体转换的结果如下⬇️:
struct JEObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
NSString *_name;
};
3、既有内部类、又有修饰符、还有.m声明
JEObject.h
@interface JEObject : NSObject
{
NSString *_strb;
NSString *_stra;
NSString *_stringc;
}
@property (nonatomic, strong) NSString *b;
@property (nonatomic, assign) NSString *a;
@property (nonatomic, assign) char c;
@end
JEObject.m
@interface JEObject ()
{
NSInteger intIn;
}
@property (nonatomic, assign) char cIn;
@end
@implementation JEObject
@end
⬆️对于这种方式,其转换为C代码之后变量的顺序发生了变化(最前面的加上isa指针之后,.h中的内部类中的属性先排在前面,再排.m中的内部类属性,再将.h、.m 中的修饰符属性按照最优的方式排列
具体转换的结果如下⬇️:
struct JEObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_strb;
NSString *_stra;
NSString *_stringc;
NSInteger intIn;
char _c;
char _cIn;
NSString * _Nonnull _b;
NSString * _Nonnull _a;
};
//class_getInstanceSize
//malloc_size 用法
// class_getInstanceSize(Class _Nullable cls) 参数是Class
size_t size = class_getInstanceSize([p class]);
//malloc_size(const void *ptr); 参数是指针 不过需要桥接 可以根据报错信息 自动Fix
size_t t1 = malloc_size((__bridge const void *)(p));
总结:
malloc_size()
与class_getInstanceSize()
区别
文字理解:
malloc_size() 系统创建时 系统为对象分配了多少内存,
class_getInstanceSize() 对象实际利用了多少内存
代码层次的理解:
malloc_size () 可以认为是在 class_getInstanceSize() 之后 进行了一次16位内存对齐