以100道面试题构建自己的iOS开发体系

256 阅读31分钟

目录

  • 一、设计原则、设计模式

  • 二、内存管理

  • 三、多线程

  • 四、Block

  • 五、Runtime

  • 六、Runloop

  • 七、KVO

  • 八、KVC

  • 九、Category

  • 十、网络

  • 十一、UI

  • 十二、其他

  • 十三、OC对象相关

一、设计原则、设计模式

1、六大设计基本原则
  • 单一职责原则 (SRP, Single Responsibility Principle)

    定义:一个类只负责一件事 优点:类的复杂度降低、可读性增强、易维护、变更引起的风险降低 应用:系统提供的UIView和CALayer的关系:UIView负责时间传递、事件响应;CALayer负责动画及展示

  • 开闭原则(OCP, Open-Close Principle)

    定义:对修改关闭、对扩展开放

    • 设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类的代码

    优点:灵活、稳定(不需修改内部代码,使得被破坏的程度大大下降) 关键:抽象化

    使用:

    • 我们可以把把行为添加到一个协议中,使用时遵守这个协议即可。
    • 添加类目(Category)方式创建
  • 里氏替换原则 (LSP,Liskov Substitution Principle)

    定义:所有引用父类的地方必须能透明地使用其子类的对象。

    • 通俗点说就是,父类可以被子类无缝替换,且原有功能不受任何影响

    优点:

    • 代码共享,减少创建类的工作量,每个子类都拥有父类的所有属性和方法
    • 提高代码的可重用性、扩张性,项目的开放性

    缺点:程序的可移植性降低,增加了对象间的耦合性

  • 依赖倒置原则(DIP, Dependence Inversion Principle)

    定义:抽象不应该依赖于具体实现,具体实现可以依赖于抽象 核心思想:面向接口编程

    优点:代码结构清晰,维护容易 实例:平时我们使用 protocol 匿名对象模式就是依赖倒置原则的最好体现

  • 接口隔离原则(ISP, Interface Segregation Principle)

    定义:客户端不应该依赖它不需要的接口

    • 使用多个专门的协议、而不是一个庞大臃肿的协议。
    • 协议中的方法应当尽量少

    例:UITableViewDataSource、UITableViewDelegate 优点:解耦、增强可读性、可扩展性、可维护性

  • 迪米特法则(LOD, Law Of Demeter) / 最小知道原则 (LKP,Least Knowledge Principle)

    定义:一个对象应该对其他对象有尽可能少的了解。

    • 也就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。

    迪米特法则应用:

    • 外观模式(Facade)
    • 中介者模式(Mediator)
    • 匿名对象

    优点:使对象之间的耦合降到最底,从而使得类具有很好的可读性和可维护性。

特点总结

  • 单一职责原则主要说明:类的职责要单一
  • 里氏替换原则强调:不要破坏继承体系
  • 依赖倒置原则描述要:面向接口编程
  • 接口隔离原则讲解:设计接口的时候要精简
  • 迪米特法则告诉我们:要降低耦合
  • 开闭原则讲述的是:对扩展开放,对修改关闭
  • 设计模式

    TODO(待填充);⌛️⌛️⌛️⌛️⌛️

二、内存管理

规则

  • 在iOS中,使用 “引用计数” 来管理OC对象的内存
  • 新创建的OC对象,引用计数是1;
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 当引用计数减为0,OC对象就会销毁,释放占用的内存空间
  • 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用release或者aoturelease释放
2、引用计数怎么存储?
- 可以直接存储在isa指针中
- 如果不够存储的话,会存储在SideTable结构体的refcnts散列表中

struct SideTable {
    spinlock_t stock;
    RefcountMap refcnts; // 存放着对象引用计数的散列表
    weak_table_t weak_table;
}
3、ARC具体为引用计数做了哪些工作?
- 编译阶段自动添加代码

