2020年iOS面试题及答案

350 阅读7分钟

1,分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

①类别中原则上只能增加方法(能添加属性的的原因只是通过runtime能添加属性的的原因只是通过runtime的objc_setAssociatedObject和objc_getAssociatedObject方法解决无setter/getter的问题而已); ②类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的( 用范围只能在自身类,而不是子类或其他地方); ③类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。 ④类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。 ⑤定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

最重要的还是类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。 分类方法未实现,编译器也不会报警告。 分类方法与原类中相同会优先调用分类。

分类的结构体

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; // 分类所实现的协议列表
}

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

atomic是在setter和getter方法里会使用自旋锁spinlock_t来保证setter方法和getter方法的线程的安全。可以看做是getter方法获取到返回值之前不会执行setter方法里的赋值代码。如果不加atomic,可能在getter方法读取的过程中,再别的线成立发生setter操作,从而出现异常值。

加上atomic后,setter和getter方法是线程安全的,原子性的,但是出了getter方法和setter方法后就不能保证线程安全了

@property (atomic, strong) NSArray*                arr;
//thread A
for (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.arr = @[@"1", @"2", @"3"];
    }
    else {
        self.arr = @[@"1"];
    }
}

//thread B
for (int i = 0; i < 100000; i ++) {
    if (self.arr.count >= 2) {
        NSString* str = [self.arr objectAtIndex:1];
    }
}

上面的例子线程B里面可能会因为数组越界而引起crash,因为加入在B线程里判断self.arr.count >= 2的时候数组是self.arr = @[@“1”, @“2”, @“3”];但是当调用[self.arr objectAtIndex:1]可能self.arr的值已经在线程A里被更改为了@[@“1”],此时数组越界了。因此,虽然self.arr是atomic的,还是会出现线程安全问题。

3,被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么? 被weak修饰的对象在被释放时候会置为nil,不同于assign;

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。 2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。 3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

struct SideTable {
    // 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

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

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

6,iOS 中内省的几个方法? class 方法和 objc_getClass 方法有什么区别?

1.当参数obj为Object实例对象 object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。

2.当参数obj为Class类对象 object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针;[obj class]返回的则是其本身。

3.当参数obj为Metaclass类对象 object_getClass(obj)返回元类对象中的isa指针,因为元类对象的isa指针指向根类,所有返回的是根类对象的地址指针;[obj class]返回的则是其本身。

4.obj为Rootclass类对象 object_getClass(obj)返回根类对象中的isa指针,因为跟类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针;[obj class]返回的则是其本身。

总结: 经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

7,RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说) Runloop是什么? 深入理解RunLoop | Garan no dou

字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。

1.通知观察者将要进入运行循环。 线程和 RunLoop 之间是一一对应的

2.通知观察者将要处理计时器。

3.通知观察者任何非基于端口的输入源即将触发。

4.触发任何准备触发的基于非端口的输入源。

5.如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。

6.通知观察者线程即将睡眠。

7.将线程置于睡眠状态,直到发生以下事件之一:

事件到达基于端口的输入源。 计时器运行。 为运行循环设置的超时值到期。 运行循环被明确唤醒。

8.通知观察者线程被唤醒。

9.处理待处理事件。

如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。 如果输入源被触发,则传递事件。 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。

10.通知观察者运行循环已退出。

8:苹果是如何实现 autoreleasepool的?

arc下编译器会优化成 void *context = objc_autoreleasePoolPush(); // {}中的代码 objc_autoreleasePoolPop(context); 复制代码 向一个结构AutoreleasePoolPage,中写入需要自动释放的对象,类似一种标记,调用objc_autoreleasePoolPop(context)后,就会把这中间的对象release一下。 这里要注意的是,方法返回值是怎么做到自动释放的? 其使用Thread Local Storage(TLS)线程局部存储,每次存入线程或者从线程取出来。 我们没有卸载{}中的自动释放对象,会在每个runloop结束时候去释放,相当于一个大的autoreleasepool中。 参考文章 苹果是如何实现autoreleasepool的

8,哪些场景可以触发离屏渲染?(知道多少说多少)

  • shouldRasterize(光栅化)
  • masks(遮罩)
  • shadows(阴影)
  • edge antialiasing(抗锯齿)
  • group opacity(不透明)
  • 复杂形状设置圆角等
  • 渐变

原文链接