iOS-底层原理17-类的扩展补充和面试题

《iOS底层原理文章汇总》

上一篇文章《iOS-底层原理16-类扩展和关联对象底层原理》介绍了关联对象底层原理

1.分析AssociationsManager不唯一和AssociationsHashMap唯一

模拟新建AssociationsManager和AssociationsHashMap两个,查看内存地址是否一致,先将AssociationsManager构造函数中的锁去掉,否则会死锁

不同的AssociationsManager manager和manager1创建的associations和associations1内存地址一样,唯一


iOS-底层原理17-类的扩展补充和面试题_第1张图片
image.png

AssociationsManager构造函数去掉锁


iOS-底层原理17-类的扩展补充和面试题_第2张图片
image.png

manager不唯一,manager内存地址不可读,调用初始化方法,并不会调用init方法,那么init方法在什么时候调用的呢?断点调试下

    AssociationsManager()   {  }
    ~AssociationsManager()  {  }
iOS-底层原理17-类的扩展补充和面试题_第3张图片
image.png

在arr_init()中调用AssociationsManager::init()方法,属于类方法对AssociationsManager进行环境准备并没有初始化,init()方法并没有返回值,map_images-->arr_init()-->_objc_associations_init()-->AssociationsManager::init()-->_mapStorage.init()


image.png

2.加锁的原因:会对唯一的表AssociationsHashMap中的数据进行读取和安放,防止多线程对数据进行篡改而导致数据不同步

iOS-底层原理17-类的扩展补充和面试题_第4张图片
image.png

相当于如下:操作前加锁,操作后解锁


iOS-底层原理17-类的扩展补充和面试题_第5张图片
image.png

3.整体结构:

AssociationsHashMap:整个项目
LGPerson LGTeacher LGStudent
对象 key -> ObjctAssociationMap (LGPerson)(name age hobby)
key -> ObjcAssociation(policy value)

Buckets桶子里面包含桶子,Buckets桶子的结构为objc::detail::DenseMapPair,结构复用,代码复用


image.png
iOS-底层原理17-类的扩展补充和面试题_第6张图片
image.png
iOS-底层原理17-类的扩展补充和面试题_第7张图片
image.png

4.面试题:请问关联对象设置后是否应该移除?不需要移除,为什么呢?

对象在释放的时候会自动移除,进入dealloc方法,- (void)dealloc-->_objc_rootDealloc(self) --> obj->rootDealloc() --> object_dispose((id)this) --> objc_destructInstance(obj) --> _object_remove_assocations(obj),从总表AssociationsHashMap中挨个移除,Bucket移除

iOS-底层原理17-类的扩展补充和面试题_第8张图片
image.png

iOS-底层原理17-类的扩展补充和面试题_第9张图片
image.png

iOS-底层原理17-类的扩展补充和面试题_第10张图片
image.png

5.面试题:主类和分类同名方法,优先调用哪一个?

    1. 非load方法会先调用分类中的
  • 2.load方法会先调用主类的,再调用分类的,为什么会是先主类后分类呢?


    iOS-底层原理17-类的扩展补充和面试题_第11张图片
    image.png

    image.png

5.load_images分析

iOS-底层原理17-类的扩展补充和面试题_第12张图片
image.png

prepare_load_methods((const headerType *)mh) --> schedule_class_load(remapClass(classlist[i])) --> add_class_to_loadable_list(cls)

在schedule_class_load方法中进行递归,将父类中的方法进行添加loadable_classes,若类有load方法,将类添加到loadable_classes中,已经开辟的和正在使用的classes_loadable类数量是否相等,若相等,则进行扩容

method = cls->getLoadMethod(),判断是否是load方法,返回imp


iOS-底层原理17-类的扩展补充和面试题_第13张图片
image.png
iOS-底层原理17-类的扩展补充和面试题_第14张图片
image.png
iOS-底层原理17-类的扩展补充和面试题_第15张图片
image.png

