iOS底层

iOS之武功秘籍⑩: OC底层题目分析

2021-02-26  本文已影响0人  長茳

iOS之武功秘籍 文章汇总

写在前面

前面篇章说了那么多的原理,那本篇就拿说说OC相关的题目吧...

本节可能用到的秘籍Demo

一、Runtime Asssociate方法关联的对象,需要我们手动释放吗?

当我们对象释放时,会调用dealloc

所以,关联对象不需要我们手动移除,会在对象析构即dealloc时释放.

dealloc的原理详解我们将在内存管理章节详细讲解,这里先附上一张dealloc流程图

二、方法的调用顺序

类的方法 和 分类方法 重名,如和调用,是什么情况?

三、Runtime是什么?

1、category 类别、分类

2、extension 类扩展

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

方法的本质:发送消息,发送消息会有以下几个流程

SEL是方法编号,也是方法名,在dyld加载镜像到内存时,通过_read_image方法加载到内存的表中了
imp函数实现指针 ,找imp就是找函数的过程

SELIMP的关系就可以解释为:

比如我们想在《程序员的自我修养——链接、装载与库》一书中找到“动态链接”(SEL),肯定会翻到179页(IMP),179页会开始讲述具体内容(函数实现)

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

具体情况具体分析:

原因:

六、[self class]和[super class]的区别以及原理分析

代码调试
TCJStudent中的init方法中打印这两种class调用,TCJStudent继续自TCJPerson.

打印结果如下

有点出乎意料,[self class]点进去来到NSObject.mm文件查看源码

其底层是获取对象的isa,当前的对象是TCJStudent,其isa是同名的TCJStudent,所以[self class]打印的是TCJStudent

[super class]中,其中super 是语法的 关键字,可以通过clangsuper的本质,这是编译时的底层源码,其中第一个参数是消息接收者,是一个__rw_objc_super结构

底层源码中搜索__rw_objc_super,是一个中间结构体

objc4-818.2源码中搜索objc_msgSendSuper,查看其隐藏参数

搜索struct objc_super

通过clang的底层编译代码可知,当前消息的接收者 等于 self,而self 等于 TCJStudent,所以 [super class]进入class方法源码后,其中的selfinit后的实例对象,实例对象的isa指向的是本类,即消息接收者是TCJStudent本类.

我们再来看[super class]在运行时是否如上一步的底层编码所示,是objc_msgSendSuper,打开汇编调试,调试结果如下

搜索objc_msgSendSuper2,从注释得知,是从 类开始查找,而不是父类

查看objc_msgSendSuper2的汇编源码,是从superclass中的cache中查找方法

所以,最完整的回答如下

[self class]方法调用的本质是发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_imagesreadClass时已经加入表中,所以打印为TCJStudent

[super class]打印的是TCJStudent,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2,其实他的消息接收者和[self class]是一模一样的,所以返回的是TCJStudent

七、内存平移问题

① 原始题

程序能否运行?是否正常输出?


运行结果与普通初始化对象一模一样,可面试的时候不可能只说能或不能,还要说出个所以然来


[person saySomething]的本质是对象发送消息,那么当前的person是什么?

所以,person是指向TCJPerson类的结构,obj也是指向TCJPerson类的结构,然后都是在TCJPerson类中的methodList中查找方法.


② 拓展一

修改打印方法saySomething——不但打印方法,同时打印属性cj_name

重新运行代码,得到结果如下

为什么会出现打印不一致的情况?

其中person方式的cj_name是由于self指向person的内存结构,然后通过内存平移8字节,取出去cj_name,即self指针首地址平移8字节获得.

其中的cls方式中的obj指针中没有其他的,所以obj表示8字节指针,self.cj_name的获取,相当于obj首地址的指针也需要平移8字节找cj_name,那么此时的obj的指针地址是多少?平移8字节获取的是什么?

obj是一个指针,是存在中的,栈是一个先进后出的结构,参数传入就是一个不断压栈的过程,其中隐藏参数会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),可以通过clang查看底层编译,隐藏参数压栈的过程,其地址是递减的,而栈是从高地址->低地址分配的,即在栈中,参数会从前往后一直压.

super通过clang查看底层的编译,是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass),那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况

从打印结果可以得出20先加入,再加入10,因此结构体内部的压栈情况是 低地址->高地址递增的,栈中结构体内部的成员反向压入栈,即低地址->高地址是递增的.

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

那我们来打印下栈的存储情况:
obj的栈的存储情况

person的栈的存储情况

objperson一起的栈的存储情况


其中为什么class_getSuperclassViewController,因为objc_msgSendSuper2返回的是当前类,两个self,并不是同一个self而是栈的指针不同,但是指向同一片内存空间

