iOS 进阶+面试(一)

作者: 书写不简单 | 来源:发表于2019-03-04 17:19 被阅读331次

    这篇文章可能有点长,所以分了三篇文章记录。内容来自网上的面试题以及自己面试过程中遇到的问题总结,也会定期更新,不合理的地方欢迎指正。

    一、分类和扩展

    • 分类和扩展有什么区别?

    category

    1、分类给类添加方法
    2、不能通过正常模式给类添加属性,但是可以通过 runtime 添加
    3、如果在分类中通过@property定义属性,那么只会对属性的 getter setter 方法进行声明,不会实现。同时也不会生成带下划线的成员变量
    4、在运行时才会编译代码

    extension

    1、扩展可以看成是特殊的分类 匿名分类
    2、可以给类添加属性,私有
    3、可以给类添加方法,也是私有
    4、在编译时期就会编译,与 .h 文件中的@interface和.m文件里的@implement一起形成了一个完整的类
    5、扩展一般用来隐藏类的信息,所以使用扩展的前提是要有类的源码!所以针对系统自带的类,是无法使用扩展的。

    为什么分类可以添加方法,而不能添加成员变量???

    因为在运行时,类的内部布局早已经确定,如果添加实例变量,会破坏类的内部布局。
    
    • 分类的结构体里面有哪些成员?

    category 是一个指向类结构体的指针,其结构体的定义如下:

    typedef struct objc_category *Category;
    
    struct objc_category {
        char *category_name                          OBJC2_UNAVAILABLE; // 分类名
        char *class_name                             OBJC2_UNAVAILABLE; // 分类所属的类名
        struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE; // 实例方法列表
        struct objc_method_list *class_methods       OBJC2_UNAVAILABLE; // 类方法列表
        struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE; // 分类所实现的协议列表
    }
    

    可以 与 objc_class 的结构体进行对比:

    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                       OBJC2_UNAVAILABLE;  // 父类
        const char *name                        OBJC2_UNAVAILABLE;  // 类名
        long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
        long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
        long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
        struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
        struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
        struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
        struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
    #endif
    } OBJC2_UNAVAILABLE;
    
    

    经过对比,发现少了一个 struct objc_ivar_list *ivars 成员变量列表!!!这也就说明了 分类是不能添加成员变量的。

    • 分类加载和方法调用顺序

      1、 加载:先加载原始类的 load() 方法 ,再去加载 分类中的 load() 方法,如果有多个分类,则按照编译顺序加载

      2、调用:先调用分类中的方法,再去调用原始类中的方法,如果要是重名,则会覆盖原始类中的方法(因为在方法列表中,分类的方法会排在原始类中同名方法的前面)。

    二、atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?

    • atomic

    atomic

    1、会对属性的 setter/getter 方法进行加锁,这仅仅只能保证在 操作 setter/getter 方法是安全的。不能保证其他线程的安全
    2、例如 : 线程1调用了某一属性的setter方法并进行到了一半,线程2调用其getter方法,那么会执行完setter操作后,在执行getter操作,线程2会获取到线程1 setter后的完整的值.
    3、当几个线程同时调用同一属性的setter、getter方法时,会get到一个完整的值,但get到的值不可控。

    例如 : 线程1 调用getter ,线程2 调用setter,线程3 调用setter,这3个线程并行同时开始,线程1会get到一个值,但是这个值不可控,可能是线程2,线程3 set之前的原始值,可能是线程2 set的值,也可能是线程3 set的值

    • atomic是线程安全的吗?

    不是,很多文章谈到atomic和nonatomic的区别时,都说atomic是线程安全,其实这个说法是不准确的.atomic只是对属性的getter/setter方法进行了加锁操作,这种安全仅仅是set/get 的读写安全,并非真正意义上的线程安全,因为线程安全还有读写之外的其他操作

    比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash
    
    • nonatomic

    nonatomic

    系统生成的getter/setter方法没有加锁线程不安全,但更快当多个线程同时访问同一个属性,会出现无法预料的结果

    • atomic的seter getter内部实现
    - (void)setCurrentImage:(UIImage *)currentImage
    {
        @synchronized(self) {
            if (_currentImage != currentImage) {
                [_currentImage release];
                _currentImage = [currentImage retain];
    
            }
        }
    }
    
    - (UIImage *)currentImage
    {
        @synchronized(self) {
            return _currentImage;
        }
    }
    
    
    • nonatomic的seter getter内部实现
    - (void)setCurrentImage:(UIImage *)currentImage
    {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];
    
        }
    }
    - (UIImage *)currentImage
    {
        return _currentImage;
    }
    

    三、被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

    参考:https://www.jianshu.com/p/b93d61418f17
    这个问题在 数据结构&&算法里面做了解答

    四、关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将其指针置空么?

    我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用 @property 并不能在分类中正确创建实例变量和存取方法。这时候就会用到关联对象。

    分类中的 @property
    @interface DKObject : NSObject
    
    @property (nonatomic, strong) NSString *property;
    
    @end
    

    在使用上述代码时会做三件事:

    • 生成带下划线的实例变量 _property
    • 生成 getter 方法 - property
    • 生成 setter 方法 - setProperty:
    @implementation DKObject {
        NSString *_property;
    }
    
    - (NSString *)property {
        return _property;
    }
    
    - (void)setProperty:(NSString *)property {
        _property = property;
    }
    
    @end
    
    

    这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里,我们既然可以在类中使用 @property 生成一个属性,那么为什么在分类中不可以呢?

    我们来做一个小实验:创建一个 DKObject 的分类 Category,并添加一个属性 categoryProperty

    @interface DKObject (Category)
    
    @property (nonatomic, strong) NSString *categoryProperty;
    
    @end
    

    看起来还是很不错的,不过 Build 一下这个 Demo,会发现有这么一个警告:

    1975281-b76dd29743ae996a.png.jpeg
    在这里的警告告诉我们 categoryProperty 属性的存取方法需要自己手动去实现,或者使用 @dynamic 在运行时实现这些方法。

    换句话说,分类中的 @property 并没有为我们生成实例变量以及存取方法,而需要我们手动实现。

    使用关联对象

    Q:我们为什么要使用关联对象?

    A:因为在分类中 @property 并不会自动生成实例变量以及存取方法,所以一般使用关联对象为已经存在的类添加『属性』。

    以下是与关联对象有关的 API,并在分类中实现一个伪属性:

    #import "DKObject+Category.h"
    #import <objc/runtime.h>
    
    @implementation DKObject (Category)
    
    - (NSString *)categoryProperty {
        return objc_getAssociatedObject(self, _cmd);
    }
    
    - (void)setCategoryProperty:(NSString *)categoryProperty {
        objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    @end
    
    

    这里的 _cmd 代指当前方法的选择子,也就是 @selector(categoryProperty)

    我们使用了两个方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 来模拟『属性』的存取方法,而使用关联对象模拟实例变量。

    在这里有必要解释两个问题:

    • 为什么向方法中传入 @selector(categoryProperty)?
    • OBJC_ASSOCIATION_RETAIN_NONATOMIC 是干什么的?

    关于第一个问题,我们需要看一下这两个方法的原型:

    id objc_getAssociatedObject(id object, const void *key);
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    

    @selector(categoryProperty) 也就是参数中的key,其实可以使用静态指针 static void *类型的参数来代替,不过在这里,笔者强烈推荐使用 @selector(categoryProperty) 作为 key 传入。因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性

    OBJC_ASSOCIATION_RETAIN_NONATOMIC 又是什么呢?如果我们使用 Command 加左键查看它的定义:

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                *   The association is made atomically. */
        OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                *   The association is made atomically. */
    };
    

    从这里的注释我们能看到很多东西,也就是说不同的 objc_AssociationPolicy 对应了不通的属性修饰符:

    objc_AssociationPolicy modifier
    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
    OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
    OBJC_ASSOCIATION_RETAIN atomic, strong
    OBJC_ASSOCIATION_COPY atomic, copy

    而我们在代码中实现的属性 categoryProperty 就相当于使用了 nonatomic 和 strong 修饰符。

    在obj dealloc时候会调用object_dispose,检查有无关联对象,有的话_object_remove_assocations删除

    五、KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

    实现原理:


    kvo原理图.png
    • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
    • 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
    • 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

    KVO与Notification之间的区别:

    • notification是需要一个发送notification的对象,一般是notificationCenter,来通知观察者。

    • KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。

    六、Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

    
    每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
    
    最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。
    

    Autorelease对象什么时候释放?

    这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。

    在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

    例子:

    __weak id reference = nil;
    - (void)viewDidLoad {
        [super viewDidLoad];    NSString *str = [NSString stringWithFormat:@"sunnyxx"];    // str是一个autorelease对象,设置一个weak的引用来观察它
        reference = str;
    }
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];    
        NSLog(@"%@", reference); 
        // Console: sunnyxx
    }
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];    
        NSLog(@"%@", reference); 
        // Console: (null)
    }
    

    当然,我们也可以手动干预Autorelease对象的释放时机:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        @autoreleasepool {        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
        }    NSLog(@"%@", str); 
    // Console: (null)
    }
    
    Autorelease原理

    AutoreleasePoolPage

    ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

    void *context = objc_autoreleasePoolPush();
    // {}中的代码objc_autoreleasePoolPop(context);
    

    而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

    AutoreleasePoolPage是一个C++实现的类


    1402395151-0.jpg
    • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。
    • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)。
    • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
    • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
    • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

    所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:


    1402396457-1.jpg

    图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

    所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

    释放时刻

    每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:

    1402391c4-2.jpg

    objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

    1.根据传入的哨兵对象地址找到哨兵对象所处的page

    2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

    3.补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理)

    刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:


    1402392N7-3.jpg
    嵌套的AutoreleasePool

    知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

    七、class_ro_t 和 class_rw_t 的区别?

    Class的结构
    1345141-1c9a3f46caca5efc.png
    class_rw_t

    class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容

    1345141-92396f3642e690d1.png
    class_ro_t

    class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容


    1345141-723fbcb1a1be35eb.png

    相关文章

      网友评论

        本文标题:iOS 进阶+面试(一)

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