ARC是LLVM编译器和Runtime系统相互协作的一个结果
- 编译器帮我们实现内存管理相关的代码
- Runtime在程序运行过程中处理弱引用
4、深拷贝与浅拷贝
  • iOS 图文并茂的带你了解深拷贝与浅拷贝

  • 深刻理解iOS中的“深拷贝”和“浅拷贝”

  • iOS copy和mutableCopy 整理

    概念:

    • 深拷贝:内容拷贝,产生新的对象
    • 浅拷贝:指针拷贝,没有产生新的对象,原对象的引用计数+1
    • 完全拷贝:深拷贝的一种,能拷贝多层内容(使用归解档技术)

    执行结果:

    • copy:不可变拷贝,产生不可变副本

    • mutableCopy:可变拷贝,产生可变副本

    准则:不可变对象的copy方法是浅拷贝,其余都是深拷贝🚩🚩🚩🚩🚩 原因:

    • 它是不可变对象,没有必要拷贝一份出来,指向同一块地址还节省内存

    • 不可变对象调用copy返回他本身,不可变对象copy就相当于是retain1、对象的拷贝

    • 遵守协议(<NSCopying, NSMutableCopying>)

    • 实现协议方法

    • (id)copyWithZone:(NSZone *)zone { Person *person = [Person allocWithZone:zone]; person.name = self.name; return person; }

    • (id)mutableCopyWithZone:(NSZone *)zone { Person *person = [Person allocWithZone:zone]; person.name = self.name; return person; }

    2、集合对象的拷贝

    • 对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,只能算是单层深复制
    • 即虽然新开辟了内存地址,但是存放在内存上的值(也就是数组里的元素仍然之乡员数组元素值,并没有另外复制一份),这就叫做单层深复制
    • 对于集合类的对象如何实现每一层都深拷贝呢?(1、initWithArray:copyItems、2、归档解档技术)

    #import <Foundation/Foundation.h>

    @interface Person : NSObject

    @property (nonatomic, copy) NSString *name;

    @end

    #import "Person.h"

    @implementation Person

    • (instancetype)initWithCoder:(NSCoder *)aDecoder { self.name = [aDecoder decodeObjectForKey:@"name"]; return self; }

    • (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.name forKey:@"name"]; }

    @end

归档和解档的概念补充:
有时存在这样的需求,即将程序中使用的多个对象及其属性值,以及它们的相互关系保存到文件中,或者发送给另外的进程。为了实现此功能,foundation框架中,可以把相互关联的多个对象归档为二进制文件,而且还能将对象的关系从二进制文件中还原出来。

