iOS 底层探索篇 ——OC底层面试解析

1. 关联对象在那里移除

关联对象在objc_removeAssociatedObjects方法里面的_object_remove_assocations移除的,关联对象是跟着对象一起走的,对象的生命周期就决定了关联对象的生命周期。

搜索一下在那里调用objc_removeAssociatedObjects发现没有调用,
在搜索一下_object_remove_assocations,发现在objc_destructInstance里面。
在这里插入图片描述
搜索objc_destructInstance的调用,发现在object_dispose里面。
在这里插入图片描述

搜索object_dispose的调用,发现在rootDealloc里面。
在这里插入图片描述
搜索rootDealloc的调用,发现在_objc_rootDealloc里面。
在这里插入图片描述
搜索_objc_rootDealloc的调用,发现在dealloc里面。
在这里插入图片描述
而当我们对象释放时,会调用dealloc

dealloc做了什么:

  • C++函数释放 :objc_cxxDestruct
  • 移除关联属性:_object_remove_assocations
  • 将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this);
  • 引用计数处理:table.refcnts.erase(this)
  • 销毁对象:free(obj)

2. Load方法在哪里调用

load方法在dyld里面调用的。
load方法的调用在load_images时会分别收集分类loadable_classes表和loadable_category表里面,然后统一递归分别调用call_class_loadscall_categoty_loads来调用load方法。
在这里插入图片描述

3. 方法的调用顺序

如果同名方法是普通方法,包括initialize – 先调用分类方法

  • 因为分类的方法是在类realize之后 attach进去的,插在类的方法的前面,所以优先调用分类的方法(注意:不是分类覆盖主类!!)
  • initialize方法什么时候调用? initialize方法也是主动调用,即第一次消息时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize中

如果同名方法是load方法

  • 先 主类load
  • 后分类load(分类之间,看编译的顺序)

c++ 方法

  • 写在objc源码里的先与类load,在objc_init 里面的static_init里面调用
  • 没有写在objc源码里的后与类load

4. Runtime是什么

runtime 是由CC++ 汇编 实现的一套API,为OC语言加入了面向对象,运行时的功能

运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时 -
举个例子🌰 extension - category 的区别

平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代 码,RuntimeObject-C 的幕后工作者

5. 方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?

方法的本质:发送消息 , 消息会有以下几个流程
1:快速查找(objc_msgSend)~ cache_t 缓存消息
2:慢速查找~ 递归自己| 父类 ~ lookUpImpOrForward
3:查找不到消息: 动态方法解析 ~ resolveInstanceMethod
4:消息快速转发~ forwardingTargetForSelector
5:消息慢速转发~ methodSignatureForSelector & forwardInvocation

sel方法编号 ~ 在read_images期间就编译进入了内存
imp 就是我们函数实现指针 ,找imp 就是找函数的过程
sel 就相当于书本的目录 tittle
imp 就是书本的⻚码 查找具体的函数就是想看这本书里面具体篇章的内容 1:我们首先知道想看什么 ~ tittle (sel)
2:根据目录对应的⻚码 (imp)
3:翻到具体的内容

6. 能否向编译后的得到的类中增加实例变量? 能否向运行时创建的类中添加实例变量

1:不能向编译后的得到的类中增加实例变量
2:只要内没有注册到内存还是可以添加
3: 可以添加属性 + 方法
原因:我们编译好的实例变量存储的位置在ro,一旦编译完成,内存结构就完全确定 就无法修改

7. [self class]和[super class]的区别以及原理分析

[self class]就是发送消息 objc_msgSend,消息接收者是self,方法编号 class

[super class] 本质就是objc_msgSendSuper,消息的接收者还是self,方法编号 class,在运行时,底层调用的是_objc_msgSendSuper2

只是 objc_msgSendSuper 会更快直接跳过self的查找

这里运行后会输出什么呢?
在这里插入图片描述

这里看到,两者都输出了LGTeacher
在这里插入图片描述
[self class]中,我们知道方法调用底层都是objc_msg_send,其中objc_msg_send有两个隐藏参数,而这里的self是隐藏参数receiver,class则是隐藏参数SEL
在这里插入图片描述

查看 class 的实现,发现其在NSObject,那么class方法在LGTeacher里面找不到,就会来到NSObject找到这个方法并实现。
在这里插入图片描述
看到这里获取对象的isa,当前的对象是LGTeacher对象,其isa指向LGTeacher类,所以[self class]打印的是LGTeacher。

在这里插入图片描述
在[super class]中,super是一个关键字而不是参数名,可以看到这里没有super。而且底层调用的是objc_msgSendSuper
在这里插入图片描述
这里看到第一个参数是个__rw_objc_super结构体,结合上图可以知道object是self,而superClass是LGTeacher的父类也就是LGPerson。而根据注释可以得知,LGPerson就是第一个去搜索的类。
在这里插入图片描述
在这里插入图片描述