其中 personTCJPerson的关系是 person是以TCJPerson为模板的实例化对象,即alloc有一个指针地址,指向isaisa指向TCJPerson,它们之间关联是有一个isa指向.
obj也是指向TCJPerson的关系,编译器会认为obj也是TCJPerson的一个实例化对象,即obj相当于isa,即首地址,指向TCJPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即obj也有cj_name.由于person查找cj_name是通过内存平移8字节,所以obj也是通过内存平移8字节去查找cj_name.

③ 拓展二

修改viewDidLoad——在obj前面加个临时字符串变量

同样道理,在obj入栈前已经有了temp变量,此时访问self.cj_name就会访问到temp

④ 拓展三

去掉临时变量,TCJPerson类新增字符串属性cj_hobby,打印方法改为打印cj_hobby,运行

ViewController就是obj偏移16字节拿到的super_class.

⑤ 拓展四

TCJPerson类新增字符串属性cj_hobby,改成int类型,打印

这种情况就是野指针——指针偏移的offset不正确,获取不到对应变量的首地址.

八、Runtime是如何实现weak的,为什么可以自动置nil

weak一行打下断点运行项目

Xcode菜单栏Debug->Debug Workflow->Always show Disassembly打上勾查看汇编——汇编代码会来到libobjc库的objc_initWeak

① weak创建过程

①.1 objc_initWeak

①.2 storeWeak

storeWeak最主要的两个逻辑点

由于是第一次调用,所以走haveNew分支——获取到的是新的散列表SideTable,主要执行了weak_register_no_lock方法来进行插入

①.3 weak_register_no_lock

①.4 append_referrer
找到弱引用对象的对应的weak_entry_t哈希数组中插入

② weak创建流程

③ weak销毁过程

由于弱引用在析构dealloc时自动置空,所以查看dealloc的底层实现

④ weak销毁流程

九、利用runtime-API创建对象

① API介绍

①.1 动态创建类

①.2 添加成员变量

①.3 注册到内存

①.4 添加属性变量

①.5 添加方法

② 整体使用

③ 注意事项

十、Method Swizzing坑点

① method-swizzling 是什么?

method-swizzling的含义是方法交换,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法.

OC中就是利用method-swizzling实现AOP,其中AOP(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程).

每个类都维护着一个方法列表,即methodListmethodList中有不同的方法即Method,每个方法中包含了方法的selIMP,方法交换就是将selimp原本的对应断开,并将sel新的IMP生成对应关系.
如下图所示,交换前后的selIMP的对应关系

② method-swizzling涉及的相关API

③ 坑点1:method-swizzling使用过程中的一次性问题

所谓的一次性就是:mehod-swizzling写在load方法中,而load方法会主动调用多次,这样会导致方法的重复交换,使方法sel的指向又恢复成原来的imp的问题

解决方案

可以通过单例设计原则,使方法交换只执行一次,在OC中可以通过dispatch_once实现单例

④ 坑点2:子类没有实现,父类实现了


运行

子类打印出结果,而父类调用却崩溃了,为什么会这样呢?

优化:避免imp找不到

通过class_addMethod尝试添加你要交换的方法

这样就不会报错了.

下面是class_replaceMethodclass_addMethodmethod_exchangeImplementations的源码实现

其中class_replaceMethodclass_addMethod中都调用了addMethod方法,区别在于bool值的判断,下面是addMethod的源码实现

⑤ 坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?

在上面测试代码的基础上加入父类TCJPersonpersonInstanceMethod的方法只写了方法声明,没有方法实现,却做了方法交换——会造成死循环

原因是 栈溢出,递归死循环了,那么为什么会发生递归呢?----主要是因为 personInstanceMethod没有实现,然后在方法交换时,始终都找不到oriMethod,然后交换了寂寞,即交换失败,当我们调用personInstanceMethod(oriMethod)时,也就是oriMethod会进入TCJ分类cj_studentInstanceMethod方法,然后这个方法中又调用了cj_studentInstanceMethod,此时的cj_studentInstanceMethod并没有指向oriMethod ,然后导致了自己调自己,即递归死循环

优化:避免递归死循环

如果oriMethod为空,为了避免方法交换没有意义,而被废弃,需要做一些事情

⑥ method-swizzling - 类方法

类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作

⑦ method-swizzling的应用

method-swizzling最常用的应用是防止数组、字典等越界崩溃问题
iOSNSNumberNSArrayNSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成.所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的.

下面列举了NSArrayNSDictionary本类的类名,可以通过Runtime函数取出本类.

⑧ 注意事项

使用Method Swizzling有以下注意事项:

写在后面

和谐学习,不急不躁.我还是我,颜色不一样的烟火.

上一篇下一篇

猜你喜欢

热点阅读