5、weak指针实现原理,SideTable的结构是什么样?
  • iOS 中 weak 的实现原理

  • iOS 底层解析weak的实现原理

  • weak的生命周期:具体实现方法

  • SideTable结构

    1、常用知识点:

    • 所引用对象的计数器不会+1,并在引用对象被释放的时候自动被设置为nil
    • 通常用于解决循环引用问题

    2、weak指针实现原理

    • Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。

    • weak表其实就是一个哈希表,key:对象的内存地址;value:指向该对象的所有弱引用的指针

    • 当对象销毁的时候,通过对象的地址值,取出对象的弱引用表,将表里面的弱引用清除

    3、为什么弱引用不会导致循环引用?

    • 没有增加引用计数

    4、SideTable的结构是什么样的? struct SideTable { // 保证原子操作的自旋锁 spinlock_t slock; // 引用计数的 hash 表 RefcountMap refcnts; // weak 引用全局 hash 表 weak_table_t weak_table; };

    5、weak属性如何自动置nil的? 具体到如何查找的? TODO(待填充);⌛️⌛️⌛️⌛️⌛️

6、自动释放池相关
  • 黑幕背后的Autorelease

    1、以下代码输出什么?会有什么问题?
    
    
    for (int i = 0; i < 1000000; i ++) {
        NSString *string = @"Abc";
        string = [string lowercaseString];
        // string = [string stringByAppendingString:@"xyz"];
        string = [string stringByAppendingFormat:@"xyz"];
        NSLog(@"%d-%@", i, string);
    }
    

    问题解析:

    • 每执行一次循环,就会有一个string加到当前NSRunloop中的自动释放池中
    • 只有当自动释放池被release的时候,自动释放池中的标示了autorelease的这些数据所占用的内存空间才能被释放掉
    • 当someLargeNumber大到一定程度时,内存空间将被耗尽而没有被释放掉,所以就出现了内存溢出的现象。

    解决方案:在循环里面加个自动释放池 for (int i = 0; i < 1000000; i ++) { @autoreleasepool { NSString *string = @"Abc"; string = [string lowercaseString]; string = [string stringByAppendingFormat:@"xyz"]; NSLog(@"%d-%@", i, string); } }

    2、自动释放池底层结构:

    • AutoreleasPool是通过以AutoreleasePoolPage为结点的 “双向链表” 来实现的

    3、AutoreleasPool运行的三个过程:

    • objc_autoreleasePoolPush()
    • [objc autorelease]
    • objc_autoreleasePoolPop(void *)

    objc_autoreleasePoolPush()

    • 调用的AutoreleasePoolPage的push函数
    • 一个push操作其实就是创建一个新的Autoreleasepool
    • 对应AutoreleasePoolPage的具体实现就是往AutoreleasePoolPage中的next位置插入一个 POOL_SENTINEL
    • 并且返回插入的 POOL_SENTINEL 的内存地址。这个地址也就是我们前面提到的 pool token
    • 在执行 pop 操作的时候作为函数的入参

    push 函数通过调用 autoreleaseFast 函数来执行具体的插入操作 autoreleaseFast 函数在执行具体的插入操作时三种情况不同的处理

    • 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;

    • 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;

    • 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中

    objc_autoreleasePoolPop(void *) 函数本质

    • 就是是调用的 AutoreleasePoolPage 的 pop 函数

    • pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,即 pool token 。

    • 当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。

    • 直到 pool token 所在 page 的 next 指向 pool token 为止。

    TODO(待填充);⌛️⌛️⌛️⌛️⌛️ 4、autoreleasepool和线程的关系?

7、Copy、Strong、Weak、Assign的区别?
assign
- 用于对基本数据类型进行赋值操作,不更改引用计数
- 也可以用来修饰对象,但是被assign修饰的对象在释放后,指针的地址还是存在的,指针并没有被置为nil,成为野指针
- 之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。

weak:
- 修饰Object类型,修饰的对象在释放后,指针地址会被置为nil,是一种弱引用
- 在ARC环境下,为避免循环引用,往往会把delegate属性用weak修饰
- weakstrong不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。

strong:
- ARC下的strong等同于MRC下的retain都会把对象引用计数加1

copy:
- 会在内存里拷贝一份对象,两个指针指向不同的内存地址。
- 一般用来修饰NSString等有对应可变类型的对象,因为他们有可能和对应的可变类型(NSMutableString)之间进行赋值操作,为确保可变对象变化时,对象中的字符串不被修改 ,应该在设置属性时拷贝一份。
- 而若用strong修饰,如果可变对象变化,对象中的字符串属性也会跟着变化。


1、block属性为什么需要用copy来修饰?
- 因为在MRC下,block在创建的时候,它的内存是分配在栈(stack)上的,而不是在堆(heap)上,可能被随时回收。
- 他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。
- 通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。
- 在ARC下写不写都行,编译器会自动对block进行copy操作。


2、代理为什么使用weak修饰?
- weak指明该对象并不负责保持delegate这个对象,delegate的销毁由外部控制
- 如果用strong修饰,强引用后外界不能销毁delegate对象,会导致循环引用


3、为什么NSMutableArray一般不用copy修饰?
- (void)setData:(NSMutableArray *)data {
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}
拷贝完成后:可变数组->不可变数组,在外操作时(添加、删除等)会存在问题


4、说到野指针了,什么是“僵尸对象”?
#[iOS-野指针与僵尸对象](https://www.cnblogs.com/junhuawang/p/9213093.html)⏰⏰⏰⏰⏰

- 一个OC对象引用计数为0被释放后就变成僵尸对象,僵尸对象的内存已经被系统回收
- 虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用
- 它的内存是随时可能被别的对象申请而占用的
8、- (void)dealloc底层执行了什么?
  • dealloc底层深入(dealloc做了什么,为什么弱引用不会导致循环引用)

    • (void)dealloc { _objc_rootDealloc(self); }

    void _objc_rootDealloc(id obj) { ASSERT(obj); obj->rootDealloc(); }

    inline void objc_object::rootDealloc() { if (isTaggedPointer()) return; // fixme necessary?

    if (fastpath(isa.nonpointer  &&          // 无优化过isa指针
                 !isa.weakly_referenced  &&  // 无弱引用
                 !isa.has_assoc  &&          // 无关联对象
                 !isa.has_cxx_dtor  &&       // 无cxx析构函数
                 !isa.has_sidetable_rc)) {   // 不存在引用计数器是否过大无法存储在isa中(使用 sidetable 来存储引用计数)
        // 直接释放
        assert(!sidetable_present());
        free(this);
    } else {
        // 下一步
        object_dispose((id)this);
    }
    

    }

    // 如果不能快速释放,则调用 object_dispose()方法,做下一步的处理 static id _object_dispose(id anObject) { if (anObject==nil) return nil;

    objc_destructInstance(anObject);
    
    anObject->initIsa(_objc_getFreedObjectClass ());
    
    free(anObject);
    return nil;
    

    }

    void *objc_destructInstance(id obj) { if (obj) { // Read all of the flags at once for performance. bool cxx = obj->hasCxxDtor(); // 是否存在析构函数 bool assoc = obj->hasAssociatedObjects(); // 是否有关联对象

        // This order is important.
        if (cxx) object_cxxDestruct(obj);           // 销毁成员变量
        if (assoc) _object_remove_assocations(obj); // 释放动态绑定的对象
        obj->clearDeallocating();
    }
    return obj;
    

    }

    /*

    • clearDeallocating一共做了两件事
    • 1、将对象弱引用表清空,即将弱引用该对象的指针置为nil
    • 2、清空引用计数表
      • 当一个对象的引用计数值过大(超过255)时,引用计数会存储在一个叫 SideTable 的属性中
      • 此时isa的 has_sidetable_rc 值为1,这就是为什么弱引用不会导致循环引用的原因 */ inline void objc_object::clearDeallocating() { if (slowpath(!isa.nonpointer)) { // Slow path for raw pointer isa. sidetable_clearDeallocating(); } else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) { // Slow path for non-pointer isa with weak refs and/or side table data. clearDeallocating_slow(); } assert(!sidetable_present()); }

三、多线程

9、多线程 - GCD相关
  • iOS 多线程:『GCD』详尽总结 - [行走少年郎]

  • GCD中队列与任务嵌套的组合测试

  • iOS gcd线程死锁问题

  • GCD信号量-dispatch_semaphore_t

    GCD核心概念:「任务」、「队列」

    1、任务:

    • 概念:指操作,线程中执行的那段代码,GCD主要放在block中;
    • 执行任务的方式:「同步执行」、「异步执行」;
    • 区别:是否等待队列的任务执行结束,是否具备开启新县城的能力;

    同步执行(sync)

    • 同步添加任务到指定队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行
    • 只能在当前线程中执行任务,不具备开启新线程的能力

    异步执行(async)

    • 异步添加任务到指定队列中,不会做任何等待,可以继续执行任务

    • 可以在新的线程中执行任务,具备开启新县城的能力

    • ⚠️异步执行虽然具有开启新线程的能力,但不一定开启新线程。(与任务指定的队列类型有关)

    2、队列(Dispatch Queue)

    • 概念:执行任务的等待队列,即用来存放任务的队列
    • 结构:特殊的线性表,采用FIFO(先进先出)原则。即每读取一个任务,则从队列中释放一个任务

    串行队列:(Serial Dispatch Queue)

    • 每次只有一个任务被执行,任务依次执行(只开启一个线程,一个任务执行完成后,再执行下一个任务)

    并发队列:(Concurrent Dispatch Queue)

    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

    • ⚠️并发队列的「并发」功能只有在异步(dispatch_async)方法下才有效

    3、GCD使用步骤

    • 创建一个队列(串行队列/并发队列)
    • 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行/异步执行)

    4、死锁条件:

    • 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列。

    面试题一、打印顺序 NSLog(@"执行任务1"); dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.1", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.2", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"执行任务2"); dispatch_sync(queue2, ^{ NSLog(@"执行任务3"); }); NSLog(@"执行任务4"); }); NSLog(@"执行任务5");

    2021-03-01 16:47:46.122744+0800 ZF_Beta[17625:344152] 执行任务1 2021-03-01 16:47:46.122977+0800 ZF_Beta[17625:344152] 执行任务5 2021-03-01 16:47:46.122984+0800 ZF_Beta[17625:344229] 执行任务2 2021-03-01 16:47:46.123171+0800 ZF_Beta[17625:344229] 执行任务3 2021-03-01 16:47:46.123300+0800 ZF_Beta[17625:344229] 执行任务4

    dispatch_queue_t ser = dispatch_queue_create("ser", DISPATCH_QUEUE_SERIAL); NSLog(@"1"); dispatch_async(ser, ^{ NSLog(@"2"); }); NSLog(@"3"); dispatch_sync(ser, ^{ NSLog(@"4"); }); NSLog(@"5");

    2021-02-26 11:25:15.703849+0800 ZF_Beta[6156:123418] 1 2021-02-26 11:25:15.704053+0800 ZF_Beta[6156:123418] 3 2021-02-26 11:25:15.704062+0800 ZF_Beta[6156:123698] 2 2021-02-26 11:25:15.704231+0800 ZF_Beta[6156:123418] 4 2021-02-26 11:25:15.704311+0800 ZF_Beta[6156:123418] 5

    • (void)viewDidLoad { [self performSelector:@selector(test3) withObject:nil afterDelay:0]; dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"---111"); }); NSLog(@"---333"); }

    • (void)test3 { NSLog(@"---222"); }

    ---333 ---111 ---222

    • (void)viewDidLoad { [self performSelector:@selector(test1) withObject:nil afterDelay:0]; [self performSelector:@selector(test2) withObject:nil]; dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"===3"); }); [UIView animateWithDuration:10 animations:^{ NSLog(@"===4"); }]; }

    • (void)test1 { NSLog(@"===1"); }

    • (void)test2 { NSLog(@"===2"); }

    2021-03-04 17:41:03.759310+0800 property[25604:424718] ===2 2021-03-04 17:41:03.759642+0800 property[25604:424718] ===4 2021-03-04 17:41:03.788454+0800 property[25604:424718] ===3 2021-03-04 17:41:03.789335+0800 property[25604:424718] ===1

    面试题二、如何打造线程安全的NSMutableArray?

    • 线程锁:使用线程锁在对数组读写时候加锁

    • 派发队列: 《Effective Objective 2.0》中41条提出的观点,串行同步:将读取和写入都安排在同一个队列里,可保证数据同步。

    面试题三、如何异步下载多张小图最后合成一张大图? dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ /*加载图片1 */ }); dispatch_group_async(group, queue, ^{ /*加载图片2 */ }); dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 合并图片 });

    面试题四、什么是线程安全?

    • 多线程操作过程中往往都是多个线程并发执行的,因此同一个资源可能被多个线程同时访问,造成资源抢夺。

    • 线程安全就是多条线程同时访问一段代码,不会造成数据混乱的情况

    面试题五、如何设置常驻线程?

    面试题六、在异步线程发送通知,在主线程接收通知。会不会有什么问题?

    面试题七、GCD线程是如何调度的

    面试题八、如何实现多个任务执行完后,再统一处理?

    • 同步阻塞
    • 栅栏函数
    • 线程组

    ⚠️基于runloop的线程保活、销毁与通信:www.jianshu.com/p/4d5b6fc33…

