题目:
出处:先是程序员,然后才是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 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];
}
}
详见: [爆栈热门 iOS 问题] atomic 和 nonatomic 有什么区别?
三. 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 {
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;
}
把源码的定义转化成类图,如下:
从源码中可以看出,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)
。
具体详见神经病院Objective-C Runtime入院第一天——isa和Class
五. 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
中的方法。
为什么分类不能添加实例变量:
因为一个类的实例变量在编译阶段,就会在在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里面没有用来存储相关变量的数组,这样的结构是不是也就注定实例变量是无法在运行期进行填充.
具体详见:
iOS分类底层实现原理小记
结合 category 工作原理分析 OC2.0 中的 runtime