iOS底层结构分析总结

497 阅读10分钟

1,OC的本质是什么? 我们平时编写的OC代码,底层实现都是基于C\C++代码,所以OC的面向对象都是基于C\C++的数据结构实现的。

2,一个NSObject对象占用多少内存? 系统分配了16个字节给NSObject对象 (通过malloc_size函数获得),在arm64结构中,一个isa指针地址只占用8个字节的内存空间,之所以系统会多分配内存,是因为系统分配内存最小值是16个字节,且都是16的倍数,假如对象中有三个int类型的属性,一个int类型占4个字节,此时对象占用的内存是最大成员变量(isa指针)的倍数24个字节,并不是20个字节,并且系统分配的是32个字节

3,alloc方法的作用是什么? 类对象在初始化过程中,先计算好对象需要的内存空间,然后在堆中开辟内存空间

4,OC对象的几种类型 1,instance对象(实例对象),class对象(类对象),meta-class对象(元类对象),class类对象, 可以通过alloc创建出多个instance对象,但是多个实例对象所对应的类对象确实独一份的 2,class对象在内存中存储的信息主要包括:isa指针,superclass指针,属性信息(@property),对象方法信息(instance method),协议信息(protocol),成员变量信息 3,每个类在内存中有且只有一个meta-class对象,meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括:isa指针,superclass指针,类的类方法信息(class method)

5,对象的isa指针指向哪里? instance的isa指向class,当调用对象方法时, 通过instance的isa找到class, 最后找到对象方法的实现进行调用,class的isa指向meta-class,当调用类方法时, 通过class的isa找到meta-class, 最后找到类方法的实现进行调用,meta-class的isa指向基类的meta-class

6,KVO的本质是什么? 利用RuntimeAPI动态生成一个子类, 并且让instance对象的isa指向这个全新的子类,当修改instance对象的属性时, 会调用Fundation的_NSSet*ValueAndNotify函数,内部会触发监听器Observer的监听方法(observeValueForKeyPath:ofObject:change:context:),注意直接修改对象的成员变量, 而不调用set方法, 将不会触发观察者方法,通过调用willChangeValueForKey:和didChangeValueForKey:方法, 就可以手动的调用KVO

7,KVC的本质是什么? KVC的全称是Key-Value-Coding, 俗称"键值编码", 可以通过一个key来访问某个属性 使用KVC给一个对象赋值时, 会有以下方法和属性的调用顺序 查看setKey:方法是否存在, 如果存在直接调用, 如果不存在进入下一步 查看_setKey:方法是否存在, 如果存在直接调用, 如果不存在进入下一步 调用成员变量:_key, _isKey, key, isKey 总结: 使用KVO给属性或成员变量赋值时, 都会触发KVO, 系统会自动调用willChangeValueForKey:和didChangeValueForKey:两个方法

8,Category的了解 Category中的对象方法和类方法在编译后, 在底层是以结构体的形式存在, 而不是并入到Person的类对象和元类对象中,在程序运行的时候, runtime会将Category的数据, 合并到类信息中(类对象、元类对象中)当Category中的方法名与类定义的方法名相同时, 会有怎样的效果?此时, Category的方法就放在了方法列表中的前面, 而类中的原有方法则存在于方法列表的最后边,并不是说分类中的方法覆盖掉原有类中的方法,如果在调用类的方法, 就会从方法列表中从前往后查询, 而如果Category中有相同的方法, 那么就会直接使用Category中的方法

9,Category和Class Extension的区别是什么? Class Extension在编译的时候, 他的数据就已经包含在类信息中 Category是在运行时, 才会将数据合并到类信息中

10,load方法 Category中有load方法, load方法会在runtime加载类的时候调用,类的load方法调用早于Category中的load方法, 调用子类的load方法之前, 会先调用父类的load方法,没有关系的类会根据编译顺序调用load方法, Category会根据编译顺序调用load方法,所有的类和分类, load方法只会调用一次,一般情况下不会主动调用load方法, 都是让系统自动调用

11,initialize方法 当一个类在第一次接受消息alloc时, 会调用他自己的+(void)initialize方法, 如果他有父类, 那么就会优先调用父类的+(void)initialize方法

12,load和initialize方法 load是根据函数地址自动调用,initialize是通过objc_msgSend调用 load是runtime加载类、分类的时候调用(只会调用一次),initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次) 先调用类的load, 在调用分类的load,先编译的类, 优先调用load, 调用子类的load之前, 会先调用父类的load,先编译的分类, 优先调用load 先初始化父类, 后初始化子类,通过消息机制调用, 当子类没有initialize方法时, 会调用父类的initialize方法, 所以父类的initialize方法会调用多次

