2020年阿里、字节:一套高效的iOS面试题(二)

610 阅读14分钟

NSNotification相关

相关参考

1、实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)

参考这篇文章

2、通知的发送是同步的,还是异步的?

同步的

3、NSNotificationCenter接收消息和发送消息是在一个线程里吗?如何异步发送消息?

通知的接收和发送是在一个线程里
实际上发送通知都是同步的,不存在异步操作。而所谓的异步发送,也就是延迟发送,在合适的实际发送。
实现异步发送:

  • 让通知的执行方法异步执行即可
  • 通过NSNotificationQueue,将通知添加到队列当中,立即将控制权返回给调用者,在合适的时机发送通知,从而不会阻塞当前的调用

参考这篇文章

4、NSNotificationQueue是异步还是同步发送?在哪个线程响应?

NSPostingStyle的值为:

  • NSPostWhenIdle和NSPostASAP:异步发送
  • NSPostNow:同步发送

响应线程:

默认情况是在主线程中响应的,倘若在调用enqueueNotification将通知添加到队列中时,是在子线程中完成的,那么,响应也会在这个子线程中。

5、NSNotificationQueue和runloop的关系

NSNotificationQueue将通知添加到队列中时,其中postringStyle参数就是定义通知调用和runloop状态之间关系。

该参数的三个可选参数:

  • NSPostWhenIdle:runloop空闲的时候回调通知方法
  • NSPostASAP:runloop在执行timer事件或sources事件完成的时候回调通知方法
  • NSPostNow:runloop立即回调通知方法

参考这篇文章

6、如何保证通知接收的线程在主线程?

有以下两种方案

  • 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
  • 通过在主线程的runloop中添加machPort,设置这个port的delegate,通过这个Port其他线程可以跟主线程通信,在这个port的代理回调中执行的代码肯定在主线程中运行,所以,在这里调用NSNotificationCenter发送通知即可,参考这篇文章
7、页面销毁时不移除通知会崩溃吗?
  • iOS9.0之前,会crash,原因:通知中心对观察者的引用是unsafe_unretained,导致当观察者释放的时候,观察者的指针值并不为nil,出现野指针。
  • iOS9.0之后,不会crash,原因:通知中心对观察者的引用是weak。
8、多次添加同一个通知会是什么结果?多次移除通知呢?

多次添加同一个通知,会导致发送一次这个通知的时候,响应多次通知回调。
多次移除通知不会产生crash。

9、下面的方式能接收到通知吗?为什么?
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

不能

需要了解通知中心存储通知观察者的结构了,具体如下:

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation        *wildcard;    /* 链表结构,保存既没有name也没有object的通知 */
  GSIMapTable        nameless;    /* 存储没有name但是有object的通知    */
  GSIMapTable        named;        /* 存储带有name的通知,不管有没有object    */
    ...
} NCTable;

// Observation 存储观察者和响应结构体,基本的存储单元
typedef    struct    Obs {
  id        observer;    /* 观察者,接收通知的对象    */
  SEL        selector;    /* 响应方法        */
  struct Obs    *next;        /* Next item in linked list.    */
  ...
} Observation;

nameless与named的具体数据结构如下:

如上图所示,当添加通知监听的时候,我们传入了name和object,所以,观察者的存储链表是这样的:
named表:key(name):value->key(object):value(Observation)
因此在发送通知的时候,如果只传入name而并没有传入object,是找不到Observation的,也就不能执行观察者回调

Runloop & KVO

runloop

1、app如何接收到触摸事件的?
  1. 首先,手机中处理触摸事件的是硬件系统进程 ,当硬件系统进程识别到触摸事件后,会将这个事件进行封装,并通过machPort,将封装的事件发送给当前活跃的APP进程。
  2. 由于APP的主线程中runloop注册了这个machPort端口,就是用于接收处理这个事件的,所以这里APP收到这个消息后,开始寻找响应链。
  3. 寻找到响应链后,开始分发事件,它会优先发送给手势集合,来过滤这个事件,一旦手势集合中其中一个手势识别了这个事件,那么这个事件将不会发送给响应链对象。
  4. 手势没有识别到这个事件,事件将会发送给响应链对象UIResponser。

参考这篇文章

2、为什么只有主线程的runloop是开启的?

app启动前会调用main函数,具体如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

mian函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行,所以在主线程中开启一个runloop,让主线程常驻。

3、为什么只在主线程刷新UI?

UIKit并不是一个 线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。

参考这篇文章

4、PerformSelector和runloop的关系。

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

参考这篇文章

