iOS 面试题一
题目:
出处:先是程序员,然后才是iOS程序员 — 写给广大非科班iOS开发者的一篇面试总结
如果让你实现属性的weak,如何实现的?
如果让你来实现属性的atomic,如何实现?
KVO为什么要创建一个子类来实现?
类结构体的组成,isa指针指向了什么?(这里应该将元类和根元类也说一下)
RunLoop有几种事件源?有几种模式?
方法列表的数据结构是什么?
分类是如何实现的?它为什么会覆盖掉原来的方法?为什么分类不能添加实例变量
一. 如果让你实现属性的weak
,如何实现的?
- 要实现
weak
属性,首先要搞清楚weak
属性的特点:
weak
此特质表明该属性定义了一种“非拥有关系”,为这种属性所修饰的值设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指对象遭到摧毁时,属性值也会清空。
先看下runtime
里源码实现:
/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line==0, the set is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
我们可以设计一个函数(伪代码)来表示上述机制:
objc_storeWeak(&a, b)函数:
objc_storeWeak
函数把第二个参数--赋值对象(b)
的内存地址作为键值key
,将第一个参数--weak
修饰的属性变量(a)
的内存地址(&a)
作为value,注册到 weak
表中。如果第二个参数(b)
为0(nil)
,那么把变量(a)
的内存地址(&a)
从weak
表中删除,
你可以把objc_storeWeak(&a, b)
理解为:objc_storeWeak(value, key)
,并且当key
变nil
,将value
置nil
。
在b
非nil
时,a
和b
指向同一个内存地址,在b
变nil
时,a
变nil
。此时向a
发送消息不会崩溃:在Objective-C
中向nil
发送消息是安全的。
而如果a
是由 assign
修饰的,则: 在 b
非 nil
时,a
和b
指向同一个内存地址,在 b
变 nil
时,a
还是指向该内存地址,变野指针。此时向 a
发送消息极易崩溃。
下面我们将基于objc_storeWeak(&a, b)
函数,使用伪代码模拟“runtime
如何实现weak属性”:
// 使用伪代码模拟:runtime如何实现weak属性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
objc_destroyWeak(&obj1);
下面对用到的两个方法objc_initWeak
和objc_destroyWeak
做下解释:
总体说来,作用是: 通过objc_initWeak
函数初始化“附有weak
修饰符的变量(obj1)
”,在变量作用域结束时通过objc_destoryWeak
函数释放该变量(obj1)
。
下面分别介绍下方法的内部实现:
objc_initWeak
函数的实现是这样的:在将“附有weak
修饰符的变量(obj1)
”初始化为0(nil)
后,会将“赋值对象”(obj)
作为参数,调用objc_storeWeak函数。
obj1 = 0;
obj_storeWeak(&obj1, obj);
也就是说:
weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
然后obj_destroyWeak
函数将0(nil)
作为参数,调用objc_storeWeak
函数。
objc_storeWeak(&obj1, 0);
前面的源代码与下列源代码相同。
// 使用伪代码模拟:runtime如何实现weak属性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);
objc_storeWeak
函数把第二个参数--赋值对象(obj)
的内存地址作为键值,将第一个参数--weak
修饰的属性变量(obj1)
的内存地址注册到 weak
表中。如果第二个参数(obj)
为0(nil)
,那么把变量(obj1)
的地址从 weak
表中删除。
使用伪代码是为了方便理解,下面我们“真枪实弹”地实现下:
如何让不使用weak修饰的@property,拥有weak的效果。
我们从setter
方法入手:
(注意以下的cyl_runAtDealloc
方法实现仅仅用于模拟原理,如果想用于项目中,还需要考虑更复杂的场景,想在实际项目使用的话,可以使用我写的一个小库,可以使用 CocoaPods
在项目中使用:CYLDeallocBlockExecutor)
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
也就是有两个步骤:
- 在
setter
方法中做如下设置:
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
- 在属性所指的对象遭到摧毁时,属性值也会清空
(nil out)
。做到这点,同样要借助runtime
:
//要销毁的目标对象
id objectToBeDeallocated;
//可以理解为一个“事件”:当上面的目标对象销毁时,同时要发生的“事件”。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
someUniqueKey,
objectWeWantToBeReleasedWhenThatHappens,
OBJC_ASSOCIATION_RETAIN);
知道了思路,我们就开始实现cyl_runAtDealloc
方法,实现过程分两部分:
第一部分:创建一个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block
执行“事件”。
.h文件
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。
typedef void (^voidBlock)(void);
@interface CYLBlockExecutor : NSObject
- (id)initWithBlock:(voidBlock)block;
@end
.m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助`block`执行“事件”。
#import "CYLBlockExecutor.h"
@interface CYLBlockExecutor() {
voidBlock _block;
}
@implementation CYLBlockExecutor
- (id)initWithBlock:(voidBlock)aBlock
{
self = [super init];
if (self) {
_block = [aBlock copy];
}
return self;
}
- (void)dealloc
{
_block ? _block() : nil;
}
@end
第二部分:核心代码:利用runtime实现cyl_runAtDealloc
方法
// CYLNSObject+RunAtDealloc.h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 利用runtime实现cyl_runAtDealloc方法
#import "CYLBlockExecutor.h"
const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;
@interface NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block;
@end
// CYLNSObject+RunAtDealloc.m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 利用runtime实现cyl_runAtDealloc方法
#import "CYLNSObject+RunAtDealloc.h"
#import "CYLBlockExecutor.h"
@implementation NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block
{
if (block) {
CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];
objc_setAssociatedObject(self,
runAtDeallocBlockKey,
executor,
OBJC_ASSOCIATION_RETAIN);
}
}
@end
使用方法: 导入
#import "CYLNSObject+RunAtDealloc.h"
然后就可以使用了:
NSObject *foo = [[NSObject alloc] init];
[foo cyl_runAtDealloc:^{
NSLog(@"正在释放foo!");
}];
如果对cyl_runAtDealloc
的实现原理有兴趣,可以看下我写的一个小库,可以使用 CocoaPods
在项目中使用:CYLDeallocBlockExecutor
具体详见:《招聘一个靠谱的iOS》
二. 如果让你来实现属性的atomic,如何实现?
atomic
特点:
系统生成
getter/setter
方法会保证get、set
操作的完整性,不受其他线程的影响。同时atomic是默认属性,会有一定的系统开销。
但是atomic
所说的线程安全只是保证了getter
和setter
存取方法的线程安全,并不能保证整个对象是线程安全的。
假设有一个 atomic
的属性 name
,如果线程 A
调[self setName:@"A"]
,线程 B
调[self setName:@"B"]
,线程 C
调[self name]
,那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行getter/setter
,其他线程就得等待。因此,属性 name
是读/写安全的。
但是,如果有另一个线程 D
同时在调[name release]
,那可能就会crash
,因为 release
不受 getter/setter
操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
如果 name
属性是nonatomic
的,那么上面例子里的所有线程 A、B、C、D
都可以同时执行,可能导致无法预料的结果。如果是 atomic
的,那么 A、B、C
会串行,而D
还是并行的。
实现automic
属性:
//@property(automic, retain) UITextField *userName;
//系统生成的代码如下:
- (UITextField *) userName {
@synchronized(self) {
return _userName;
}
}
- (void) setUserName:(UITextField *)userName {
@synchronized(self) {
if(userName != _userName) {
[_userName release];
_userName = [userName_ retain];
}
}
}
nonatomic
实现:
//@property(nonatomic, retain) UITextField *userName;
//系统生成的代码如下:
- (UITextField *) userName {
return _userName;
}
- (void) setUserName:(UITextField *)userName {
if(userName != _userName) {
[_userName release];
_userName = [userName_ retain];
}
}
三. KVO为什么要创建一个子类来实现?
基本的原理:
当观察某对象A
时,KVO
机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath
的setter
方法。setter
方法随后负责通知观察对象属性的改变状况。同时子类的class
方法也会重写为返回父类(原始类)的class
。
深入剖析:
Apple
使用了isa
混写(isa-swizzling)
来实现KVO
。当观察对象A
时,KVO
机制动态创建一个新的名为: NSKVONotifying_A
的新类,该类继承自对象A
的本类,且KVO
为NSKVONotifying_A
重写观察属性的setter
方法,setter 方法会负责在调用原setter
方法之前和之后,通知所有观察对象属性值的更改情况。
(备注: isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;)
①NSKVONotifying_A
类剖析:在这个过程,被观察对象的 isa
指针从指向原来的A
类,被KVO
机制修改为指向系统新创建的子类 NSKVONotifying_A
类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO
的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A
的中间类,并指向这个中间类了。
(isa
指针的作用:每个对象都有isa
指针,指向该对象的类,它告诉Runtime
系统这个对象的类是什么。所以对象注册为观察者时,isa
指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter
的调用就会调用已重写的 setter
,从而激活键值通知机制。
②子类setter
方法剖析:KVO
的键值观察通知依赖于 NSObject
的两个方法:willChangeValueForKey:
和 didChangevlueForKey:
,在存取数值的前后分别调用2个方法:
被观察属性发生改变之前,willChangeValueForKey:
被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey:
被调用,通知系统该keyPath
的属性值已经变更;之后,observeValueForKey:ofObject:change:context:
也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
-(void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; //KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO在调用存取方法之后总调用
}
既然是重写,就有两种选择: 改变本类和改变子类
- 改变本类,就会污染到本类的所有其他对象的方法,显然这种做法是不可取的
- 改变子类, 只针对被添加
KVO
监听的类创建子类,同时对该子类的sette
r和class
方法的进行重写,这样就不需要担心影响到本类的其他对象,会因为方法的修改而导致bug.
具体详见: iOS--KVO的实现原理与具体应用
四. 类结构体的组成,isa指针指向了什么?(这里应该将元类和根元类也说一下)
- 对
class
与object
的定义
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
把源码的定义转化成类图,如下:
image.png
从源码中可以看出,Objective-C对象都是C语言结构体实现的,在·objc2.0·中,所有的对象都会包含一个·isa_t·类型的结构体。
objc_object
被源码typedef
成id
类型,这也就是平常所用的id
类型,这个结构体中只包含一个isa_t
类型的结构体。
objc_class
继承自objc_object
。所以在objc_class
中也会包含isa_t
类型的结构体isa
。至此,可以得出: Objective-C中类也是一个对象。在objc_class
中,除了isa
之外,还有3
个成员变量,一个是父类的指针,一个是方法缓存,最后一个是这个类的实例方法链表。
isa
指针指向
当一个对象的实例方法被调用的时候,会通过isa
找到相对应的类,然后在该类的class_data_bits_t
中去查找方法。class_data_bits_t
是指向了类对象的数据区域。在该数据区域内查找相应方法的对应实现。
同样当我们调用类方法的时候,类对象的isa
里面是什么呢?这里为了和对象查找方法的机制一致,遂引入了元类(meta-class)
的概念。
在引入元类之后,类对象和对象查找方法的机制就完全统一了。
对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。
meta-class
之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class
,因为每个类的类方法基本不可能完全相同。
对象,类,元类之间的关系图如下:
图中实线是super_class
指针,虚线是isa
指针。
-
根类Root class (class)
其实就是NSObject
,NSObject
是没有超类的,所以根类Root class (class)
的superclass
指向nil
。 -
每个
类Class
都有一个isa
指针指向唯一的元类(Meta class)
-
根元类Root class(meta
)的superclass
指向Root class(class)
,也就是NSObject
,形成一个回路。 -
每个
元类Meta class
的isa
指针都指向Root class (meta)
。
五. RunLoop有几种事件源?有几种模式?
-
RunLoop
的事件源
在CoreFoundation
里面关于RunLoop
有5
个类:
CFRunLoopRef - 获得当前RunLoop和主RunLoop
CFRunLoopModeRef RunLoop - 运行模式,只能选择一种,在不同模式中做不同的操作
CFRunLoopSourceRef - 事件源,输入源
CFRunLoopTimerRef - 定时器时间
CFRunLoopObserverRef - 观察者
其中 CFRunLoopModeRef
类并没有对外暴露,只是通过 CFRunLoopRef
的接口进行了封装。他们的关系如下:
一个RunLoop
包含若干个 Mode
,每个 Mode
又包含若干个 Source/Timer/Observer
。每次调用 RunLoop
的主函数时,只能指定其中一个 Mode
,这个Mode
被称作 CurrentMode
。如果需要切换 Mode
,只能退出Loop
,再重新指定一个 Mode
进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer
,让其互不影响。
CFRunLoopSourceRef
是事件产生的地方。Source
有两个版本:Source0
和Source1
。
•Source0
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source)
,将这个Source
标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 RunLoop
,让其处理这个事件。
• Source1
包含了一个 mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source
能主动唤醒 RunLoop
的线程,其原理在下面会讲到。
CFRunLoopTimerRef
是基于时间的触发器,它和 NSTimer
是toll-free bridged
的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调。
CFRunLoopObserverRef
是观察者,每个 Observer
都包含了一个回调(函数指针),当 RunLoop
的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
上面的 Source/Timer/Observer
被统称为 mode item
,一个 item
可以被同时加入多个 mode
。但一个item
被重复加入同一个 mode
时是不会有效果的。如果一个 mode
中一个 item
都没有,则 RunLoop
会直接退出,不进入循环。
RunLoop
的Model
系统默认注册了5
个Mode
:
1.kCFRunLoopDefaultMode
: App
的默认 Mode
,通常主线程是在这个 Mode
下运行的。
-
UITrackingRunLoopMode
: 界面跟踪Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响。 -
UIInitializationRunLoopMode
: 在刚启动App
时第进入的第一个Mode
,启动完成后就不再使用。
4:GSEventReceiveRunLoopMode
: 接受系统事件的内部Mode
,通常用不到。
5:kCFRunLoopCommonModes
: 这是一个占位的Mode
,作为标记kCFRunLoopDefaultMode
和UITrackingRunLoopMode
用,并不是一种真正的Mode
详见:深入理解RunLoop
六. 方法列表的数据结构是什么?
struct objc_method_list {
/* 这个变量用来链接另一个单独的方法链表 */
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
/* 结构中定义的方法数量 */
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
七. 分类是如何实现的?它为什么会覆盖掉原来的方法?为什么分类不能添加实例变量
分类是如何实现的:
- 在程序启动的入口函数
_objc_init
中通过如下调用顺序
void _objc_init(void)
└──const char *map_2_images(...)
└──const char *map_images_nolock(...)
└──void _read_images(header_info **hList, uint32_t hCount)
在_read_images
中进行分类的加载,主要做了这两件事:
-
把
category
的实例方法、协议以及属性添加到类上 -
把
category
的类方法和协议添加到类的metaclass
上
相关代码如下:
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
这里 addUnattachedCategoryForClass(cat, cls->ISA(), hi);
主要是为类添加添加未依附的分类。
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertWriting();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
执行过程伪代码:
1.取得存储所有 unattached 分类的列表
NXMapTable *cats = unattachedCategories();
2.从 cats 列表中找倒 cls 对应的 unattached 分类的列表
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
3.将新来的分类 cat 添加刚刚开辟的位置上
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
4.将新的 list 重新插入 cats 中,会覆盖老的 list
NXMapInsert(cats, cls, list);
执行完这个过程,系统将分类放到一个该类cls
对应的unattached
分类的list中。
接着执行remethodizeClass(cls)
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
执行过程伪代码:
1.取得 cls
类的unattached
的分类列表
category_list *cats = unattachedCategoriesForClass(cls, false/*not realizing*/)
2.将 unattached
的分类列表 attach
到 cls
类上
attachCategories(cls, cats, true /* 清空方法缓存 flush caches*/);
执行完上述过程后,系统就把category的实例方法、协议以及属性添加到类上。
在attachCategories(cls, cats, true /* 清空方法缓存 flush caches*/)
函数内部:
1.在堆上创建方法、属性、协议数组,用来存储分类的方法、属性、协议
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
2.遍历 cats
,取出各个分类的方法、属性、协议,并填充到上述代码创建的数组中
int mcount = 0; // 记录方法的数量
int propcount = 0; // 记录属性的数量
int protocount = 0; // 记录协议的数量
int i = cats->count; // 从后开始,保证先取最新的分类
bool fromBundle = NO; // 记录是否是从 bundle 中取的
while (i--) { // 从后往前遍历
auto& entry = cats->list[i]; // 分类,locstamped_category_t 类型
// 取出分类中的方法列表;如果是元类,取得的是类方法列表;否则取得的是实例方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist; // 将方法列表放入 mlists 方法列表数组中
fromBundle |= entry.hi->isBundle(); // 分类的头部信息中存储了是否是 bundle,将其记住
}
// 取出分类中的属性列表,如果是元类,取得是nil
property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
if (proplist) {
proplists[propcount++] = proplist; // 将属性列表放入 proplists 属性列表数组中
}
// 取出分类中遵循的协议列表
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist; // 将协议列表放入 protolists 协议列表数组中
}
}
- 取出
cls
的class_rw_t
数据
auto rw = cls->data();
4.存储方法、属性、协议数组到 rw
// 准备 mlists 中的方法
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 将新方法列表添加到 rw 中的方法列表数组中并释放mlists
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
// 将新属性列表添加到 rw 中的属性列表数组中并释放proplists
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 将新协议列表添加到 rw 中的协议列表数组中并释放protolists
rw->protocols.attachLists(protolists, protocount);
free(protolists);
其中 rw->methods.attachLists
是用来合并category
中的方法:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}
这段代码就是先调用 realloc()
函数将原来的空间拓展,然后把原来的数组复制到后面,最后再把新数组复制到前面。
这就是为什么类别中的方法会在类中的方法前面的原因。
它为什么会覆盖掉原来的方法?
我们来看下 runtime
在查找方法时的逻辑:
static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists) {
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
可见搜索的过程是按照从前向后的顺序进行的,一旦找到了就会停止循环。由于, category
中的方法在类中方法的前面,因此 category
中定义的同名方法不会替换类中原有的方法,但是对原方法的调用实际上会调用 category
中的方法。
为什么分类不能添加实例变量:
image.png因为一个类的实例变量在编译阶段,就会在在objc_class
的class_ro_t
这里进行存储和布局,而category
是在运行时才进行加载的,
然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:
// 从 `class_data_bits_t `调用 `data` 方法,将结果从 `class_rw_t `强制转换为 `class_ro_t `指针
const class_ro_t *ro = (const class_ro_t *)cls->data();
// 初始化一个 `class_rw_t` 结构体
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
// 设置`结构体 ro` 的值以及 `flag`
rw->ro = ro;
// 最后设置正确的` data`。
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
运行时加载的时候class_ro_t
里面的方法、协议、属性等内容赋值给class_rw_t
,而class_rw_
t里面没有用来存储相关变量的数组,这样的结构是不是也就注定实例变量是无法在运行期进行填充.