11、线程和线程之间如何通信?
  • iOS开发多线程篇—线程间的通信

    线程通信的表现:

    • 1个线程传递数据给另1个线程

    • 在1个线程中执行完特定任务后,转到另1个线程继续执行任务

    线程间通信常用方法:

    • (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

    • (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

    例:

    • (void)viewDidLoad { [super viewDidLoad]; }

    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 在子线程中调用download方法下载图片 [self performSelectorInBackground:@selector(download) withObject:nil]; }

    • (void)download { // 1.根据URL网络中下载图片 NSURL *urlstr=[NSURL URLWithString:@"fdsf"];

      // 2、把图片转换为二进制的数据, 这一行操作会比较耗时 NSData *data=[NSData dataWithContentsOfURL:urlstr];

      // 3、把数据转换成图片 UIImage *image=[UIImage imageWithData:data];

      // 4、回到主线程中设置图片 [self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:NO]; }

    //设置显示图片

    • (void)settingImage:(UIImage *)image { self.iconView.image=image; }
12、谈谈atomic的实现机制,为什么不能保证绝对线程安全?
实现机制
- 编译器自动生成getter/setter方法中添加锁保证线程安全

为什么不能保证绝对安全?
- 在getter/setter中加锁,仅保证存取时线程安全,不会让你拿到一个崩溃的值
- 无法保证对容器的修改是线程安全的,例:假设属性是可变容器(@property (atomic) NSMutableArray *array)时
- 重写getter/setter方法时,只能依靠自己在getter/setter中保证线程安全

- (void)setCurrentImage:(UIImage *)currentImage {
    if (_currentImage != currentImage) {
        [_currentImage release];
        _currentImage = [currentImage retain];
    }
}

- (UIImage *)currentImage {
    return _currentImage;
}

- (void)setCurrentImage:(UIImage *)currentImage {
    @synchronized(self) {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];
        }
    }
}

