学而时习之,不亦说乎
原谅我,我是个标题党,所有文章的名字只是我的噱头,伟大的乔布斯告诉我们"Stay hungry,Stay foolish",希望大家有空杯心态 ,一起学习,一起进步。
分类Category 我想绝大部分人应该不陌生,就算自己没写过分类Category ,一些知名的三方库里都会用到。 而且这也是各一线大厂面试出现频率很高的题 。
上一篇文章,有人反馈说,文章提到的问题,是最基础的。意思是还要深挖?更多深底层? 我知道人的精力是有限的,每个人准备面试时间也是有限的,在有限的时间里,最大限度的提高复习效率,这才是正道。 我也尽量在文中体现适当的原理,源码细节。
不禁感慨想起一句话: 「面试造火箭,入职拧螺丝」。 不过小白们也不要愤愤不平。 毕竟好岗位竞争激烈,只有面试造火箭才能找出出更优秀人。不然大家都考100分,那怎么区分出谁更厉害。
开始面试
我正在会议室略有紧张的等待面试,忽然看到一个穿着格子衬衫,大腹便便的中年男子拿着简历向我走来, 我看着他头上快要绝顶的头发,心想这肯定是个iOS开发技术牛逼闪闪的老前辈。
还好看过杯子写《iOS之一起进大厂》系列,想想现在是满腹经纶,刚紧张到提到嗓子眼的心,又按下去了,淡定从容,一点都不虚好伐,就是这么自信淡定。
我什么时候也能变成那样厉害的高手
1. 小伙子,你家里里提到用过Category,那你可以说他的使用场景是什么,用途是什么?
帅气逼人的面试官您好,Category的用户可以归纳为以下几点:
给现有类添加方法,丰富现有类的功能。 比如有的人就为NSString这类添加了一些很实用的方法(判断字符串是否邮箱,转化字符串为MD5)
分解代码庞大功能复杂的类。把功能复杂代码很多的类,可以按照不同功能的做分类,同一功能放到一个文件里,体现单一职责原则。
声明私有方法。比如定义一个分类,只有头文件放到对应宿主.m里,满足私有方法的声明和使用,不暴露具体实现。
还有其他用法, 但是苹果不欢迎这样用法: 把系统Framework的私有方法公开化。
-
那你说说Category的底层实现是什么?
熟悉oc底层的同学知道,在runtime层都是类和对象都是 struct表示的,category也不例外,category用结构体category_t,结构体包含:
类的名字(name)
类(cls)
category中所有给类添加的实例方法的列表(instanceMethods)
category中所有添加的类方法的列表(classMethods)
category实现的所有协议的列表(protocols)
category中添加的所有属性(instanceProperties)
typedef struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
} category_t;
从category的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)。 category成员变量列表是只读,所以category不能添加实例变量。
-
小伙子不错,那你再说说 Category的特点有什么?和 Class Extendsion(类扩展)有什么区别?
帅气的面试官,这个不难。 我先说下特点。
- 「分类的特点」
分类是运行时决议。怎么理解运行时决议呢? 编译好的分类的文件,是没把相应分类的内容加到宿主类上的。只有在运行时Runtime 才把分类的内容添加到宿主类上。
- 「类扩展的用途是什么?」
一般把不想对外公开一些类的方法,属性,成员变量的时候可以用类的扩展 类扩展代码格式:
@interface XXX ()
//私有属性
//私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
@end
-
那咱们再聊深一点的,Category中+load 和+initialize调用的顺序是什么样的? Category的+load 和+initialize 方法的区别什么?
「调用顺序:」
类要优先于分类调用+load方法。
先编译的分类的+load方法会被优先调。
父类+load优先于子类。
由于分类是objc_msgSend机制,+initialize在所有分类要优先于类调用。 在类中 +initialize父类优先于子类 。如果子类没有实现+initialize,会调用父类的+initialize(所以同一个父类的+initialize可能会被调用多次)。
-
「是什么决定了哪个先编译呢?如下图Category1的+load比Category2的+load优先调用。如果编译文件顺序换一下,被调用的顺序也跟着变。」
「+load 和+initialize 区别在于调用方式和调用时刻不同」
调用方式不同:+load是根据函数地址直接调用,initialize是通过objc_msgSend调用
调用时刻不同:+load方法会在runtime加载类、分类时调用(而且程序运行过程中只调用一次)。
+initialize是类第一次接收到消息的时候调用(即是类调用alloc时),每一个类只会initialize一次(上面提到子类没有实现+initialize,会调用父类的+initialize。这样父类的initialize方法可能会被调用多次)-
那怎么理解category方法覆盖的问题?
「分类添加的方法可以“覆盖”原类方法」
其实覆盖没有真正的覆盖。如果在category里添加了methodA,那么原类的methodA也是存在的,没有真正覆盖掉。只是系统只会调用后来category添加的 methodA。
因为category的methodA被放到了方法列表的前面,原来类中methodA被放到了方法列表的后面,在运行时查找方法会先找到了category 添加的同名方法,就产生了“覆盖“原来类的同名方法的效果。
如果多个分类有同名方法,那么谁能生效取决于谁最后参与编译。最后参与编译的分类对应的方法就会生效。
-
能否给category添加实例变量?那如何给分类添加实例变量?
「不能。」 从上面提到的分类的底层实现,category成员变量列表是只读,所以category不能添加实例变
量。
「那怎么添加呢? 直接添加肯定是不行,可以间接的添加.」
思路1 分类添加全局变量,并且手动重写setter/getter方法实现。但是全局变量有很多隐患,对象销毁时无法销毁。不建议采用。
思路2 用关联对象方法添加实例变量。通过runtime提供的关联对象的方法可以简洁的给分类添加成员变量。通过下面提供的关联对象下API,可以实现给分类添加实例变量:
添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
获得关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象
void objc_removeAssociatedObjects(id object)
「简单说下关联对象方法添加实例变量原理:」
其实关联对象并没有添加到实例变量到被关联对象内存中, 关联对象有一个全局内容管理器(「AssociationsManager」),关联对象通过上面提到的三个API,操作实例变量 存取,移除。从而完成了对分类的添加实例变量。
「面试结束」
小伙子回答的不错,很对我口味,记得明天再来,明天还有更精彩的面试其他内容。 我的内心OS:天呐噜,明天还有!! 这是要在走秃(走向秃顶的道路) 越走越远啊。 (为了下一篇文章,强行做引子,哈哈)
持续更新--请iOS的小伙伴关注! 喜欢的话给一个赞吧!
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS技术圈子:1001906160 ,进群密码000,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!