【iOS】—— 面向对象,Runtime,ARC等问题总结

对于暑假学习大多数是对之前学习的一个复习,在这里只做对之前学习欠缺知识的补充以及这些知识点涉及的一些问题,从问题入手学习。

面向对象

1.一个NSObject对象占多少内存?

结论:一个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的倍数就好。

2.对象的isa指针指向哪里?

来看这张图,基本就能懂了。
在这里插入图片描述
用自己的话解释一遍就是isa从实例走向该类再往父类指指到NSObject的时候,就指向类的元类,当指到最上面一层的时候,就指向自己。

3.OC的类信息存放在哪里

先说结论:

  • 1、对象方法、属性、成员变量、协议信息,存放在class对象中。
  • 2、类方法,存放在meta-class对象中。
  • 3、成员变量的具体值,存放在instance对象。

一、instance对象(实例对象)

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对象在内存中存储的信息包括:

  • isa指针
  • 其他成员变量

二、class对象(类对象)

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对象在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的属性信息(@property)
  • 类的对象方法信息(instance method)
  • 类的协议信息(protocol)
  • 类的成员变量信息(ivar)

三、meta-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对象的内存结构是一样的,但是用途不一样。

在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)

扩展

1、Class objc_getClass(const char *aClassName)

  • 传入字符串类名
  • 返回对应的类对象

2、Class object_getClass(id obj)

  • 传入的obj可能是instance对象、class对象、meta-class对象
  • 返回值
    • a) 如果是instance对象,返回class对象
    • b) 如果是class对象,返回meta-class对象
    • c) 如果是meta-class对象,返回NSObject(基类)的meta-class对象

3、- (Class)class、+ (Class)class

  • 返回的就是类对象

结论

OC的类信息存放在哪里?

  • 1、对象方法、属性、成员变量、协议信息,存放在class对象中。
  • 2、类方法,存放在meta-class对象中。
  • 3、成员变量的具体值,存放在instance对象。

Runtime

1.讲一下OC的消息机制

OC对象调用方法在编译阶段不知道具体的方法在哪里,是在运行的过程中,向对象发送消息,通过对象得到函数地址,调用函数,如果没有找到,则抛出异常。
OC中方法调用,其实都是转成了objc_msgSend函数的调用, 给receiver 【方法调用者】 发送了一条消息 【selector 方法名】。

objc_msgSend 底层有3大阶段:

  • OC 消息发送
  • 动态方法解析
  • 消息转发

每个对象都有一个指向所属类的指针isa。通过该指针,对象可以找到它所属的类,也就找到了其全部父类,如下图所示:请添加图片描述

当向一个对象发送消息时,objc_msgSend方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatchtable)中查找selector。如果无法找到selector,objc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatchtable)中查找selector,以此类推直到NSObject类。一旦查找到selector,objc_msgSend方法根据调度表的内存地址调用该实现。
通过这种方式,message与方法的真正实现在执行阶段才绑定。

2.消息转发机制流程

消息转发机制大致可分为三个步骤:

  • 动态方法解析
  • 备援接收者
  • 完整消息转发
    请添加图片描述

3.什么是runtime,平时项目中有用到过吗?

  • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
  • OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动 态性相关的函数
  • 平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用

  • 利用关联对象(AssociatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  • 交换方法实现(交换系统的方法)
  • 利用消息转发机制解决方法找不到的异常问题

4.[self class]和[super class]

在研究这个问题之前,我们先来打印这两个结果看看(多打印一个):

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。

ARC在编译和运行时做了什么

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这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,会执行一个特殊函数。

我应该如何看待 ARC ?它将 retains/releases 调用的代码放在哪了?

对于如何看待ARC,以下是一些观点:

  • 简化内存管理:ARC使开发人员无需手动管理内存,减少了内存泄漏和野指针等常见错误的风险。这使得开发过程更加简单和高效。
  • 自动释放对象:ARC会自动在对象不再被使用时释放它们,从而减少了手动调用release方法的需要。这样可以减少代码中的冗余,并提高开发速度。
  • 引用循环问题:尽管ARC可以自动处理大部分内存管理任务,但它并不能解决所有情况下的引用循环问题。在存在强引用循环的情况下,需要使用弱引用或无主引用来打破循环。

关于ARC将retains和releases调用的代码放在哪里,这是由编译器自动生成的。在ARC中,编译器会根据代码的语义和上下文,在适当的位置插入引用计数操作。这些操作通常是隐藏的,不需要开发人员显式地编写或管理。

block 是如何在 ARC 中工作的?

在ARC下,编译器会根据情况自动将栈上的block复制到堆上,比如block作为函数返回值时,这样你就不必再调用Block Copy。
需要注意的一件事是,在ARC下,NSString * __block myString这样写的话,block会对NSString对象强引用,而不是造成悬垂指针问题。如果你要和MRC保持一致,请使用__block NSString * __unsafe_unretained myString或(更好的是)使用__block NSString * __weak myString

ARC 速度上慢吗?

不。编译器有效地消除了许多无关的retain/release调用,并且已经投入了大量精力来加速 Objective-C 运行时。特别的是,当方法的调用者是ARC代码时,常见的 “return a retain/autoreleased object” 模式要快很多,并且实际上并不将对象放入自动释放池中。

dealloc方法的执行流程

在这里插入图片描述
对象的引用计数为0时会执行dealloc函数,调用栈如下:
dealloc->_objc_rootDealloc->object_dispose->objc_destructInstance
objc_destructInstance函数内部会依次调用c++析构函数object_cxxDestruct、关联对象析构函数_object_remove_assocations、弱引用析构函数clearDeallocating

为什么KVO和通知要显式调用dealloc

在Objective-C中,通知(Notifications)和键值观察(Key-Value Observing,KVO)都涉及到对象之间的观察和通信。当一个对象注册为通知的观察者或者添加了KVO观察者时,它需要在适当的时候取消观察,以避免潜在的内存泄漏。

显式调用dealloc方法是一种常见的方式来取消观察。当一个对象被释放时,它的dealloc方法会被调用,这是一个对象生命周期结束的时机。在dealloc方法中,你可以取消对通知的观察或者移除KVO观察者。

以下是关于为什么要显式调用dealloc的一些原因:

  • 内存管理:通过取消观察,可以确保不再持有已释放对象的引用,从而避免内存泄漏。如果没有正确地取消观察,观察者对象可能会继续存在,并且仍然保持对已释放对象的引用,导致内存泄漏。
  • 避免崩溃:如果一个已释放的对象仍然是通知的观察者或者KVO观察者,并且通知或者KVO事件发生时尝试访问该对象,就会导致崩溃。通过在dealloc方法中取消观察,可以避免这种情况发生。

需要注意的是,在ARC(Automatic Reference Counting)环境下,dealloc方法会被自动插入并处理内存管理。因此,你不需要手动调用dealloc来释放对象。但是,你仍然需要在适当的时候取消观察和移除KVO观察者,以确保正确的内存管理和避免潜在的问题。

猜你喜欢

转载自blog.csdn.net/m0_62386635/article/details/131730611