iOS 面试题一

作者: 果哥爸 | 来源:发表于2017-12-17 16:20 被阅读133次

    题目:

    出处:先是程序员,然后才是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),并且当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 <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被源码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,因为每个类的类方法基本不可能完全相同。

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

    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 的接口进行了封装。他们的关系如下:

    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 中的方法。

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

    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里面没有用来存储相关变量的数组,这样的结构是不是也就注定实例变量是无法在运行期进行填充.

    image.png

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

    相关文章

      网友评论

        本文标题:iOS 面试题一

        本文链接:https://www.haomeiwen.com/subject/cicpixtx.html