文章目录
- 前言
- 面向对象
- 1.1 一个NSObject对象占用多少内存?
- 1.2 iOS的继承链 & 对象的指针指向了哪里?
- 1.3 OC的类的信息存放在哪里?-isa指针
- 1.4 isMemberOfClass & isKindOfClass
- Runtime
- 1.4 讲一下OC的消息机制
- 1.5 消息转发机制流程
- 1.6 什么是runtime?
- runtime运行时交互
- runtime的实际应用
- Objective-C的一道题:[self class] 与 [super class]
- 内存管理
- 1.6 什么是ARC?
- 1.7 ARC和MRC的实现
- MRC
- ARC 规则
- ARC实现
- **__strong**
- **objc_retain**
- **objc_release**
- retainCount
- 1.8 ARC在编译期和运行期做了什么?
- 1.9 TaggedPointer
- 总结
- TaggedPointer特点
- 1.10 iOS内存对齐
前言
面向对象 + 内存管理. + runtime总结 后续随时补充,总结为主,省略了一些源码实现
面向对象
1.1 一个NSObject对象占用多少内存?
参考:MJ-iOS底层原理总结】一个NSObject对象占用多少内存?
在编译器中查看,有两种查看Size的方法
方式一:class_getInstanceSize([NSObject class])
方式二:malloc_size((__bridge const void *)(obj))
// 一个NSObject对象占用多少内存
- (void)testObj {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"class_getInstanceSize = %zu", class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size = %zu", malloc_size((__bridge const void *)(obj)));
/*
*/
}
class_getInstanceSize([NSObject class])
的意思是:获得NSObject实例对象的成员变量所占用的大小 ,并非获取NSObject所占用的大小。
malloc_size((__bridge const void *)(obj))
的意思是:获得obj指针所指向内存的大小。
结论:所以一个NSObject
对象占用16个字节,而真正利用起来的只有8个字节。系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但NSObject
对象内部只使用了8个字节的空间
原理:OC底层在分配的时候判断一个对象至少分配6个字节,小于16字节强制分配6字节给这个对象。
1.2 iOS的继承链 & 对象的指针指向了哪里?
继承链分为类的继承链,元类的继承链和isa指针的指向三部分来学习。
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// NSLog(@"类继承探究:");
// [self testSuperClass:Stu.class];
// [self testSuperClass:Person.class];
// [self testSuperClass:NSObject.class];
//
NSLog(@"元类继承探究:");
[self testMetaClass:Stu.class];
[self testMetaClass:Person.class];
[self testMetaClass:NSObject.class];
}
// 元类继承链
/*
元类是系统自动创建的,和关联类同名。
对象的isa指向类,类对象的isa指向元类
*/
-(void)testMetaClass: (id) class {
Class cls = class;
Class metaClass = object_getClass(cls);
NSLog(@"类:%@_%p",cls,cls);
NSLog(@"元类:%@_%p",metaClass,metaClass);
[self testSuperClass:metaClass];
// 观察打印结果知道,父类的元类 = 元类的父类(根类NSObject除外)
// 根元类的父类 = 根类
}
// 继承链
- (void)testSuperClass:(id) class {
Class cls = class;
Class superClass = class_getSuperclass(cls);
Class rootSuperClass = class_getSuperclass(superClass);
NSLog(@"类:%@_%p",cls,cls);
NSLog(@"父类:%@_%p",superClass,superClass);
NSLog(@"父类:%@_%p",rootSuperClass,rootSuperClass);
NSLog(@"----------");
}
@end
元类继承链重点总结:
1. 元类是系统自动创建的,和关联类同名。
2. 对象的isa指向类,类对象的isa指向元类
3. 元类的父类 == 父类的元类(根类除外)
3. 根元类的父类 == 根类本身。
isa指针的指向总结:
- 对象的isa指向类
- 类的isa指向元类
- 元类的isa指向根元类
- 根元类的isa指向根元类
-(void) testSuperIsa:(id) obj {
Class isa = object_getClass(obj);
Class metaIsa = object_getClass(isa);
Class rootMetaIsa = object_getClass(metaIsa);
NSLog(@"对象:%@_%p",obj,obj);
NSLog(@"对象的isa-->%@_%p",isa,isa);
NSLog(@"类的isa-->%@_%p",metaIsa,metaIsa);
NSLog(@"元类isa-->%@_%p",rootMetaIsa,rootMetaIsa);
NSLog(@"----------");
}
- 下图能为上面的总结
1.3 OC的类的信息存放在哪里?-isa指针
在 Objective-C 中,每个对象都有一个 isa 指针,指向它的类对象。isa 指针实际上是一个指向一个 Class 结构体的指针,这个结构体包含了与类相关的一些信息。
- 类的名字;
- 父类的指针;
- 类的成员变量列表;
- 类的属性列表;
- 类的方法列表;
- 类的协议列表。
类对象在内存中有且仅有一个对象 主要包括 isa指针 super Class指针 类的属性信息 类的对象方法信息 类的协议信息 类的成员变量信息
类对象的isa指针有
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存 formerly cache pointer and vtable
class_data_bits_t bits; // 用于获取具体的类信息 class_rw_t * plus custom rr/alloc flagsflags
...
bits
里面存储了类的方法列表等等信息,是class_data_bits_t
类型的结构体。
我们已经知道了isa指针的指向,所以问题的总结如下:
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法存放在meta-class对象中(元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的)
- 成员变量的具体值存放在instance对象中
1.4 isMemberOfClass & isKindOfClass
参考自:iOS采坑 isKindOfClass & isMemberOfClass
从实现学习,看懂本质
类的走位图:
类方法的实现
- (void)ClassMethod {
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[Person class] isKindOfClass:[Person class]];
BOOL re4 = [(id)[Person class] isMemberOfClass:[Person class]];
NSLog(@" re1 :%hhd re2 :%hhd re3 :%hhd re4 :%hhd",re1,re2,re3,re4);
// 1 0 0 0
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
//类方法
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
实例方法的实现
- (void)instanceMethod {
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[Person alloc] isKindOfClass:[Person class]];
BOOL re8 = [(id)[Person alloc] isMemberOfClass:[Person class]];
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
// 1 1 1 1
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
Runtime
1.4 讲一下OC的消息机制
OC是一门动态语言,消息机制就是对象发送消息的时候进行的一系列过程。
OC对象调用方法在编译阶段不知道具体的方法在哪里,是在运行的过程中,向对象发送消息,通过对象得到函数地址,调用函数,如果没有找到,则抛出异常。
当向一个对象发送消息时,objc_msgSend
方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatchtable
)中查找selector
。如果无法找到selector,objc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatchtable)中查找selector,以此类推直到NSObject类。一旦查找到selector,objc_msgSend方法根据调度表的内存地址调用该实现。
通过这种方式,message与方法的真正实现在执行阶段才绑定。
OC
1.5 消息转发机制流程
OC的消息转发机制流程主要是三次拯救机制
- 动态方法解析:调用
resloveInstaanceMethod
或者resolveClassMethod
方法 ,尝试给没有实现的方法添加实现 - 备援接受者:调用
forwaardingtargetForSelector
方法尝试让本类的其他对象去执行这个函数(快速消息转发) - 完整的消息转发:如果没有进行快速转发,则调用
methodSignatureForSeletor
和forwardInvocation
方法进行完整的消息转发和替换方法。
1.6 什么是runtime?
runtime-运行时,是iOS系统的核心,它的本质是一套底层的C语言API。runtime将一些工作放在代码运行的时候才处理而非编译时 为 Objective-C 语言的动态属性提供支持,所以很多的类和成员在我们编译的时候是不知道的,在运行时,所编写的代码会转换成完整的确定的代码运行。
Apple - Objective-C 运行时
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。 Objective-C 中所有方法的调用/类的生成都在运行时进行
runtime运行时交互
在OC中运行时系统有三个层次的交互:
- 通过OC源码:我们编写OC代码,Runtime系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行的时候确定对应的调用那个方法。
- 通过Foundation的NSObject定义的方法
- 通过直接调用运行时的函数。
runtime的实际应用
- 利用关联对象(AssociatedObject)给分类添加属性
- 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
- 交换方法实现(交换系统的方法)swizzling
- 动态的添加方法:这个我也没用过,不过理解了消息转发的整个流程,就能够理解为什么这样行得通。
Objective-C的一道题:[self class] 与 [super class]
- 下面代码输出什么?
son 和 Father?
self和super的区别:
- self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。
- super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的法。
- 在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是 当前类的父类super_class。
我理解错误的原因就是 误认为[super class]
是调用的[super_class class]。
objc_msgSendSuper的工作原理应该是这样的:
- 从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,最后的调用者是objc->receiver,而不是super_class!
- 那么objc_msgSendSuper最后就转变成如下
// 注意这里是从父类开始msgSend,而不是从本类开始,
objc_msgSend(objc_super->receiver, @selector(class))
/// Specifies an instance of a class. 这是类的一个实例
__unsafe_unretained id receiver;
- (Class)class {
return object_getClass(self);
}
由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = self。self就是son,调用 class, 所以objc_msgSend(self, @selector(class))和objc_msgSendSuper(objc_super, @selector(class))传递给class这个方法的IMP的参数id都是同一个对象实例,所以最终二者的输出是相同的。
内存管理
1.6 什么是ARC?
ARC的全称是Automatic Reference Counting
, 是Objective的内存管理机制。 直接的说就是代码中加入了retain/release
,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动的由编译器完成
ARC的基本规则:只要某个对象被任意一个strong指向,那么它将不会被销毁,如果对象没有被任何strong指向,那么就将被销毁,weak类型的指针也可以指向对象但是不会持有该对象。
ARC的使用是为了解决对象retain
和release
匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。
1.7 ARC和MRC的实现
MRC:手动的通过retain去为对象获取内存,并用release释放内存的操作称为MRC (Manual Reference Counting)。
MRC
内存管理的思考方式
- 自己生成的对象自己持有
:alloc new copy mutableCopy
创建并持有对象。 - 非自己生成的对象自己也可以持有:
retain
//取得的对象存在但不持有
id obj = [NSMutableArray array];
//持有该对象
[obj retain];
- 不需要自己持有的对象就将其释放
release
//自己持有对象
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似可以访问,但对象一经释放绝对不可访问
[obj release];
- 非自己持有的对象自己无法释放
//非自己持有的对象无法释放,crash
id obj = [NSMutableArray array];
[obj release];
MRC_autorelease原理
autorelease 是Objective-C中的一种内存管理方式,它使用了自动释放池来延迟对象的释放时间
实际上只是把对象对 release 的调用延迟了,对于每一个 autorelease,系统只是把该对象放入了当前的 autorelease pool 中,且调用完 autorelease 方法后,对象的计数器不变。当该 pool 被释放时,该 pool 中的所有对象会被调用 release 方法。
注意⚠️
- 并不是放到自动释放池代码的对象都会自动释放,需要手动调用
autorelease
方法。 - 不要连续的调用
autorelease
/ 调用autorelease
之后用release
- 在MRC下对于一个对象每次调用retain方法都需要对应一次release方法,每次调用alloc、copy或者new方法都需要对应一次release方法或者autorelease方法。
ARC 规则
ARC的实现主要是所有权修饰符的学习
__Strong
_strong修饰符是id类型和对象类型默认的所有权修饰符
,ARC中不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。- 强引用对象的所有者和对象的生命周期:持有强引用的变量超出其作用域的时候被废弃,随着强引用的失效引用的对象会随之释放。我们可以理解为强引用修饰符就是持有者的转变
__strong
修饰对象可能造成对象之间的互相强引用导致循环引用。
__weak修饰符 避免循环引用
- __weak 弱引用不能持有对象实例。
__unsafe_unretained修饰符不安全的所有权修饰符,附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。并且容易出现垂悬指针。
- weak 修饰的指针变量,在指向的内存地址销毁后自动置为 nil。
- _Unsafe_Unretained 不会置为 nil,容易出现 悬垂指针,发生崩溃,
- 悬垂指针: 指针指向的内存已经被释放了,但是指针还存在 或者说 野指针。所以在使用
__unsafe_unretained
修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在,如果不存在,那么程序就会崩溃
__autoreleasing修饰符
- 自动调用:编译器会检查方法名是否以alloc / new / copy / mutableCopy开始,如果不是则自动将返回值的对象注册到autopool里面。
ARC实现
__strong
OC代码:
id __strong obj0 = [[NSObject alloc] init];
NSLog(@"%@", obj0);
内部方法
//初始化的两个方法如下:
objc_alloc_init
objc_storeStrong
//所有程序执行完之后:
objc_autoreleasePoolPop
storeStrong函数
objc_storeStrong(id *location, id obj)
{
//用prev保留被赋值对象原来所指向的对象
id prev = *location;
//如果所赋的值和被赋值对象所指的对象是同一个,就直接return不进行任何操作
if (obj == prev) {
return;
}
//如果所赋的值和被赋值对象所指的对象不是同一个
//就先objc_retain使所赋的值对象的引用计数+1(因为赋值成功之后要持有)
objc_retain(obj);
//改变被赋值对象所指向的对象为新的对象
*location = obj;
//因为prev保留了被赋值对象原来所指向的对象,所以对prev进行objc_release使原来的旧对象引用计数-1,因为现在我们的被赋值对象已经不指向它了
objc_release(prev);
}
EG:
obj = otherObj;
//会变成如下函数调用
objc_storeStrong(&obj, otherObj);
- 检查输入的 obj 地址 和指针指向的地址是否相同。
- 持有对象,引用计数 + 1 。
- 指针指向 obj。
- 原来指向的对象引用计数 - 1。
objc_retain
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
->retain()方法
objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
retain方法的流程和isa指针是否优化有关系,isa指针的优化是其结构决定的(nonpointer),其中引用计数还涉及到了(extra_c 和 has_sidetable_rc)
- 优化后的isa可以存储额外信息。
uintptr_t nonpointer : 1;//->表示使用优化的isa指针
uintptr_t has_sidetable_rc : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1,未溢出的时候为0
uintptr_t extra_rc : 19; //->存储引用计数
对于retain的实现
- 最简单的判断是不是taggedPointer,是就直接返回。
- 不是这开始判断是否支持Nonpointer isa。
- 不支持:去sidetable取出计数信息 执行加一操作。直接sidetable_retain,这是由于计数都存储在sidetable中了,处理逻辑较支持Nonpointer isa的情况要简单一些。
- 支持优化指针:
- 先判断是否为 其一定支持Nonpointer isa的架构,但是isa没有额外信息
如果没有额外信息 那就和不支持意义一样(判断是否有优化) 引用计数存储在sidetable中,走sidetable的引用计数+1的流程。 - 接着判断对象是否正在释放,如果正在释放则执行dealloc流程。
- 有存储额外信息,包含引用计数。我们尝试对isa中的extra_rc++加一进行测试
- 如果没有溢出越界的情况,我们将isa的值修改为extra_rc++之后的值
如果有溢出 将一半的计数存储到extra_rc,另一半存储到sidetable中去 设置设置标志位位true
retain过程是如何达到优化的?
- 核心在于isa是否支持存储信息,isa能够存储信息帮我们省去了去sidetable中读取计数信息,提高了效率(release相对应也可以被优化,因为retain和release是成对的出现的)。
struct SideTable {
spinlock_t slock; // 保证原子操作的自旋锁
RefcountMap refcnts; // 引用计数的 hash 表
weak_table_t weak_table; // weak 引用全局 hash 表
};
objc_release
objc_release(id obj)
{
if (obj->isTaggedPointerOrNil()) return;
return obj->release();
}
release流程总结:
- 依旧判断是否为taggedPointer,是就返回false,不需要就dealloc。
- 判断是否有优化,没有操作散列表,引用计数 + 1;
- 引用计数是否为0,为0执行dealloc流程。
- 若isa有优化,则对象的isa位存储的引用计数减一,判断是否向下溢出, 如果是,如果到-1 就放弃newisa改为old,并将散列表中一半引用计数取出来,然后将这一半引用计数减一在存到isa的extra_rc。
- 如果sidetable的引用计数为0,对象进行dealloc流程
- 和retain的区别就是引用计数减一
retainCount
对象的引用计数存储分为两种情况
- 如果对象的 isa 是非指针的话(优化),引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和。
- 对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中。
retainCount的过程
- 当对象的isa经过优化,首先获取isa位域extra_rc中的引用计数,默认会+1(防止你没持有就要打印)然后获取散列表的引用计数表中的引用计数,两者相加得到对象的最终的引用计数
- 当对象的isa没有经过优化,则直接获取散列表的引用计数表中的引用计数,返回。
- 对象在初始化的时候引用计数默认为1 是编译在底层决定防止对象被释放加一的,这个1不会出现在sidetable中,也不会出现在extra_rc中,因为
sidetable
和extra_rc
当中存放的都是该对象本身之外的引用计数的数量,所以初始状态sidetable
和extra_rc
中的值都是0,然后我们后续进行的retain
和release
操作都是针对sidetable
和extra_rc
中的引用计数进行+1或-1。
参考:iOS 从源码解析Runtime (五):聚焦objc_object(retain、release、retaincount)
1.8 ARC在编译期和运行期做了什么?
在编译期,ARC能够把相互抵消的retain
,release
,autorelease
操作简化,当同一个对象被执行了多次保留和释放操作的时候,ARC有时可以成对的移除这两个操作,ARC会分析对象的生存期需求并在编译的时候自动插入适当的内存管理方法调节代码,而不需要我们手动的使用retain
、release
、autorelease
方法。编译器还会为你生成合适的dealloc方法。
ARC可以在运行期检测到autorelease
后面跟随retain
这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,会执行一个特殊函数。
1.9 TaggedPointer
参考:iOS - 老生常谈内存管理(五):Tagged Pointer
为了节省内存和提高执行效率,苹果在64bit程序中引入了Tagged Pointer
技术,用于优化NSNumber、NSDate、NSString
等小对象的存储。
Tagged Pointer的背景
在64位机器中,一个指针占据8个字节,一个对象包含isa指针,也是8个字节。对于包含整形的NSNumber来说,还必须有8个字节存储这个整型数字。所以一个NSNumber类型的对象加上一个指针,至少会占据24个字节。
苹果为了优化对象的内存设计了Tagged Pointer,在64位的机器上,把诸如整型,char类型,或者一些长度较小的字符串,直接放入指针里面,然后在高四位和低四位加上标记位,表示当前的指针为Tagged Pointer并且指明当前的数据类型。这样就可以方便地存储和访问数据了。引入Tagged Pointer后,内存占用会减少一半以上,访问速度会提升3倍。Tagged Pointer并不是对象,它的创建和销毁过程比对象也快很多。以一个整型的NSNumber为例,不使用Tagged Pointer的情况下,至少占用24字节,而使用了Tagged Pointer后,占用的字节数为8个字节,可见,内存方便的提升还是很明显的。
Tagged Pointer支持的类型
常见的数据类型NSString,NSNumber,NSIndexPath,NSDate和UIColor支持Tagged Pointer
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSDate = 6,
OBJC_TAG_UIColor = 17,
⚠️:当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString
Tagged Pointer和对象之间的差异
在生成Tagged Pointer的过程中,实际是对指针做了位运算。
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
Tagged Pointer并不是对象,也没有isa指针,内存分配和销毁的过程和对象也不一样。
id objc_retain(id obj)
{
if (_objc_isTaggedPointerOrNil(obj)) return obj;
return obj->retain();
}
void objc_release(id obj)
{
if (_objc_isTaggedPointerOrNil(obj)) return;
return obj->release();
}
引用计数管理的时候,如果是Tagged Pointer,函数会直接return。
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
//如果是Tagged Pointer,直接返回
if (_objc_isTaggedPointerOrNil(referent)) return referent_id;
//其他代码
//...
}
在修改weak表的时候,如果被弱引用的是Tagged Pointer,这个时候Tagged Pointer不会加入到weak表里面。
TaggedPointer的特点
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghij"];
});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghi"];
});
}
代码1crash 代码二正常
分别打印两段代码的self.name类型看看,原来第一段代码中self.name为__NSCFString类型,而第二段代码中为NSTaggedPointerString类型。
__NSCFString存储在堆上,它是个正常对象,需要维护引用计数的。self.name通过setter方法为其赋值。
- (void)setName:(NSString *)name {
if(_name != name) {
[_name release];
_name = [name retain]; // or [name copy]
}
}
异步并发执行setter方法,可能就会有多条线程同时执行[_name release],连续release两次就会造成对象的过度释放,导致Crash。
解决办法:
- 使用atomic属性关键字。
- 加锁
而第二段代码中的NSString为NSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash,因为根本就没执行释放操作。
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
总结
Tagged Pointer 的引入也带来了问题,即 Tagged Pointer 并不是真正的对象,而是一个伪对象,所有对象都有isa 指针,而 Tagged Pointer 其实是没有的,因为它不是真正的对象。
TaggedPointer特点
- Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate。
- Tagged Pointer 指针的值不再是地址了,而是真正的值。
1.10 iOS内存对齐
参考:iOS内存对齐原理
获取内存大小的方法
- sizeof:其作用就是返回一个对象或者类型所占的内存字节数。
- class_getInstaceSize: 是runtime提供的api,用于获取类的实例对象所占用的內存大小 ,并返回具体的字节数,其本质就是获取实例对象中成员变量的內存大小
- malloc_size:获取系统实际分配的內存大小
结果:8 24 32
Person *p1 = [[Person alloc] init];
NSLog(@"p1对象类型占用的内存大小:%lu",sizeof(p1)); // 因为对象的本质是结构体指针,而指针占的是8个字节。
NSLog(@"p1对象实际占用的内存大小:%lu",class_getInstanceSize([p1 class])); // 为什么对象实际占用24字节,不是20吗?isa(8字节)+NSString *name(8字节)+int age(4字节)? 确实字节大小共为20字节,但是依照內存对齐原则进行了字节补齐,所以补齐到了24字节(3个8字节放得下)。
NSLog(@"p1对象实际分配的内存大小:%lu",malloc_size((__bridge const void *)(p1))); // malloc_size是系統分配的大小,以16字节对齐,大小20个字节要32字节(兩个16字节放得下)。
內存对齐的原因
- 效能提升
- 未对齐的內存,处理器需要作两次內存访问;而对齐的內存访问仅需要一次访问。最重要的是提高內存系統的性能。
- 对应各家平台
- 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则拋出硬件异常。
结构体内存对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16來改变这一系数,其中的n就是你要指定的“对齐系数”。在iOS中,Xcode默认为#pragma pack(8),即8字节对齐。
规则一
- 数据成员对齐规则:结构体或者联合体的第一个成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员的大小或者该成员的子成員的大小的整数倍开始
规则二
- 结构体作为成员:如果一个结构体A中有结构体B作为子成员,B中存放有char,int,double等元素,那么B应该从double也就是8的整数倍开始存储
规则三
- 结构体的总体大小,即sizeof的结果,必须是其內部最大成员的整数倍,不足的需要补齐.
*/
- (void)Eg1 {
struct StructA {
double a; // 8 (0-7)
char b; // 1 [8 1] (8)
int c; // 4 [9 4] 9 10 11 (12 13 14 15)
short d; // 2 [16 2] (16 17)
} strA;
struct StructB {
double a; //8 (0-7)
int b; //4 (8 9 10 11)
char c; //1 (12)
short d; //2 13 (14 15) - 16
} strB;
// 輸出
NSLog(@"strA = %lu,strB = %lu", sizeof(strA), sizeof(strB));
// strA = 24,strB = 16
}