其实super底层调用的是objc_msgSendSuper2,运行程序打下断点并进入汇编模式。发现确实调用的是objc_msgSendSuper2。
在这里插入图片描述

所以 [self class] 和 [super class] 的消息接受者是一样的,只是[self class ]是先到本类的方法列表中寻找,而 [super class] 则是跳过本类的方法列表,先去父类的方法列表里面寻找,而这里LGTeacher 和 LGPerson 都没有实现 class 方法,所以调用的都是NSObject的 class 方法。 而 class 方法里传的参数 self 都是 LGTeacher 的实例对象,所以打印的都是LGTeacher。
在这里插入图片描述

8. 内存平移问题

正常来说,方法的调用都是上面的形式,那么下面这样的方式,能否成功调用方法呢?

在这里插入图片描述

运行一下,发现是可以的。
在这里插入图片描述
想了解为什么下面可以调用,就得先了解为什么person可以调用方法。
方法存在类的cache里面的,对象通过isa找到类的首地址,然后在通过内存平移,平移到类中的cache,然后进行方法的查找。
在这里插入图片描述
而这里的kc的值也指向LGPerson类的首地址,而objc_msg_send不管传进来的是什么,只要指向的是LGPerson类的首地址,那么就可以在LGPerson类的cache里面查找方法。

那么如果在saySomething里面打印属性会发生什么呢?

在这里插入图片描述
运行一下,发现person打印的是null,因为没有赋值,而kc则是打印了person。
在这里插入图片描述
这是为啥呢?person是一个对象,它开辟了内存空间,里面放了isa和成员变量。而kc只是一个指针地址,没有内存空间。
person访问kc_name的时候,是经过内存平移访问的,也就是self + kc_name的offset(偏移量)来找到kc_name的指针地址,然后获取里面的值。
在这里插入图片描述

那么kc没有内存,怎么获取到kc_name 呢?当前,person是首地址向下平移8位来找到kc_name的。
在这里插入图片描述
而kc则也模仿person,进行了向下平移8位,也就是 cls + 0x8. kc是一个指针,是存在栈中的,栈是一个先进后出的结构,那么就看上一个对象是什么就会输出什么了。我们知道程序里的上一个对象是person,所以就会输出person了。在看一下cls 和 person的地址,果然差了8。
在这里插入图片描述
那么如果在kc_name之前在多加一个属性,会不会是向下平移0x10位呢?
添加一个属性后运行,发现输出的是一个ViewController对象。

在这里插入图片描述

压栈

参数和结构体会存在压栈的情况。
参数会压入栈,其地址是递减的,而栈是从高地址->低地址 分配的,即在栈中,参数会从前往后一直压。
运行后证明一下,发现确实是这样的。
在这里插入图片描述
添加一个结构体。
在这里插入图片描述
在viewDidLoad多创建一个结构体和LGPerson对象。
在这里插入图片描述
然后运行,输出三个对象的地址。
在这里插入图片描述
然后在输出结构体的两个成员的地址。
在这里插入图片描述
由输出结构可以看出,结构体是这样的。结构体内部的压栈情况是 低地址->高地址递增的,栈中结构体内部的成员是反向压入栈,即低地址->高地址,是递增的。
在这里插入图片描述

通过这段代码打印下栈的存储来验证。
在这里插入图片描述
运行一下。
在这里插入图片描述

  • 0x7ffee8b3f188:person
  • 0x7ffee8b3f190 : <LGPerson: 0x7ffee8b3f198>:kc
  • 0x7ffee8b3f198 : LGPerson:cls
  • 0x7ffee8b3f1a0 : <ViewController: 0x7fd113d093a0>:self
  • 0x7ffee8b3f1a8 : ViewController:superclass
  • 0x7ffee8b3f1b0 : viewDidLoad:cmd
  • 0x7ffee8b3f1b8 : <ViewController: 0x7fd113d093a0>: self

所以到目前为止,栈中从高地址到低地址的顺序的:self - _cmd - (id)class_getSuperclass(objc_getClass(“ViewController”)) - self - cls - kc - person

这里结构体会压栈进来是因为msgSendSuper会生成一个objc_super的结构体。

那么如果在kc_name 添加一个8字节属性,那么是不是会打印ViewController呢?实验一下,发现确实如此。
在这里插入图片描述

哪些东西在栈里 哪些在堆里

  • alloc的对象 都在堆中
  • 指针、对象 在栈中,例如person指向的空间在堆中,person所在的空间在栈中
  • 临时变量在栈中
  • 属性值 在堆,属性随对象是在栈中

注意:

  • 堆是从小到大,即低地址->高地址
  • 栈是从大到小,即从高地址->低地址分配
    – 函数隐藏参数会从前往后一直压,即 从高地址->低地址 开始入栈,
    – 结构体内部的成员是从低地址->高地址
  • 一般情况下,内存地址有如下规则
    – 0x60 开头表示在 堆中
    – 0x70 开头的地址表示在 栈中
    – 0x10 开头的地址表示在全局区域中
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值