【OC总结 面向对象 + 内存管理 + runtime】

文章目录

  • 前言
  • 面向对象
    • 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 vtableclass_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方法尝试让本类的其他对象去执行这个函数(快速消息转发)
  • 完整的消息转发:如果没有进行快速转发,则调用methodSignatureForSeletorforwardInvocation方法进行完整的消息转发和替换方法。

1.6 什么是runtime?

runtime-运行时,是iOS系统的核心,它的本质是一套底层的C语言API。runtime将一些工作放在代码运行的时候才处理而非编译时 为 Objective-C 语言的动态属性提供支持,所以很多的类和成员在我们编译的时候是不知道的,在运行时,所编写的代码会转换成完整的确定的代码运行。
Apple - Objective-C 运行时

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。 Objective-C 中所有方法的调用/类的生成都在运行时进行

runtime运行时交互

在OC中运行时系统有三个层次的交互:

  1. 通过OC源码:我们编写OC代码,Runtime系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行的时候确定对应的调用那个方法。
  2. 通过Foundation的NSObject定义的方法
  3. 通过直接调用运行时的函数。
    在这里插入图片描述

runtime的实际应用

  1. 利用关联对象(AssociatedObject)给分类添加属性
  2. 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  3. 交换方法实现(交换系统的方法)swizzling
  4. 动态的添加方法:这个我也没用过,不过理解了消息转发的整个流程,就能够理解为什么这样行得通。

Objective-C的一道题:[self class] 与 [super class]

  • 下面代码输出什么?
    请添加图片描述

son 和 Father?
请添加图片描述

self和super的区别:

  1. self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。
  2. super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的法。
  3. 在调用[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的使用是为了解决对象retainrelease匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。

1.7 ARC和MRC的实现

MRC:手动的通过retain去为对象获取内存,并用release释放内存的操作称为MRC (Manual Reference Counting)。

MRC

内存管理的思考方式

  1. 自己生成的对象自己持有:alloc new copy mutableCopy创建并持有对象。
  2. 非自己生成的对象自己也可以持有:retain
//取得的对象存在但不持有
id obj = [NSMutableArray array];
//持有该对象
[obj retain];
  1. 不需要自己持有的对象就将其释放 release
//自己持有对象
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似可以访问,但对象一经释放绝对不可访问
[obj release];
  1. 非自己持有的对象自己无法释放
 //非自己持有的对象无法释放,crashid 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);
  1. 检查输入的 obj 地址 和指针指向的地址是否相同。
  2. 持有对象,引用计数 + 1 。
  3. 指针指向 obj。
  4. 原来指向的对象引用计数 - 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,未溢出的时候为0uintptr_t extra_rc          : 19;  //->存储引用计数

在这里插入图片描述

对于retain的实现

  1. 最简单的判断是不是taggedPointer,是就直接返回。
  2. 不是这开始判断是否支持Nonpointer isa。
  3. 不支持:去sidetable取出计数信息 执行加一操作。直接sidetable_retain,这是由于计数都存储在sidetable中了,处理逻辑较支持Nonpointer isa的情况要简单一些。
  4. 支持优化指针:
  • 先判断是否为 其一定支持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流程总结:

  1. 依旧判断是否为taggedPointer,是就返回false,不需要就dealloc。
  2. 判断是否有优化,没有操作散列表,引用计数 + 1;
  3. 引用计数是否为0,为0执行dealloc流程。
  4. 若isa有优化,则对象的isa位存储的引用计数减一,判断是否向下溢出, 如果是,如果到-1 就放弃newisa改为old,并将散列表中一半引用计数取出来,然后将这一半引用计数减一在存到isa的extra_rc。
  5. 如果sidetable的引用计数为0,对象进行dealloc流程
  6. 和retain的区别就是引用计数减一
    在这里插入图片描述

retainCount

对象的引用计数存储分为两种情况

  • 如果对象的 isa 是非指针的话(优化),引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和。
  • 对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中。

retainCount的过程

  1. 当对象的isa经过优化,首先获取isa位域extra_rc中的引用计数,默认会+1(防止你没持有就要打印)然后获取散列表的引用计数表中的引用计数,两者相加得到对象的最终的引用计数
  2. 当对象的isa没有经过优化,则直接获取散列表的引用计数表中的引用计数,返回。
  3. 对象在初始化的时候引用计数默认为1 是编译在底层决定防止对象被释放加一的,这个1不会出现在sidetable中,也不会出现在extra_rc中,因为sidetableextra_rc当中存放的都是该对象本身之外的引用计数的数量,所以初始状态sidetableextra_rc中的值都是0,然后我们后续进行的retainrelease操作都是针对sidetableextra_rc中的引用计数进行+1或-1。

参考:iOS 从源码解析Runtime (五):聚焦objc_object(retain、release、retaincount)

1.8 ARC在编译期和运行期做了什么?

在编译期,ARC能够把相互抵消的retainreleaseautorelease操作简化,当同一个对象被执行了多次保留和释放操作的时候,ARC有时可以成对的移除这两个操作,ARC会分析对象的生存期需求并在编译的时候自动插入适当的内存管理方法调节代码,而不需要我们手动的使用retainreleaseautorelease方法。编译器还会为你生成合适的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。
解决办法:

  1. 使用atomic属性关键字。
  2. 加锁

而第二段代码中的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特点

  1. Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate。
  2. Tagged Pointer 指针的值不再是地址了,而是真正的值。

1.10 iOS内存对齐

参考:iOS内存对齐原理
获取内存大小的方法

  1. sizeof:其作用就是返回一个对象或者类型所占的内存字节数。
  2. class_getInstaceSize: 是runtime提供的api,用于获取类的实例对象所占用的內存大小 ,并返回具体的字节数,其本质就是获取实例对象中成员变量的內存大小
  3. 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字节放得下)。