- (UIImage *)currentImage {
    @synchronized(self) {
        return _currentImage;
    }
}
13、进程和线程的区别
区别:
- 一个线程只能属于一个进程.
- 一个进程可以有多个线程,但至少有一个线程。
- 线程是操作系统可识别的最小执行和调度单位。

资源分配:
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源
- 同一进程中的多个线程共享代码段、数据段、扩展段
- 但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量
14、Notification与线程相关
  • iOS Notification 与多线程

  • Notification与多线程

  • 官方文档

    官方文档:

    • 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。

    • 换句话说就是在哪个线程发送通知,就在哪个线程接受通知。

    如何实现在不同线程中post和转发一个Notification?

    重定向的实现思路: 1、自定义一个通知队列(用数组类型),让它去维护那些我们需要重定向的Notification 2、我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程 3、如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification 4、指定的线程在收到信号后,将Notification从队列中移除,并进行处理

    // 查看一下这个api

    • (id )addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
15、dispatch_once底层实现
16、线程锁相关
  • iOS 开发中的八种锁

  • iOS开发中的11种锁以及性能对比

  • iOS 中常见的几种锁-代码示例

    线程锁的作用:

    • 我们在使用多线程的时候,多个线程可能会访问同一块资源,就很容易引发数据错乱和数据安全等问题

    • 这时候就需要我们保证每次只有一个线程访问这一块资源

    线程锁类型:

    • 互斥锁

    • 自旋锁

    • 信号量

    • 递归锁

    • atomic

    1、互斥锁

    • 标记用来保证在任一时刻,只能有一个线程访问对象

    • NSLock

    • @synchronized (self)

    2、自旋锁

    • OSSpinLock(YYKit作者有一篇文章写它不安全,可以自己研究一下)

    • os_unfair_lock

    3、信号量(Semaphore - dispatch_semaphore_t)

    • 多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用

    • 在进入一个关键代码段之前,线程必须获取一个信号量;关键代码段完成后,该线程必须释放信号量

    • 其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量

    4、递归锁(NSRecursiveLock)

    • 同一个线程可以多次加锁,不会造成死锁

    5、atomic

    • atomic 修饰的对象,系统会保证在其自动生成的 getter/setter 方法中的操作是完整的,不受其他线程的影响

    线程不安全:

    • 如果有另一个线程同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制
    • 这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作

四、Block

17、block相关
1、block本质是什么?
- block是将函数及其执行上下文封装起来的对象