13,block的底层结构 block的本质就是封装了函数调用以及函数调用环境的OC对象,在OC中变量的类型主要使用三种, 分别是auto、static、全局变量, 其中auto和static修饰的是局部变量,对这三种类型的变量, block在使用使用时, 会有不同的捕获方式,在block中, 如果使用局部变量, 那么block就会捕获该变量值,再block调用之后改变,block里面的也不再改变,如果使用全局变量, 那block调用时, 直接使用全局变量, 所以全局变量的值改变, block中使用的值也会相应改变,经过static修饰的局部变量,那么block就会捕获该变量指针地址,block中使用的值同样会相应改变

14,block的类型 MRC下类型: 内部没有使用auto类型变量的block, 就是__NSGlobalBlock__类型 内部使用了auto类型变量的block, 就是__NSStackBlock__类型 __NSStackBlock__类型的block调用copy后就是__NSMallocBlock__类型, 通过copy, 将block从栈区复制到了堆区 ARC下类型: 将__NSStackBlock__类型的block赋值给__strong指针时, 会将block复制到堆区 block作为GCD API的方法参数时, block在堆区 block作为Cocoa API中方法名含有usingBlock的方法参数时, block在堆区,比如数组的遍历方法

15,Block的内存分析 MRC下类型: 栈中的block不会将对象类型的auto变量进行retain处理, 只有在将block复制到堆上时, 才会将对象类型的auto变量进行retain处理(引用计数+1),当堆中的block释放时, 会对其中的对象类型的auto变量进行release处理(引用计数-1), 如果此时对象类型的auto变量的引用计数为零, 就会被释放 ARC下类型: 当block被复制到堆上时,会调用__main_block_copy_0函数, 来对捕获的对象类型的auto变量进行强引用,当block从堆上移除时, 又会被调用__main_block_dispose_0函数, 对捕获的对象类型的auto变量解除强引用 ARC下, 程序提供了__weak关键字, 用来修饰对象类型的auto变量,当block捕获到的对象类型的auto变量被__weak修饰时, 即便block被复制到了堆上, __main_block_copy_0方法也不会对被捕获的对象类型的auto变量进行强引用 总结: 不论在ARC还是MRC下,栈中的block不会对捕获到的对象类型auto变量进行强引用(引用计数+1), 只会在copy到堆中时, 会对对象类型auto变量进行强引用,ARC下, 被__weak修饰的对象类型auto变量, 在block复制到堆中时不会进行强引用

16 __block的使用 static修饰的变量, 在block内可以修改变量的值是因为, 在底层block捕获的是age的地址, 而不是age存储的数据,全局变量可以直接在block中修改值,block不会捕获全局变量, 而是直接使用, 所以可以直接改值,但是普通的局部变量是不能修改值的,使用__block可以解决block内部无法修改auto变量值的问题,本质是经过__block修饰的变量底层被包装成一个指针类型

17,OC消息发送机制 OC中调用方法, 会使用objc_msgSend函数给消息接收者发送消息,所以OC调用方法的过程也被称为消息机制,当消息接收者为空时, 直接返回, 结束objc_msgSend函数的调用,当消息接收者有值时, 查看缓存,如果方法没有被缓存过, 就会查询方法列表

18,动态方法解析与消息转发 在objc_msgSend中, 一共可以分为三个阶段: 消息发送、动态方法解析、消息转发,当消息发送过程中,没有找到要调用的方法时, 就会进入动态方法解析阶段, 当动态方法解析也没有找到需要调用的方法实现时, 就会进入消息转发阶段, 调用_objc_msgForward_impcache函数,定时器中就使用了中间类NSProxy消息转发来解决定时间不释放的内存泄漏问题,最新的定时器block方法是不会造成强引用,是需要使用weak就能解决

19,super 从实际代码中可以看到, 这两个成员变量分别传入了self和[Person class],所以消息接收者是self, 从[Person class]中开始查找方法,所以[super run];在底层代码的原意是: 给self发送一条消息, 消息名称是run, 这条消息从[Person class]开始查找,super的含义是, 查询方法的起点是父类, 不是本身的类对象,消息接收者是self, 不是父类对象,发送的消息是调用的方法

20,@dynamic @dynamic的作用就是: 告诉编译器不要生成setter和getter方法, 同时不要生成_age成员变量, 等到运行时再添加方法实现

21,Runloop基本认识 顾名思义, Runloop就是运行循环, 在程序运行过程中循环做一些事情,当没有任何消息发生的时候, 程序处于睡眠状态等待消息发生,当消息产生后, 就会处理消息, 接着继续睡眠等待, 程序并不会马上退出,而是保持运行状态

22,多线程的安全隐患 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题 解决多线程问题,采用锁机制,比如自旋锁,互斥锁,递归锁

23,原子锁 atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,原子锁只能保证setter和getter内部的区域是安全的, 但是外部使用的时候就没办法保证