类中的load方法加载完成,再加载分类中的load方法,加到loadable_categories,若已经使用的分类和开辟的相等,则进行扩容

iOS-底层原理17-类的扩展补充和面试题_第16张图片
image.png
iOS-底层原理17-类的扩展补充和面试题_第17张图片
image.png

调用load方法call_load_methods(),循环先调用类中load方法,后调用分类中load方法,函数指针消息发送(*load_method)(cls, @selector(load))传入两个参数cls和@selector(load)


iOS-底层原理17-类的扩展补充和面试题_第18张图片
image.png
iOS-底层原理17-类的扩展补充和面试题_第19张图片
image.png
iOS-底层原理17-类的扩展补充和面试题_第20张图片
image.png

调用完毕后,调用自动释放池回收整片内存空间


iOS-底层原理17-类的扩展补充和面试题_第21张图片
image.png

load_images分析流程图


image.png

面试题

initialize方法在第一次消息发送的时候调用
load方法调用是先主类后分类
其他方法并不是分类覆盖了主类,而是分类中的方法编译时写在了前面,会先调用

Runtime是什么

runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象,运行时的功能
运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时
举例子:extension-category的区别
平常编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,RuntimeObject-C的幕后工作者

6.[self class]和[super class]的区别

iOS-底层原理17-类的扩展补充和面试题_第22张图片
image.png
2020-12-27 19:12:35.385708+0800 KCObjc[65535:2691177] LGTeacher - LGTeacher
2020-12-27 19:12:35.386547+0800 KCObjc[65535:2691177] 
  • 1.[self class]会调用object_getClass(self),返回对象的isa,也就是类LGTeacher
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
  • 2.super是关键字,clang LGTeacher.m查看LGTeacher的源码得到


    iOS-底层原理17-类的扩展补充和面试题_第23张图片
    image.png

[super class]编译为了如下代码,__rw_objc_super为中间形态,搜objc_msgSendSuper源码

((Class (*)(__rw_objc_super *, SEL))(void
*)objc_msgSendSuper)((__rw_objc_super){(id)self, 
(id)class_getSuperclass(objc_getClass("LGTeacher"))}, 
sel_registerName("class"))

objc_msgSendSuper结构第一个参数为结构体struct objc_super *super,结构体中有两个参数id receiver,class super_class,故源码中是self调用sel_registerName("class"),在方法instancetype _I_LGTeacher_init中的self为LGTeacher对象,[super class]相当于是[self class],和[self class]是同一套调用源码流程顺序,会调用object_getClass(self),返回对象的isa,也就是类LGTeacher


image.png

iOS-底层原理17-类的扩展补充和面试题_第24张图片
image.png

iOS-底层原理17-类的扩展补充和面试题_第25张图片
image.png

// super : 关键字
// [super class] (class)(id self , sel _cmd)
// self->isa LGTeacher
// self 消息的接受者 LGTeacher

运行程序,断点object_getClass,发现方法会进入两次,间接证明输出的是LGTeacher


image.png

iOS-底层原理17-类的扩展补充和面试题_第26张图片
image.png

3.[self class]和[super class]的区别,self再去查找class方法的时候,不先从本类中去查找了,直接从父类中去查找,跳过了本类的查找流程,比[self class]查找速度更快super_class is the first class to search,消息的接收者还是self,查找的方式变化了


iOS-底层原理17-类的扩展补充和面试题_第27张图片
image.png

假设将[super class]改为[LGPerson class]呢,查看源码


iOS-底层原理17-类的扩展补充和面试题_第28张图片
image.png

由上面可知编译时期[super class]调用的方法为objc_msgSendSuper,那么运行时期呢?实质上调用的方法为objc_msgSendSuper2


image.png
image.png

将当前类传入结构体struct objc_super中,在结构体内部再取当前类的父类,而不是现在就把当前类的父类传进去,结构体内部objc_super中会从类的父类super_class开始查找,这一点和objc_msgSendSuper不一样,消息的接收者receiver还是本类self,[super class]还是输出LGTeacher
[self class]从本类中查找class方法,[super class]从父类中开始查找class方法
查看汇编代码:是直接从superclass中查找class方法


