面试题iOS面试题必看face

百度三面被挂掉之后,沉下心来总结,构建自己的iOS开发体系(上)

2021-03-27  本文已影响0人  iOS弗森科

在百度三面被挂掉之后,沉下心来,整理构建自己的开发体系,方便以后查看。 金三银四已经降临,为此提供了找了不少学习方向给大家,也是一些进价方向,希望能帮大家快速提升自己的短板!

本章节:

标:不要浪费美好的年华,做自己觉得对的事情!

目录

2021年【最新iOS开发面试题】感谢观看赠资料:

下载地址:

全新iOS 电子书大全 和 iOS 进阶面试文档

一、设计原则、设计模式

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

定义:对修改关闭、对扩展开放
- 设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类的代码

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

使用:
- 我们可以把把行为添加到一个协议中,使用时遵守这个协议即可。
- 添加类目(Category)方式创建

定义:所有引用父类的地方必须能透明地使用其子类的对象。
- 通俗点说就是,父类可以被子类无缝替换,且原有功能不受任何影响

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

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

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

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

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

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

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

定义:一个对象应该对其他对象有尽可能少的了解。
- 也就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。

迪米特法则应用:
- 外观模式(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、深拷贝与浅拷贝
概念:
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象,原对象的引用计数+1
- 完全拷贝:深拷贝的一种,能拷贝多层内容(使用归解档技术)

执行结果:
- copy:不可变拷贝,产生不可变副本
- mutableCopy:可变拷贝,产生可变副本

准则:不可变对象的copy方法是浅拷贝,其余都是深拷贝🚩🚩🚩🚩🚩
原因:
- 它是不可变对象,没有必要拷贝一份出来,指向同一块地址还节省内存
- 不可变对象调用copy返回他本身,不可变对象copy就相当于是retain

1、对象的拷贝
- 遵守协议(<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 <NSCoding>

@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的结构是什么样?
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、自动释放池相关
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修饰
- weak和strong不同的是:当一个对象不再有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底层执行了什么?
- (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相关
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的线程保活、销毁与通信:https://www.jianshu.com/p/4d5b6fc33519

下一章:百度三面被挂掉之后,沉下心来总结,构建自己的iOS开发体系(下)

小编文章请观看合集

文章来源作者:强子ly

上一篇下一篇

猜你喜欢

热点阅读