"2022" 新 iOS 开发进阶+面试大全(一)

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

  • 更多技术题合集:

iOS技术题大全(上)

iOS技术题大全(中)

iOS技术题大全(下)

提升iOS开发技能学习网址:docs.qq.com/doc/DVWlQam9Qd3B1cEF2

为自己的面试,为自己的跳槽,加油吧 iOS开发

一、分类和扩展

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

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,会发现有这么一个警告:

image

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

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

使用关联对象

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

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

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

#import "DKObject+Category.h"
#import 

@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)?

实现原理:

image
  • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 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++实现的类

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

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

image

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

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

释放时刻

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

image

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

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

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

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

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

image
嵌套的AutoreleasePool

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

七、class_ro_t 和 class_rw_t 的区别?

Class的结构
image
class_rw_t

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

image
class_ro_t

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

image

收录文章来源

你可能感兴趣的:("2022" 新 iOS 开发进阶+面试大全(一))