2、关于block的截获特性,你是否有了解?block的截获变量特性是怎样的?
变量捕获机制分析:
- 对于“基本数据类型”的“局部变量”截获其值
- 对于“对象”类型的局部变量“连同所有权修饰符”一起截获
- 以“指针形式”截获局部静态变量(指针传递)
- 不截获全局变量、静态全局变量(直接访问)

改外部变量必要条件
- 将auto从栈copy到堆
原因:栈中内存管理是由系统管理,出了作用域就会被回收,堆中才是可以由程序员管理


3、对栈上的block进行copy之后,假如在mrc环境下,内存是否回泄漏?
- copy操作之后,堆上的block没有额外的成员变量指向它,正如我们alloc对象后,没有进行release,造成内存泄漏


4、面试题:请思考,这段代码有问题么?
{
    __block MCBlock *blockSelf = self;
    _blk = ^int(int num){
        return num * blockSelf.var;
    }
    _blk(3);
}
- 在MRC下,不会产生循环引用
- 在ARC下,会产生循环引用,造成内存泄漏


5、为什么block会产生循环引用?
- 如果当前block对当前对象的某一成员变量进行截获,block会对当前对象有一个强引用
- 而当前block由于当前对象对其有一个强引用,产生了一个自循环引用的一个循环引用的问题


6、Block不允许修改外部变量的值
原因:
- block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域---改变了作用域。
- 在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。
- 比如想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。

- 所以 Apple 在编译器层面做了限制,如果在 block 内部试图修改 auto 变量(无修饰符),那么直接编译报错。
- 可以把编译器的这种行为理解为:对 block 内部捕获到的 auto 变量设置为只读属性---不允许直接修改。


7、如何实现对外部变量的捕获?
- 将变量设置为全局变量。原理:block内外可直接访问全局变量
- 加 static (放在静态存储区/全局初始化区)。原理是block内部对外部auto变量进行指针捕获
- 最优解:使用__block 关键字


8、__block
- 将auto变量封装为结构体(对象),在结构体内部新建一个同名的auto变量
- block内截获该结构体的指针
- 在block中使用自动变量时,使用指针指向的结构体中的自动变量

__block int var = 10;
void(^blk)(void) = ^{
    var = 20;
};
blk();

转换后的代码:

struct __Block_byref_var_0 {
    void *__isa;
    __Block_byref_var_0 *__forwarding;
    int __flags;
    int __size;
    int var; // 10 => 20 该结构体持有相当于原来自动变量的成员变量
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_var_0 *var; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};


9、block在修改NSMutableArray需不需要添加__block
- 不需要
- 当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。
- 所以,在block里面对指针指向内容做的修改,在block外面也一样生效。


10、block是如何捕获局部变量的?
- block捕获外界变量时,在内部会自动生成同一个属性来保存


11、UIView动画中block回调里self要不要弱引用?
- 不需要,它不会造成循环引用,因为它是类方法。
- 之所以需要弱引用本身,是因为怕对象之间产生循环引用,当前控制器不可能强引用一个类,所以循环无法形成。


12、block里面会不会存在self为空的情况(weak strong的原理)?

__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{    __strong typeof(weakself) strongself = weakself;    [strongself.dataSource reloadDataWithCompletion:nil];
}];

- 有时候weakSelf在block里在执行reloadDataWithCompletion还存在
- 但在执行reloadDataWithCompletion前,可能会被释放了
- 为了保证self在block执行过程里一直存在,对他强引用strongSelf


13、__block与__weak的区别
- _block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型
- __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)
- __block对象可以在block中被重新赋值,__weak不可以。 


14、多层block嵌套如何使用weakSelf?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{    __strong typeof(weakself) strongself = weakself;    __weak typeof(self) weakSelf2 = strongself;    [strongself.dataSource reloadDataWithCompletion:^(BOOL result) {        __strong typeof(self) strongSelf2 = weakSelf2;    }];
}];


15、Masonry对于block内部引用self会不会造成循环引用?
- 不会
- 这个block没有copy,是在栈上,使用完直接释放了,

- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
18、 代理、Block利弊
- 与委托代理模式的代码相比,用block写出的代码更为整洁

代理优点:
- 代理语法清晰,可读性高,易于维护
- 它减少代码耦合性,使事件监听与事件处理分离
- 一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高

代理缺点:
- 实现代理的过程较繁琐
- 跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱
- 当多个对象同时传值时不易区分,导致代理易用性大大降低


block优点:
- 语法简洁,代码可读性和维护性较高
- 配合GCD优秀的解决多线程问题
block缺点:
- Block中得代码将自动进行一次retain操作,容易造成内存泄漏
- Block内默认引用为强引用,容易造成循环应用

运行成本:
delegate运行成本低,block的运行成本高
- block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是假引用技术,使用完block置nil才会消除
- delegate只是保存了一个对象的指针,直接回调,没有额外的消耗。就像c的函数指针,只多了一个查表动作
19、有哪些情况会出现内存泄漏。
- block循环引用
- delegate循环引用问题
- NSTimer循环引用
- 地图类处理

