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 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),并且当keynil,将valuenil

bnil时,ab指向同一个内存地址,在bnil时,anil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

而如果a是由 assign 修饰的,则: 在 bnil 时,ab 指向同一个内存地址,在 bnil 时,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_initWeakobjc_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;
   }];
}

也就是有两个步骤:

  1. setter方法中做如下设置:
  objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
  1. 在属性所指的对象遭到摧毁时,属性值也会清空(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所说的线程安全只是保证了gettersetter存取方法的线程安全,并不能保证整个对象是线程安全的。

假设有一个 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当前类的子类,并为这个新的子类重写了被观察属性keyPathsetter 方法。setter方法随后负责通知观察对象属性的改变状况。同时子类的class方法也会重写为返回父类(原始类)的class
深入剖析:

Apple 使用了isa 混写(isa-swizzling)来实现KVO。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVONSKVONotifying_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监听的类创建子类,同时对该子类的setter和class方法的进行重写,这样就不需要担心影响到本类的其他对象,会因为方法的修改而导致bug.

具体详见: iOS--KVO的实现原理与具体应用

四. 类结构体的组成,isa指针指向了什么?(这里应该将元类和根元类也说一下)

  • classobject 的定义
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;
}

把源码的定义转化成类图,如下:


iOS 面试题一_第1张图片
image.png

从源码中可以看出,Objective-C对象都是C语言结构体实现的,在·objc2.0·中,所有的对象都会包含一个·isa_t·类型的结构体。

objc_object被源码typedefid类型,这也就是平常所用的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,因为每个类的类方法基本不可能完全相同。

对象,类,元类之间的关系图如下:

iOS 面试题一_第2张图片
image.png

图中实线是super_class指针,虚线是isa指针。

  1. 根类Root class (class)其实就是NSObject,NSObject是没有超类的,所以根类Root class (class)superclass指向nil

  2. 每个类Class都有一个isa指针指向唯一的元类(Meta class)

  3. 根元类Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。

  4. 每个元类Meta classisa指针都指向Root class (meta)

具体详见神经病院Objective-C Runtime入院第一天——isa和Class

五. RunLoop有几种事件源?有几种模式?

  • RunLoop的事件源
    CoreFoundation 里面关于 RunLoop5个类:
CFRunLoopRef - 获得当前RunLoop和主RunLoop
CFRunLoopModeRef RunLoop - 运行模式,只能选择一种,在不同模式中做不同的操作
CFRunLoopSourceRef - 事件源,输入源
CFRunLoopTimerRef - 定时器时间
CFRunLoopObserverRef - 观察者

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

iOS 面试题一_第3张图片
image.png

一个RunLoop包含若干个 Mode,每个 Mode又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0Source1

Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef是基于时间的触发器,它和 NSTimertoll-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 会直接退出,不进入循环。

  • RunLoopModel

系统默认注册了5Mode:
1.kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

  1. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode影响。
  2. UIInitializationRunLoopMode: 在刚启动App 时第进入的第一个 Mode,启动完成后就不再使用。
    4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    5: kCFRunLoopCommonModes: 这是一个占位的 Mode,作为标记kCFRunLoopDefaultModeUITrackingRunLoopMode用,并不是一种真正的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中进行分类的加载,主要做了这两件事:

  1. category的实例方法、协议以及属性添加到类上

  2. 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 的分类列表 attachcls 类上

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 协议列表数组中
    }
}
  1. 取出 clsclass_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 中的方法。

为什么分类不能添加实例变量:

iOS 面试题一_第4张图片
image.png

因为一个类的实例变量在编译阶段,就会在在objc_classclass_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 面试题一_第5张图片
image.png

具体详见:
iOS分类底层实现原理小记
结合 category 工作原理分析 OC2.0 中的 runtime

你可能感兴趣的:(iOS 面试题一)