內存对齐的原因

  1. 效能提升
  • 未对齐的內存,处理器需要作两次內存访问;而对齐的內存访问仅需要一次访问。最重要的是提高內存系統的性能。
  1. 对应各家平台
  • 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则拋出硬件异常。

结构体内存对齐规则

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#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
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/6269.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【指针和数组笔试题(1)】详解指针、数组笔试题

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言整型数组字符数组第一组题第二组题第三组题 总结 前言 在计算之前要了解基本概念&#xff1a; 数组名的理解 数组名是数组首元素的地址 有两个例外 1.sizeof(…

Linux网络基础 — 数据链路层

目录 数据链路层 认识以太网 局域网转发的原理 认识以太网的MAC报头 以太网帧格式 认识MAC地址 对比理解MAC地址和IP地址 基于MAC帧协议再次谈一谈局域网转发的原理 认识MTU MTU对IP协议的影响 MTU对UDP协议的影响 MTU对于TCP协议的影响 ARP协议 ARP协议的作用 …

Xcode 15 beta 4 (15A5195m) - Apple 平台 IDE

Xcode 15 beta 4 (15A5195m) - Apple 平台 IDE IDE for iOS/iPadOS/macOS/watchOS/tvOS/visonOS 请访问原文链接&#xff1a;https://sysin.org/blog/apple-xcode-15/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org visonOS …

SpringBoot整合SpringCloudStream3.1+版本的Kafka死信队列

SpringBoot整合SpringCloudStream3.1版本的Kafka死信队列 上一篇直通车 SpringBoot整合SpringCloudStream3.1版本Kafka 实现死信队列步骤 添加死信队列配置文件&#xff0c;添加对应channel通道绑定配置对应的channel位置添加重试配置 结果 配置文件 Kafka基本配置&#…

C++ deque/queue/stack的底层原理

deque容器的存储结构 和 vector 容器采用连续的线性空间不同&#xff0c;deque 容器存储数据的空间是由一段一段等长的连续空间构成&#xff0c;各段空间之间并不一定是连续的&#xff0c;可以位于在内存的不同区域。 deque采用一块所谓的map数组&#xff08;注意&#xff0c…

rabbitmq模块启动报java.net.SocketException: socket closed的解决方法

问题 最近在接手一个项目时&#xff0c;使用的是spring-cloud微服务构架&#xff0c;mq消息消费模块是单独一个模块&#xff0c;但启动这个模块一直报如下错误&#xff1a; java.net.SocketException: socket closed 这个错误是这个模块注册不到nacos报的错&#xff0c;刚开…

day34-Animated Countdown(动画倒计时)

50 天学习 50 个项目 - HTMLCSS and JavaScript day34-Animated Countdown&#xff08;动画倒计时&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport&q…

态势标绘专题介绍