- 线程保活target:self
20、__weak来解决block中的循环引用,还有别的方法吗。

五、Runtime

21、以下方法打印什么
  • iOS Runtime面试题([self class] 与 [super class])

    @implementation Son : Father

    • (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; }

    @end

    这两个都打印出来的是:Son.

    • [self class] 会先在当前类的方法列表中查找class这个方法

    • [super class] 会先到父类中去查找class方法

    • 两者在找不到的时候,都会继续向祖先类查询class方法,最终到NSObject类

    • NSObject中class的实现

    • (Class)class { return object_getClass(self); }

22、Runtime如何通过selector找到对应的IMP地址?IMP和SEL关系是?
23、Runtime的相关术语
SEL、id、Class、Method、IMP、Cache、Property

- 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
- 为什么要设计metaclass
- class_copyIvarList & class_copyPropertyList区别
- class_rw_t 和 class_ro_t 的区别
23、交互两个方法的现实有什么风险?
  • Runtime交换类方法和实例方法

    • class_replaceMethod
    • method_exchangeImplementations
    • class_getInstanceMethod

    个人经验总结:

    • 当我们写的类没有继承的关系的时候,俩种方法都没什么问题

    • 当有继承关系又不确定方法实现没实现,最好用class_replaceMethod方法

    补充:在美图秀秀面试时,一个面试官问到方法交互,我说就是交换两个放的IMP指针指向, 他问还有么?不知道还有什么,现在想起来,他应该是想问从isa指针到方法查找,再到根据SEL查找IMP过程吧

    • 多次hook方法会存在什么问题?

    TODO(待填充);⌛️⌛️⌛️⌛️⌛️

24、对象关联底层数据结构
  • iOS关联对象技术原理

    通过 runtime 的源码得知:

    • 关联属性并没有添加到 category_t(分类)里边
    • 运行时也不会合并到元类对象里边
    • 而是存储在一个全局的AssociationsManager里边

    #import <objc/runtime.h> // 添加 objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>) // 获取 objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>) // 移除 objc_removeAssociatedObjects(<#id _Nonnull object#>)

    • 关联对象的应用?系统如何实现关联对象的
    • 关联对象的如何进行内存管理的?关联对象如何实现weak属性
    • 关联的对象,需要在主对象dealloc的时候释放么?

    被关联的对象的生命周期内要比对象本身释放晚很多, 它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。

层级关系

25、消息转发流程,向一个nil对象发送消息会怎样
  • Objc中向一个nil对象发送消息会怎样

    转发过程(一定要回答出从缓存中查找)

    • 消息发送

    • 动态方法解析

    • 消息转发

    1、消息发送过程 objc_msgSend(receiver, selector)

    • 向一个对象发送消息时,runtime会根据对象的isa指针找到所属类

    • 在该类的方法列表及父类方法列表中寻找方法(缓存)

    • 如果在最顶层父类中依然找不到对应方法,会报 unrecognized selector send to xxx

    2、向一个nil对象发送消息会怎样?

    • objc_msgSend会通过判断self来决定是否发送消息

    • 如果self为nil,那么selector也会为空,直接返回,不会出现问题

    • 但对于[NSNull null]对象发送消息时,是会crash的,因为NSNull类只有一个null方法

    在崩溃前有三次拯救程序崩溃的机会,就是接下来的消息转发

    3、消息转发流程 TODO(待填充);⌛️⌛️⌛️⌛️⌛️

26、performSelector:withObject:afterDelay: 内部大概是怎么实现的,有什么注意事项么?
内部实现:
- 创建一个定时器, 时间结束后系统会使用runtime通过方法名称去方法列表中找到对应的方法实现并调用方法
- Selector本质就是方法名称

注意事项:
- 调用performSelector:withObject:afterDelay:方法时,先判断希望调用的方法是否存在respondsToSelector:
- 这个方法是异步方法,必须在主线程调用,在子线程调用永远不会调用到想调用的方法

六、Runloop

27、RunLoop相关
什么是RunLoop?
- RunLoop 实际上是一个对象
- 这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)
- 从而保持程序的持续运行
- 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能


// 简单的理解为如下代码
int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
        // ......
    } while (running);  // 判断是否需要退出

    return 0;
}