iOS-底层原理17-类的扩展补充和面试题_第29张图片
image.png

完整回答:[self class]和[super class]两个都会输出LGTeacher,[self class]调用的本质是消息发送msgSend,通过调用class底层方法获取到对象的isa即LGPerson元类型,类已经加载到内存,获取元类类型,在map_images的readClass方法中类名已经加载到类名表中,读取%@时是一个字符串类型,打印LGTeacher,super是一个关键字,底层调用objc_msgSendSuper2,消息接收者为self,和[self class]消息接收者一模一样,返回LGTeacher

7.内存平移

- (void)viewDidLoad {
    [super viewDidLoad];
    Class cls = [LGPerson class];
    void  *kc = &cls; 
    [(__bridge id)kc saySomething];
 }
 #import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{
 NSLog(@"%s",__func__);
}

输出的结果一模一样,可以正常调用实例方法,kc调用saySomething和person对象调用saySomething指向的内存空间一致,可以调用


image.png

iOS-底层原理17-类的扩展补充和面试题_第30张图片
image.png
  • 在saySomething方法中增加获取self.kc_name,[person saySomething]打印的self.kc_name为nil,[(__bridge id)kc saySomething]打印的self.kc_name为什么呢?


    iOS-底层原理17-类的扩展补充和面试题_第31张图片
    image.png

打印出来的结果为-[LGPerson saySomething] - ,为什么打印的是ViewController???

image.png

  • 1.分析[person saySomething]调用self.kc_name的内存平移情况,在函数-(void)viewDidLoad中,栈的情况,栈是先进后出,最先压栈的是-(void)viewDidLoad方法中的隐藏的两个参数(self,_cmd),之后[super viewDidLoad]方法clang编译会生成一个结构体{(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}结构体两个参数self和super_class分别压入栈中

    iOS-底层原理17-类的扩展补充和面试题_第32张图片
    image.png

  • 2.分析栈的情况

  • 隐藏参数会压入栈帧:参数从前往后一直压栈,栈区是从高地址到低地址,person先压入栈分配的是高地址,person2和person3后压入栈,依次分配比person低的地址


    iOS-底层原理17-类的扩展补充和面试题_第33张图片
    image.png
  • 函数调用的压栈情况[super viewDidLoad]分析:查看编译源码分析能得到,会生成一个结构体{(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))},会传入结构体属性self和(id)class_getSuperclass(objc_getClass("ViewController")),那么结构体属性,是怎么一个压栈情况呢
