对于暑假学习大多数是对之前学习的一个复习,在这里只做对之前学习欠缺知识的补充以及这些知识点涉及的一些问题,从问题入手学习。
结论:一个NSObject对象占16个字节内存
我们来打印一下看看:
NSLog(@"实际占用内存——%zu, 实际分配内存——%zu", class_getInstanceSize([NSObject class]), malloc_size((__bridge const void *)obj));
为什么实际占用和实际分配有区别呢?
我们来细看看,其实在之前学runtime的时候那篇博客其实已经能解释这个问题了,我们围绕这个问题再来细说说。
首先来看为什么第一个是8:
很简单,下面这张图就可以看懂:
就一句话,因为isa指针的大小是8,所以其实一个NSObject实际占了8。
接着说,那为什么实际又分配了16呢?
这是在alloc时调的方法,里面默认设置如果内存小于16,直接默认内存为16。
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
最少是16,如果超过了16,返回的值和调用getinstanceSIze返回的是一致的,这里计算好后需要调用calloc全部初始化0,而calloc会进行内存对齐,对齐为16的倍数,所以malloc_size返回16的倍数是正确的值。
我们再来继承一个别的类看看:
@interface Person : NSObject
@property (nonatomic, assign) int a;
@property (nonatomic, assign) int b;
@property (nonatomic, assign) int c;
@end
NSLog(@"实际占用内存——%zu, 实际分配内存——%zu", class_getInstanceSize([Person class]), malloc_size((__bridge const void *)p));
每个内存是4,总共12,加上8,就是20,然后围绕8进行内存对齐即可。这个32就还是上面alloc那个方法,对齐到16的倍数就好。
来看这张图,基本就能懂了。
用自己的话解释一遍就是isa从实例走向该类再往父类指指到NSObject的时候,就指向类的元类,当指到最上面一层的时候,就指向自己。
先说结论:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
NSLog(@"%p %p", object1, object2);
}
return 0;
}
instance
对象在内存中存储的信息包括:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = object_getClass(object1);
Class objectClass4 = object_getClass(object2);
Class objectClass5 = [NSObject class];
NSLog(@"%p %p", object1, object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);
}
return 0;
}
输出结果:
objectClass1 ~ objectClass5都是NSObject的class对象(类对象)
它们是同一个对象。每个类在内存中有且只有一个class对象。
class
对象在内存中存储的信息主要包括:
NSObject *object1 = [[NSObject alloc] init];
Class objectClass1 = [object1 class];
Class objectMetaClass1 = object_getClass(objectClass1);
NSObject *object2 = [[NSObject alloc] init];
Class objectClass2 = [object2 class];
Class objectMetaClass2 = object_getClass(objectClass2);
NSLog(@"%p %p", objectMetaClass1, objectMetaClass2);
objectMetaClass是NSObject的meta-class对象(元类对象)
每个类在内存中有且只有一个meta-class对象
注意:meta-class对象和class对象的内存结构是一样的,但是用途不一样。
在内存中存储的信息主要包括:
1、Class objc_getClass(const char *aClassName)
2、Class object_getClass(id obj)
3、- (Class)class、+ (Class)class
OC的类信息存放在哪里?
OC对象调用方法在编译阶段不知道具体的方法在哪里,是在运行的过程中,向对象发送消息,通过对象得到函数地址,调用函数,如果没有找到,则抛出异常。
OC中方法调用,其实都是转成了objc_msgSend函数的调用, 给receiver 【方法调用者】 发送了一条消息 【selector 方法名】。
objc_msgSend 底层有3大阶段:
每个对象都有一个指向所属类的指针isa。通过该指针,对象可以找到它所属的类,也就找到了其全部父类,如下图所示:
当向一个对象发送消息时,objc_msgSend方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatchtable)中查找selector。如果无法找到selector,objc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatchtable)中查找selector,以此类推直到NSObject类。一旦查找到selector,objc_msgSend方法根据调度表的内存地址调用该实现。
通过这种方式,message与方法的真正实现在执行阶段才绑定。
消息转发机制大致可分为三个步骤:
在研究这个问题之前,我们先来打印这两个结果看看(多打印一个):
NSLog(@"%@ %@ %@", [self class], [super class], [self superclass]);
输出结果:
其实结果和我想象的有些不一样,我们来研究一下为什么:
简单来说,self和super都是指向当前实例的,不同的是,[self class]会在当前类的方法列表中去找class这个方法,[super class]会直接开始在当前类的父类中去找calss这个方法,两者在找不到的时候,都会继续向祖先类查询class方法,最终到NSObject类。那么问题来了,由于我们在Person中都没有去重写class这个方法,最终自然都会去执行NSObject中的class方法,结果也自然应该是一样的。至于为什么是Person,我们可以看看NSObject中class的实现:
-(Class)class {
return object_getClass(self);
}
这就说的通了,返回的都是self的类型,self此处正好就是Person,因此结果就会输出Person。
chat:
在编译期,ARC会根据代码的语法和规则进行静态分析,确定每个对象的生命周期,并在适当的位置插入retain、release和autorelease等内存管理方法的调用。这样,在编译后的代码中,就会自动包含了正确的内存管理操作。
在运行期,ARC会跟踪对象的引用计数,并在对象不再被使用时自动释放其内存。当一个对象的引用计数减为0时,ARC会自动调用dealloc方法来释放对象占用的内存,并且会自动处理对象之间的循环引用问题。
需要注意的是,ARC只负责管理Objective-C对象的内存,对于Core Foundation框架中的C类型对象(如CFArrayRef、CFStringRef等),仍然需要手动管理内存。
总结起来,ARC在编译期通过静态分析插入合适的内存管理代码,而在运行期跟踪对象的引用计数并自动释放内存,从而简化了开发者对内存管理的工作。
除了会自动调用“保留”与“释放”方法外,使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。
在编译期,ARC会把能够相互抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行了多次“保留”与“释放”操作,那么ARC有时可以成对的移除这两个操作。ARC会分析对象的生存期需求,并在编译时自动插入适当的内存管理方法调用的代码,而不需要你记住何时使用retain、release、autorelease方法。编译器还会为你生成合适的dealloc方法。
将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化。比如,ARC可以在运行期检测到autorelease后面跟随retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,会执行一个特殊函数。
retains/releases
调用的代码放在哪了?对于如何看待ARC,以下是一些观点:
关于ARC将retains和releases调用的代码放在哪里,这是由编译器自动生成的。在ARC中,编译器会根据代码的语义和上下文,在适当的位置插入引用计数操作。这些操作通常是隐藏的,不需要开发人员显式地编写或管理。
在ARC下,编译器会根据情况自动将栈上的block复制到堆上,比如block作为函数返回值时,这样你就不必再调用Block Copy。
需要注意的一件事是,在ARC下,NSString * __block myString
这样写的话,block会对NSString对象强引用,而不是造成悬垂指针问题。如果你要和MRC保持一致,请使用__block NSString * __unsafe_unretained myString
或(更好的是)使用__block NSString * __weak myString
。
不。编译器有效地消除了许多无关的retain/release
调用,并且已经投入了大量精力来加速 Objective-C 运行时。特别的是,当方法的调用者是ARC代码时,常见的 “return a retain/autoreleased object
” 模式要快很多,并且实际上并不将对象放入自动释放池中。
对象的引用计数为0时会执行dealloc函数,调用栈如下:
dealloc->_objc_rootDealloc->object_dispose->objc_destructInstance
objc_destructInstance
函数内部会依次调用c++析构函数object_cxxDestruct
、关联对象析构函数_object_remove_assocations
、弱引用析构函数clearDeallocating
在Objective-C中,通知(Notifications
)和键值观察(Key-Value Observing,KVO
)都涉及到对象之间的观察和通信。当一个对象注册为通知的观察者或者添加了KVO观察者时,它需要在适当的时候取消观察,以避免潜在的内存泄漏。
显式调用dealloc
方法是一种常见的方式来取消观察。当一个对象被释放时,它的dealloc方法会被调用,这是一个对象生命周期结束的时机。在dealloc
方法中,你可以取消对通知的观察或者移除KVO观察者。
以下是关于为什么要显式调用dealloc
的一些原因:
需要注意的是,在ARC(Automatic Reference Counting
)环境下,dealloc
方法会被自动插入并处理内存管理。因此,你不需要手动调用dealloc来释放对象。但是,你仍然需要在适当的时候取消观察和移除KVO观察者,以确保正确的内存管理和避免潜在的问题。