- 讲讲runloop,项目中有用到么?
- runloop内部实现逻辑?
- timer与runloop的关系?
- 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
- runloop是怎么响应用户操作的,具体流程是什么样的?
- 说说runloop的几种状态?
- runloop的mode作用是什么

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            // 睡眠中等待消息
            int message = sleep_and_wait;
            // 处理消息
            retVal = process_message(message);
        } while (retVal == 0)
        return 0;
    }
}

  • runloop内部实现逻辑

    RunLoop运行逻辑图.png

  • 应用范畴

    • 定时器(Timer)、PerformSelect
    • GCD Async Main Queue
    • 事件响应、手势识别、界面刷新
    • 网络请求
    • AutoreleasePool
  • 基本应用

    • 保持程序的持续运行
    • 处理App中的各种事件(比如触摸事件、定时器事件等)
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  • runloop和线程的关系

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
    • RunLoop会在线程结束时销毁
    • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

    /*

    • 从字典中获取,如果没有则直接创建 */ CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFSpinUnlock(&loopsLock); if (!loop) { CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFSpinLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

      if (!loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; }

      // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFSpinUnlock(&loopsLock); CFRelease(newLoop); }

28、 NSTimer相关
1、NSTimer准吗?如果不准的话原因是什么?如何解决?
原因:
- NSTimer的触发时间到的时候,会在RunLoop中被检测一次;
- 如果在这一次的RunLoop中做了耗时的操作,会处于阻塞状态
- 时间超过了定时器的间隔时间,触发时间就会推迟到下一个runloop周期

解决方法:
- 在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作
- 使用CADisplayLink(时钟???)
- 使用GCD定时器


2、使用NSTimer是如何处理循环引用的?
- 使用类方法
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
拓展、如何利用runloop监控卡顿

七、KVO

29、KVO相关

KVO 的 全称Key-Value Observing,俗称“键值监听”,可以用于某个对象属性值的改变

1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)
- 利用runtimeAPI动态生成一个子类(NSKVONotifying_XXXX),并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会动用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey
- 父类原来的setter方法
- didChangeValueForKey
- 内部触发监听器(ObserveValueForKeyPath:ofObject:change:context)

2、如何手动触发KVO
- 手动调用willChangeValueForKey
- 修改成员变量值
- 手动调用didChangeValueForKey

3、直接修改成员变量会触发KVO么
- 不会触发KVO(原因看KVO的本质)

4、object_getClass(self.person) 和 [self.person class];分别打印什么?为什么?
- object_getClass(self.person); -> NSKVONotifying_MJPerson
- [self.person class];          -> MJPerson

- 原因:NSKVONotifying_MJPerson重写底层实现,目的:隐藏动态创建的类,不让用户感知
- (Class)class {
    return [MJPerson class];
}


// 伪代码 Function框架
void _NSSetIntValueForKey(){
    [self willChangeValueForKey:@"age"];
    [self setAge:age];
    [self didChangeValueForKey:@"age"];
}

// 通知监听器
- (void)didChangeValueForKey:(NSString *)key {
   [obser observeValueForKeyPath:key ofObject:self change:nil content:nil];
}

其他:
根据地址打印方法:p (IMP)0X1065....
类对象:  object_getClass(self.person);
原类对象:object_getClass(object_getClass(self.person));

KVO原理

  • 使用kvo什么时候移除监听(dealloc不能移除的情况)?

八、KVC

30、KVC相关

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性

1、通过KVC修改属性会出发KVO么?
- 能触发KVO()
- KVC在修改属性时,会调用willChangeValueForKey:和didChangeValueForKey:方法;


2、KVC的赋值和取值过程是怎样的?原理是什么?
- 见下图

3、使用场景
- 单层字典模型转化:[self.model setValuesForKeysWithDictionary:dict];

- 通过KVC修改未暴露的属性:
UILabel *placeholderLabel=[self.userTextField valueForKeyPath:@"placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];

- 使用valueForKeyPath可以获取数组中的最小值、最大值、平均值、求和
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];

- 数组内部去重
[dataArray valueForKeyPath:@"@distinctUnionOfObjects.self"]


- 数组合并(去重合并:distinctUnionOfArrays.self、直接合并:unionOfArrays.self)
NSArray *temp1 = @[@3, @2, @2, @1];
NSArray *temp2 = @[@3, @4, @5];

NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@distinctUnionOfArrays.self"]);
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@unionOfArrays.self"]);

输出两个数组:( 5, 1, 2, 3, 4 ), ( 3, 2, 2, 1, 3, 4, 5 )。


- 大小写转换(uppercaseString)及 打印字符串长度同样适用(length)
NSArray *array = @[@"name", @"w", @"aa", @"jimsa"];
NSLog(@"%@", [array valueForKeyPath:@"uppercaseString"]);
打印:
(NAME,W,AA,JIMSA)

KVC赋值原理 - setValue:forKey:

- 首先会按照setKey、_setKey的顺序查找方法,找到方法,直接调用方法并赋值;
- 未找到方法,则调用+ (BOOL)accessInstanceVariablesDirectly;
- 若accessInstanceVariablesDirectly方法返回YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常;
- 若accessInstanceVariablesDirectly方法返回NO,则直接抛出异常;

KVC取值原理 - valueForKey:

- 首先会按照getKey、key、isKey、_key的顺序查找方法,找到直接调用取值
- 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出异常;
- 若返回的YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到则取值;
- 找不到则抛出异常;

*