5、如何使线程保活?
  • 在NSThread执行的方法中添加while(true){},这样是模拟runloop的运行原理,结合GCD的信号量,在{}中处理任务。参考这篇文章
  • 采用runloop的方式。参考这篇文章

KVO

1、实现原理。

在给对象A的属性name添加KVO观察者的时候,runtime会动态创建一个类B,这个类B继承自类A,并且重写了父类的属性name的setter方法,在重写的方法中,在给name成员变量赋值的前后,分别通知调用观察者回调。

参考这篇文章

2、如何手动关闭kvo?
  • 重写被观察对象的automaticallyNotifiesObserversForKey方法,返回NO
  • 重写automaticallyNotifiesObserversOf<key>,返回NO

注意:关闭kvo后,需要手动在赋值前后添加willChangeValueForKey和didChangeValueForKey,才可以收到观察通知。

参考这篇文章

3、通过KVC修改属性会触发KVO么?

4、哪些情况下使用kvo会崩溃,怎么防护崩溃?
  • removeObserver一个未注册的keyPath,导致错误:Cannot remove an observer A for the key path "str",****because it is not registered as an observer.

解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。

  • 添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath发生变化时,kvo中的观察者的引用变成了野指针,导致crash。

解决办法:在观察者即将销毁的时候,先移除这个观察者。

其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象,参考KVOController

5、kvo的优缺点?

缺点补充:

  • 只能通过重写 -observeValueForKeyPath:ofObject:change:context:方法来获得通知。
  • 不同通过指定selector的方式获取通知。
  • 不能通过block的方式获取通知。

参考这篇文章

Block

1、block的内部实现,结构体是什么样的?

block的结构体如下:

struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 {
    unsigned long int reserved;         // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        // optional helper functions
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
        // required ABI.2010.3.16
        const char *signature;                         // IFF (1<<30)
    } *descriptor;
    // imported variables
};

isa:由此可知,block也是一个对象类型,具体类型包括_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock。

flags:block 的负载信息(引用计数和类型信息),按位存储,也可以获取block版本兼容的相关信息。以下是flags按bit位取与的所有可能值:

enum {
    // Set to true on blocks that have captures (and thus are not true
    // global blocks) but are known not to escape for various other
    // reasons. For backward compatibility with old runtimes, whenever
    // BLOCK_IS_NOESCAPE is set, BLOCK_IS_GLOBAL is set too. Copying a
    // non-escaping block returns the original block and releasing such a
    // block is a no-op, which is exactly how global blocks are handled.
    BLOCK_IS_NOESCAPE      =  (1 << 23),

    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), // helpers have C++ code
    BLOCK_IS_GLOBAL =         (1 << 28),
    BLOCK_HAS_STRET =         (1 << 29), // IFF BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE =     (1 << 30),
};
switch (flags & (3<<29)) {
  case (0<<29):      10.6.ABI, no signature field available
  case (1<<29):      10.6.ABI, no signature field available
  case (2<<29): ABI.2010.3.16, regular calling convention, presence of signature field
  case (3<<29): ABI.2010.3.16, stret calling convention, presence of signature field,
}

由此可知:当flags & (3<<29) is BLOCK_HAS_COPY_DISPOSE的时候,才会有copy_helper和dispose_helper函数指针。
invoke:是block具体实现函数指针地址,可以通过此地址直接调用block。
Block_descriptor_1:block的描述文内容,它包括如下:
size:block所占的内存大小
copy_helper:copy函数指针(不同版本不一定存在)
dispose_helper:dispose函数指针(不同版本不一定存在)
signature:block的实现函数的签名(不同版本不一定存在),可以通过此指针获取block的参数内容描述、返回值内容描述等
获取block的方法签名,可以参考这篇文章

2、block是类吗,有哪些类型?

从block的结构体中可知,block同样也有一个isa指针,所以block也是一个类,它的类型包括:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock
3、一个int变量被 __block 修饰与否的区别?block的变量截获?

没有被__block修饰的int,block体中对这个变量的引用是值拷贝,在block中是不能被修改的。

通过__block修饰的int,block体中对这个变量的引用是指针拷贝,它会生成一个结构体,复制这个变量的指针引用,从而达到可以修改变量的作用。

关于block的变量截获:

block会将block体内引用外部变量的变量进行拷贝,将其拷贝到block的数据结构中,从而可以在block体内访问或修改外部变量。

外部变量未被__block修饰时,block数据结构中捕获的是外部变量的值,通过__block修饰时,则捕获的是对外部变量的指针引用。