objc_msgSendSuper({(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));
  • 结构体属性的压栈情况:结构体里面的属性num2的内存地址为0x00007ffeed838178,num1的内存地址为0x00007ffeed838170,person3的内存地址为0x00007ffeed838168,说明结构体中的属性,后面的属性先压栈,即先压入20,再压入10,因此objc_msgSendSuper方法中的结构体{(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}先入栈的是super_class,后入栈的是self,压栈顺序为self-->cmd-->(id)class_getSuperclass(objc_getClass("ViewController"))-->self


    iOS-底层原理17-类的扩展补充和面试题_第34张图片
    image.png
  • 完整压栈顺序:self-->cmd-->(id)class_getSuperclass(objc_getClass("ViewController"))-->self-->cls-->kc-->person


    iOS-底层原理17-类的扩展补充和面试题_第35张图片
    image.png
  • 理解指针和地址的概念:p是指针变量,在栈中赋值前后p和q的地址0x7ffee5373160、0x7ffee5373158不变,给指针变量p赋值&a(a的地址0x7ffee537316c),p的地址0x7ffee5373160中存放a的地址0x7ffee537316c,a的内存地址0x7ffee537316c中存放了整形数值10,q的地址0x7ffee5373158存放p的地址0x7ffee5373160,q取双重指针得到10


    iOS-底层原理17-类的扩展补充和面试题_第36张图片
    image.png

    image.png
  • 3.理解了指针和地址的概念,查看当前栈中内存地址情况,*(void *)address打印address这一个内存地址中存放的地址所
    指向的区域,比如address的内存地址中存放
    kc的内存地址,则
    (void **)address为kc的内存
    地址中所存放的对象或值的内存情况,kc的内存地址中指向的内容为cls的内存地址
    image.png

kc的内存地址中存放的内容为cls的内存地址,cls的内存地址中存放的是[LGPerson class],address的内存地址中存放的是kc的内存地址

image.png

address的内存地址
image.png

image.png

self为消息接收者 - LGPerson
从person对象中找到唯一的属性kc_name,需要将person对象内存地址平移一个isa指针8字节的位置,获取kc_name的值,0x7ffee119e178平移8字节得0x7ffee119e180,0x7ffee119e178+0x8 = 0x7ffee119e180正好是ViewController的内存地址 (0x7ffee119e180 - ),故会输出 -[LGPerson saySomething] -
//LGPerson: 0x7ffee119e178
//person VS LGPerson(实例化)(isa)
//kc -> LGPerson (实例化) kc_name
iOS-底层原理17-类的扩展补充和面试题_第37张图片
image.png

第二个问题:为什么第三个参数super_class返回的是ViewController?(id)class_getSuperclass(objc_getClass("ViewController")),因为objc_msgSendSuper2返回的是当前的类ViewController,为什么不是ViewController的父类UIViewController呢???

[super viewDidLoad]方法,运行时经过汇编走的代码是objc_msgSendSuper2,在进入汇编之前要传入两个参数,一个是结构体指针struct objc_super * _Nonnull super,一个是SEL _Nonnull op为sel_registerName("viewDidLoad"),查看结构体struct objc_super中存在两个参数一个是receiver,一个是super_class,消息的接收者为self本类,super_class为多少呢???

iOS-底层原理17-类的扩展补充和面试题_第38张图片
image.png

查看objc_msgSendSuper2的解释,super_class传入的是当前类ViewController并不是当前类的父类UIViewController,在汇编中去查找方法viewDidLoad时才去查找当前类的父类,若传入的是当前类的父类UIViewController,则在汇编中查找的是父类的父类UIResponer,
所以第三项打印为当前类(本类)ViewControler,objc_msgSendSuper2的汇编代码实质为 objc_msgSendSuper2({self, objc_getClass("ViewController")}, sel_registerName("viewDidLoad"));
image.png

iOS-底层原理17-类的扩展补充和面试题_第39张图片
image.png

改一下LGPerson中的属性结构,结果变化如何:增加一个属性,则平移isa + kc_hobby总共16字节,0x7ffee7808178 + 0x8 + 0x8 = 0x7ffee7808188

为self ——> ViewController


iOS-底层原理17-类的扩展补充和面试题_第40张图片
image.png

再改下LGPerson的属性结构

iOS-底层原理17-类的扩展补充和面试题_第41张图片
image.png

iOS-底层原理17-类的扩展补充和面试题_第42张图片
image.png

此时person对象平移isa(8字节)再平移int类型(4字节),即将内存地址 0x7ffee5d98180 - 劈开一半,读取出来为数值1463859280,不是一段完整的数据,程序没有崩溃

此面试题的一次,外层传一个cls,无论是什么cls,都可以用kc接收,更加实现多态化,但是不安全,对象属性变化,内存访问不到,会崩溃

image.png

kc为嘛能调用LGPerson中的对象方法

Class cls = [LGPerson class];
void *kc = &cls;//ISA
LGPerson *person = [LGPerson alloc];// person - 指针 - ISA -> LGPerson

person指针地址里面有ISA,ISA指向LGPerson,cls的首地址是ISA,kc相当于ISA,故能调用类中的方法

alloc出来的内存空间都在堆里面,常量,指针在栈中,0x70一般代表栈,0x60一般代表堆

你可能感兴趣的:(iOS-底层原理17-类的扩展补充和面试题)