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

iOS之武功秘籍 文章汇总

写在前面

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

本节可能用到的秘籍Demo

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

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

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

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

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

二、方法的调用顺序

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

  • 如果同名方法是普通方法,包括initialize -- 先调用分类方法
    • 因为分类的方法是在类realize实现之后再attach进去的,插在类的方法的前面,所以优先调用分类的方法(注意:不是分类覆盖主类!!)
    • initialize方法什么时候调用? initialize方法也是主动调用,即第一次消息发送时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize
  • 如果同名方法是load方法 -- 先 主类load,后分类load(分类之间,看编译的顺序)可以参考iOS之武功秘籍⑧: 类和分类加载过程文章中的load_images原理分析

三、Runtime是什么?

  • runtime是由CC++汇编实现的一套API,为OC语言加入了面向对象以及运行时的功能
  • 运行时是指将数据类型的确定由编译时 推迟到了 运行时
    • 举例:extensioncategory 的区别
  • 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtimeC语言代码, runtimeOC的幕后工作者

1、category 类别、分类

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员属性,也无法取到
  • 注意:其实可以通过runtime 给分类添加属性,即属性关联,重写settergetter方法
  • 分类中用@property 定义变量,只会生成变量的settergetter方法的声明,不能生成方法实现 和 带下划线的成员变量

2、extension 类扩展

  • 可以说成是特殊的分类 ,也可称作 匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

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

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

  • 1.快速查找流程——通过汇编objc_msgSend查找缓存cache_t是否有imp实现
  • 2.慢速查找流程——通过C++lookUpImpOrForward递归查找当前类和父类的rwmethodlist的方法
  • 3.查找不到消息:动态方法解析——通过调用resolveInstanceMethodresolveClassMethod来动态方法决议——实现消息动态处理
  • 4.快速转发流程——通过CoreFoundation来触发消息转发流程,forwardingTargetForSelector实现快速转发,由其他对象来实现处理方法
  • 5.慢速转发流程——先调用methodSignatureForSelector获取到方法的签名,生成对应的invocation;再通过forwardInvocation来进行处理

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

SELIMP的关系就可以解释为:

  • sel 就相当于本书的目录标题
  • imp 就相当于书本的页码
  • 具体的函数就是具体页码对应的内容

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

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

具体情况具体分析:

  • 编译好的类不能添加实例变量
  • 运行时创建的类可以添加实例变量,但若已注册到内存中就不行了

原因:

  • 编译好的实例变量存储的位置在ro,而ro是在编译时就已经确定了的
  • ⼀旦编译完成,内存结构就完全确定就⽆法修改
  • 只能修改rw中的方法或者可以通过关联对象的方式来添加属性

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

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

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

  • 只是objc_msgSendSuper2 会更快,直接跳过self的查找

代码调试
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是什么?

  • personisa指向类TCJPersonperson的首地址 指向 TCJPerson的首地址,我们可以通过TCJPerson的内存平移找到cache在cache中查找方法
  • [(__bridge id)obj saySomething]中的obj是来自于TCJPerson这个类,然后有一个指针obj,将其指向TCJPerson的首地址.

所以,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

  • self_cmdviewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈的
  • class_getSuperClassselfobjc_msgSendSuper2中的结构体成员,是从最后一个成员变量,即低地址->高地址反向压栈的

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

person的栈的存储情况

objperson一起的栈的存储情况


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

  • [(__bridge id)obj saySomething]调用时,此时的objTCJPerson: 0x7ffee0d57048,所以saySomething方法中传入的self 还是TCJPerson,但并不是我们通常认为的TCJPerson,是我们当前传入的消息接收者,即TCJPerson: 0x7ffee0d57048,是TCJPerson的实例对象,此时的操作与普通的TCJPerson是一致的,即TCJPerson的地址内存平移8字节.
  • 普通person流程:person -> cj_name - 内存平移8字节
  • obj流程:0x7ffee0d57048 + 0x80 -> 0x7ffee0d57050,即为self,指向<ViewController: 0x7ffee0d57050>

其中 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

  • 1、通过SideTable 找到我们的 weak_table
  • 2、weak_table 根据 referent找到或者创建 weak_entry_t
  • 3、然后append_referrer(entry,referrer)将我的新弱引用的对象加进去entry
  • 4、最后 weak_entry_insert,把entry加入到我们的weak_table

weak一行打下断点运行项目

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

① weak创建过程