介绍 这个专栏是专门针对基于Cesium来实现态势标绘的专题专栏,专栏主要实现了30余种态势几何形状的标绘和编辑、文本的标绘和编辑、图片的标绘和编辑以及简单模型的标绘,同时支持标绘结果的导出以及导入。包括最终编写成的一个完整的Vue3.2+TS+Cesium1.107.2的标绘组件。专栏…

从用户的角度谈GPT时代技术突破的两大关键逻辑

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

EtherCAT转TCP/IP网关EtherCAT解决方案

你是否曾经为生产管理系统的数据互联互通问题烦恼过&#xff1f;曾经因为协议不同导致通讯问题而感到困惑&#xff1f;现在&#xff0c;我们迎来了突破性的进展&#xff01; 介绍捷米特JM-TCPIP-ECT&#xff0c;一款自主研发的Ethercat从站功能的通讯网关。它能够连接到Etherc…

通过FPGA实现基于RS232串口的指令发送并控制显示器中目标位置

目录 1.算法理论概述 串口通信模块 指令解析模块 位置控制模块 显示器驱动模块 2.部分核心程序 3.算法运行软件版本 4.算法运行效果图预览 5.算法完整程序工程 1.算法理论概述 通过FPGA实现基于RS232串口的指令发送并控制显示器中目标位置是一种常见的应用场景&#x…

Prompt 技巧指南-让 ChatGPT 回答更准确

随着 ChatGPT 等大型语言模型 (LLM)的兴起&#xff0c;人们慢慢发现&#xff0c;怎么样向 LLM 提问、以什么技巧提问&#xff0c;是获得更加准确的回答的关键&#xff0c;也由此产生了提示工程这个全新的领域。 提示工程(prompt engineering)是一门相对较新的领域&#xff0c;用…

java学习003

Java数组 Java 语言中提供的数组是用来存储固定大小的同类型元素&#xff0c;这一点和PHP语言的可变数组长度不同。 声明变量数组 首先必须声明数组变量&#xff0c;才能在程序中使用数组。下面是声明数组变量的语法&#xff1a; dataType[] arrayRefVar; // 首选的方法 或 …

云计算——云计算与虚拟化的关系

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​ 目录 前言 一.虚拟化 1.什么是虚拟化 2.虚拟化技术作用 二.云计算与虚拟化的关系 三.虚…

华为eNSP:ospf的配置

一、拓扑图 二、路由器的配置 1、路由器依据规划配置接口IP AR1: <Huawei>system-view [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 10.10.10.1 24 [Huawei-GigabitEthernet0/0/0]qu AR2: <Huawei>system-view [Huawei]int g0/0/0 [Huawei-Gi…

RabbitMQ消息可靠性问题及解决

说明&#xff1a;在RabbitMQ消息传递过程中&#xff0c;有以下问题&#xff1a; 消息没发到交换机 消息没发到队列 MQ宕机&#xff0c;消息在队列中丢失 消息者接收到消息后&#xff0c;未能正常消费&#xff08;程序报错&#xff09;&#xff0c;此时消息已在队列中移除 …

STM32(HAL库)驱动AD8232心率传感器

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 ADC外设配置 2.3 串口外设配置 2.4 GPIO配置 2.5 项目生成 3、KEIL端程序整合 3.1 串口重映射 3.2 ADC数据采集 3.3 主函数代码整合 4 硬件连接 5 效果展示 1、简介 本文通过STM32…

Linux文件处理命令

目录&#xff1a; linux系统与shell环境准备linux常用命令之文件处理Linux系统登录与文件操作 1.linux系统与shell环境准备 Linux 系统简介&#xff1a; Linux 内核最初只是由芬兰人林纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;在赫尔辛基大学上学时出于个人爱好而…

分布式光伏并网防孤岛保护装置AM5SE-IS

分布式光伏并网防孤岛保护装置AM5SE-IS 应用场景 防孤岛原理&#xff1a;防孤岛保护装置检测到并网点有逆功率、频率突变、 等异常数据时&#xff0c;即发生孤岛现象时&#xff0c;装置可配合断路器快速切除并网点&#xff0c;使本站与电网侧快速脱离&#xff0c;保证整个电站…

blender 纹理材质

添加材质纹理需要哪五个节点&#xff1f; 映射节点&#xff1a;调整纹理的位置、大小、缩放&#xff1b; 纹理坐标&#xff1a;怎么映射&#xff0c;以什么方式去映射这张图&#xff0c;换句话说就是如何将 2D 的图片映射到 3D 的图像上&#xff1b;纹理坐标就是以什么坐标方式…