注意:block内部访问全局变量时,全局变量不会被捕获到block数据结构中。

举个栗子:

未被__block修饰的情况

int param = 1;
int a = param; // 没用__block修饰的时候,block内部捕获的外部变量
[self updateInt:a];
NSLog(@"----:%@", @(param));// 这里输出:1

// 没用__block修饰的时候,block内部实现如下
- (void)updateInt:(int)a{
    a = 2;// 此时对外部变量修改是无效的
}

被__block修饰的情况

int param = 1;
int *a = &param; // 用__block修饰的时候,block内部捕获的外部变量,是外部变量的指针
[self updateInt:a];
NSLog(@"----:%@", @(param));// 这里输出:2


// 用__block修饰的时候,block内部实现如下
- (void)updateInt:(int *)a{
    *a = 2;// 此时对外部变量修改是有效的
}

参考这篇文章

4、block在修改NSMutableArray,需不需要添加__block?
  • 如果修改的是NSMutableArray的存储内容的话,是不需要添加__block修饰的。
  • 如果修改的是NSMutableArray对象的本身,那必须添加__block修饰。

参考block的变量捕获。

5、block怎么进行内存管理的?

block按照内存分布,分三种类型:全局内存中的block、栈内存中的block、堆内存中的block。
在MRC和ARC下block的分布情况不一样
MRC下:
当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。
当block内部引用了外部的非全局变量的时候,该block是在栈内存中的。
当栈中的block进行copy操作时,会将block拷贝到堆内存中。
通过__block修饰的变量,不会对其应用计数+1,不会造成循环引用。
ARC下:
当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。
当block内部引用了外部的非全局变量的时候,该block是在堆内存中的。
也就是说,ARC下只存在全局block和堆block。
通过__block修饰的变量,在block内部依然会对其引用计数+1,可能会造成循环引用。
通过__weak修饰的变量,在block内部不会对其引用计数+1,不会造成循环引用。
参考这篇文章

6、block可以用strong修饰吗?

在MRC环境中,是不可以的,strong修饰符会对修饰的变量进行retain操作,这样并不会将栈中的block拷贝到堆内存中,而执行的block是在堆内存中,所以用strong修饰的block会导致在执行的时候因为错误的内存地址,导致闪退。

在ARC环境中,是可以的,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作。

7、解决循环引用时为什么要用__strong、__weak修饰?

__weak修饰的变量,不会出现引用计数+1,也就不会造成block强持有外部变量,这样也就不会出现循环引用的问题了。

但是,我们的block内部执行的代码中,有可能是一个异步操作,或者延迟操作,此时引用的外部变量可能会变成nil,导致意想不到的问题,而我们在block内部通过__strong修饰这个变量时,block会在执行过程中强持有这个变量,此时这个变量也就不会出现nil的情况,当block执行完成后,这个变量也就会随之释放了。

8、block发生copy时机?

一般情况在ARC环境中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作。

  • block作为方法或函数的返回值时,编译器会自动完成copy操作。
  • 当block赋值给通过strong或copy修饰的id或block类型的成员变量时。
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。
9、Block访问对象类型的auto变量时,在ARC和MRC下有什么区别?

首先我们知道,在ARC下,栈区创建的block会自动copy到堆区;而MRC下,就不会自动拷贝了,需要我们手动调用copy函数。
我们再说说block的copy操作,当block从栈区copy到堆区的过程中,也会对block内部访问的外部变量进行处理,它会调用Block_object_assign函数对变量进行处理,根据外部变量是strong还会weak对block内部捕获的变量进行引用计数+1或-1,从而达到强引用或弱引用的作用。
因此
在ARC下,由于block被自动copy到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个block,就会形成循环引用。
在MRC下,由于访问的外部变量是auto修饰的,所以这个block属于栈区的,如果不对block手动进行copy操作,在运行完block的定义代码段后,block就会被释放,而由于没有进行copy操作,所以这个变量也不会经过Block_object_assign处理,也就不会对变量强引用。

简单说就是:
ARC下会对这个对象强引用,MRC下不会。
参考这篇文章

要得到你必须要付出,要付出你还要学会坚持,如果你真的觉得很难,那你就放弃,但是你放弃了就不要抱怨,我觉得人生就是这样,世界真的是平等的,每个人都要通过自己的努力,去决定自己生活的样子。

推荐👇:

  • 面试题持续整理更新中,需要拿到第一手大厂面试题及答案文档可以添加 iOS进阶学习交流群:789143298 !结实人脉、讨论技术你想要的这里都有!