①.1 objc_initWeak

  • location:表示__weak指针的地址(我们研究的就是__weak指针指向的内容怎么置为nil
  • newObj:所引用的对象,即例子中的person

①.2 storeWeak

  • HaveOldweak指针之前是否已经指向了一个弱引用
  • HaveNewweak指针是否需要指向一个新引用
  • CrashIfDeallocating:如果被弱引用的对象正在析构,此时再弱引用该对象,是否应该crash

storeWeak最主要的两个逻辑点

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

①.3 weak_register_no_lock

  • 主要进行了isTaggedPointerdeallocating条件判断
  • 将被弱引用对象所在的weak_table中的weak_entry_t哈希数组中取出对应的weak_entry_t
  • 如果weak_entry_t不存在,则会新建一个并插入
  • 如果存在就将指向被弱引用对象地址的指针referrer通过函数append_referrer插入到对应的weak_entry_t引用数组

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

② weak创建流程

③ weak销毁过程

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

  • _objc_rootDealloc->rootDealloc
  • rootDealloc->object_dispose
  • object_dispose->objc_destructInstance
  • objc_destructInstance->clearDeallocating
  • clearDeallocating->sidetable_clearDeallocating
  • weak_clear_no_lock->table.refcnts.erase

④ weak销毁流程

九、利用runtime-API创建对象

① API介绍

①.1 动态创建类

①.2 添加成员变量

①.3 注册到内存

①.4 添加属性变量

①.5 添加方法

② 整体使用

③ 注意事项

  • 记得导入<objc/runtime.h>
  • 添加成员变量class_addIvar必须在objc_registerClassPair前,因为注册到内存时ro已经确定了,不能再往ivars添加
  • 添加属性变量class_addProperty可以在注册内存前后,因为是往rw中添加的
  • class_addProperty中“属性的属性”——nonatomic/copy是根据属性的类型变化而变化的
  • class_addProperty不会自动生成settergetter方法,因此直接调用KVC会崩溃
    • 不只可以通过KVC打印来检验,也可以下断点查看ro、rw的结构来检验

十、Method Swizzing坑点

① method-swizzling 是什么?

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

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

  • OOPAOP都是一种编程的思想
  • OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元
  • AOP面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性.

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

② method-swizzling涉及的相关API

  • 通过sel获取方法Method
    • class_getInstanceMethod:获取实例方法
    • class_getClassMethod:获取类方法
  • method_getImplementation:获取一个方法的实现
  • method_setImplementation:设置一个方法的实现
  • method_getTypeEncoding:获取方法实现的编码类型
  • class_addMethod:添加方法实现
  • class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP
  • method_exchangeImplementations:交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP

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

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

解决方案

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

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

  • 父类TCJPerson类中有-personInstanceMethod方法,子类TCJStudent类没有重写
  • 子类TCJStudent类新建分类做了方法交换,新方法中调用旧方法
  • TCJPerson类、TCJStudent类调用-personInstanceMethod

运行

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

  • [student personInstanceMethod];中不报错是因为 student中的imp交换成了cj_studentInstanceMethod,而TCJStudent中有这个方法(在TCJ分类中),所以不会报错.
  • 崩溃的点在于[person personInstanceMethod];,其本质原因:TCJStudent的分类TCJ中进行了方法交换,将personimp 交换成了 TCJStudent中的cj_studentInstanceMethod,然后需要去 TCJPerson中的找cj_studentInstanceMethod,但是TCJPerson中没有cj_studentInstanceMethod方法,即相关的imp找不到,所以就崩溃了

优化:避免imp找不到

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

  • 如果添加成功,即类中没有这个方法,则通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加
  • 如果添加不成功,即类中有这个方法,则通过method_exchangeImplementations进行交换

这样就不会报错了.

下面是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为空,为了避免方法交换没有意义,而被废弃,需要做一些事情

  • 通过class_addMethodoriSEL添加swiMethod方法
  • 通过method_setImplementationswiMethodIMP指向不做任何事的空实现

⑥ method-swizzling - 类方法

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

  • 需要通过class_getClassMethod方法获取类方法
  • 在调用class_addMethodclass_replaceMethod方法添加和替换时,需要传入的类是元类,元类可以通过object_getClass方法获取类的元类

⑦ method-swizzling的应用

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

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

⑧ 注意事项

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

  • 尽可能在+load方法中交换方法
  • 最好使用单例保证只交换一次
  • 自定义方法名不能产生冲突
  • 对于系统方法要调用原始实现,避免对系统产生影响
  • 做好注释(因为方法交换比较绕)
  • 迫不得已情况下才去使用方法交换

写在后面

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容