Skip to content

Latest commit

 

History

History
executable file
·
610 lines (391 loc) · 25.2 KB

J-Knight面试题解答.md

File metadata and controls

executable file
·
610 lines (391 loc) · 25.2 KB

Interview-Question & Answer

初级篇完结

起初乍一看感觉问题并不是很多,通过总结才发现面试官的准备十分充分,涵盖了很多方面,在总结的过程中,我也等于是复习了一遍。

目前针对初级篇的问题大致总结了一下,我看了中级以及高级的题目,大致分为以下几类

再加上是基础题目里也有很多值得拓展的问题

  • 内存管理
  • 数据持久化
  • 多线程
  • 属性修饰符
  • 内存语义。。。。

关于中高级的问题,我会接下来做仔细的分析,我心里并没有十足的把握,或许上面的回答也是漏洞百出,但是希望各位同行能多多指教,指出我的不足,在此先行谢过。

在整理这篇答案的时候,借鉴了很多网上的资料,很杂,也很难一一列出。

喵神的关于storyBoard那篇

链接在此:再看关于 Storyboard 的一些争论


此篇是根据知名博主 J-Knight 所提供的面试题目,所整理的答案,感谢 J-Knight 的分享,点击查看原文。

另外,我写此文的目的在于和广大的iOS开发者进行沟通交流,里面的内容有自己的理解,也有很大一部分参照网上的解释。很感谢之前的分享者,文末会附上相关的链接。如果在本文有理解不正确的地方,也希望大家多多指正。

面试题分为三个部分,我们先从基础开始。

基础

1. 为什么说Objective-C是一门动态的语言?

其实Objective-C是一门动态语言的用运行时Runtime可以更好地说明,但我看后面还有关于运行时的问题,在此处就先不展开了。

  1. 动态类型:例如“id”类型,动态类型属于弱类型,在运行时才决定消息的接收者
  2. 动态绑定:程序在运行时需要调用什么代码是在运行时决定的,而不是在编译时。
  3. 动态载入:程序在运行时的代码模块以及相关资源是在运行时添加的,而不是启动时就加载所有资源

2.简要概括一下 MVCMVVMMVP三种模式。

MVC

MVC模式所有的模块通信都是单向的(这一点个人持怀疑态度,希望大家提出意见)

  1. View传递指令给Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈

还有一种是Controller直接接受指令

MVP

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。

  1. 各部分之间的通信,都是双向的。
  2. ViewModel 不发生联系,都通过 Presenter 传递。
  3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(data-binding)View的变动,自动反映在 ViewModel,反之亦然。AngularEmber 都采用这种模式。

3.为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?

1.首先,为什么代理要用weak?

其实就是循环引用!!! 我们一般在声明一个协议的时候,会定义一个代理属性,如果代理用的比较溜就会知道,一般都是别的类成为当前协议的代理,也就是说,代理实际是外部的一个类。代理属性的销毁不由当前协议类控制,而是由外部代理者自己控制。 如果在定义代理属性时,使用Strong,外界就无法销毁代理属性,造成循环引用,无法释放。

2.代理的delegate和dataSource有什么区别。

delegatedataSource 常见于UITableViewUICollectionViewdataSource是数据源,决定了显示多少个区域,每个区域显示多少,每行现实的具体内容,头部,尾部视图等。 delegate是交互行为的代理,比如点击取消选中是否高亮等等。

关于这个问题我有一些疑惑,比如delegate里面也有决定头部视图显示什么,尾部视图显示什么的方法,按我的理解应该在DataSource才对,请大家指教。

3.block和代理的区别?

Block是带有局部变量的匿名函数,是一个代码段,Block更面向结果,他适合与状态无关的操作,例如直接返回某些值得时候,就比较适合用Block

delegate回调则更加面向过程,例如执行的回调需要几个不同的步骤,这个时候使用delegate则更为合适

4.属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的?

想深入了解,可以看一下详细的总结 : https://github.com/liberalisman/2018-Interview-Preparation#04-property

1.实质就是 ivar(实例变量)、存取方法(access method = getter + setter)。

@property 的本质.

@property = ivar + getter + setter;
2.属性可以拥有的特质分为四类:
  • 原子性--- nonatomic 特质,在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用自旋锁。请注意,尽管没有名为atomic的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。

  • 读/写权限---readwrite(读写)、readonly (只读)

  • 内存管理语义---assign、strong、 weak、unsafe_unretained、copy

  • 方法名 - getter= 、setter=

3.属性的默认关键字:

在 ARC 下,如果如果修饰的是 Objective-C 对象。

@property (atomic,strong,readwrite) UIView *view;

如果如果修饰的是基本数据类型。

@property (atomic,assign,readwrite) int num;
4.“自动合成”( autosynthesis)

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做**“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter** 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。 也可以在类的实现代码里通过**@synthesize** 语法来指定实例变量的名字.

5.@dynamic

告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。

如果@synthesize@dynamic都没写,那么默认的就是

@syntheszie var = _var;

// @synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
// @dynamic告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。
6.为了搞清属性是怎么实现的,反编译相关的代码,大致生成了五个东西
1. OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
2. setter 与 getter 方法对应的实现函数
3. ivar_list :成员变量列表
4. method_list :方法列表
5. prop_list :属性列表
也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,
在 method_list 中增加 setter 与 getter 方法的描述,
在属性列表中增加一个属性的描述,
然后计算该属性在对象中的偏移量,
然后给出 setter 与 getter 方法对应的实现,
在 setter 方法中从偏移量的位置开始赋值,
在 getter 方法中从偏移量开始取值,
为了能够读取正确字节数,
系统对象偏移量的指针类型进行了类型强转.

5.NSString为什么要用copy关键字,如果用strong会有什么问题?

NSString有可变的子类NSMutableString。因为父类指针可以指向子类,避免NSMutableStringNSString赋值,造成原有的值被无形修改,所以用Copy修饰。

我们修饰 NSString 使用 Copy 关键字。

  • 如果传进来的也是 NSString类型,这时候Copy作为指针拷贝,是浅拷贝,内容不会发生变化。

  • 如果传进来的是 NSMutableString类型,这时候Copy作为内容拷贝,是深拷贝,在内存中新开辟出一块儿新的地址,防止原有的值被改变。


以上是我之前的回答,热心网友对此问题作了完善的补充

NSString 使用 copy 和它的子类并没有关系,而且凡是 NSObject 都有 copy 方法,并不是 NSString 独有。

如下代码:

NSString *string = @"测试数据";
NSString *copyString = [string copy];
NSMutableString *mutableCopyString = [string mutableCopy];
NSMutableString *copyMutableString = [string copy];
NSLog(@"%p,%p,%p,%p", string, copyString, mutableCopyString, copyMutableString);

输出它们的地址,stringcopyString 的地址是相同的,说明它们的指针指向同一个地址,也就是说 copy 是浅拷贝,即指针拷贝mutableCopyString 和其他的地址都不一样,说明新开辟了一块内存空间,也就是和前两个没有任何关系,也就是说 mutableCopy 发生了深拷贝

copyMutableString 和其他的地址也不一样,同 mutableCopyString ,也是发生了深拷贝

对于 copyStringcopyMutableString 同是使用的 copy,但是地址却不一样,是因为苹果对于不可变的对象执行的引用操作,而对于可变对象,相对于之前的不可变对象,那么地址肯定会不一样,所以这个时候就要拷贝一份,和之前的就没有任何关系了。

还有就是属性中的 copy 关键字,如下代码:

@property (nonatomic, copy) NSString *copyString;
@property (nonatomic, strong) NSString *strongString;

对于上面的代码,copyStringstrongStringset 方法中,

-(void)setCopyString:(NSString *)copyString {
_copyString = [copyString copy];  // 调用 copy 方法,所以并不是直接赋值
}
-(void)setStrongString:(NSString *)strongString {
_strongString = strongString;  // 直接引用
}

如果想要外界赋值的值对 string 有影响,那么就用 strong,这样两者相当于还是一个对象,如果在赋值以后不想要外界再对 string 有影响,那么就用 copy。也就是说用 strong 还是 copy 可以根据情况而定。

6.如何令自己所写的对象具有拷贝功能?

简单说就是遵守NSCopying,NSMutableCopying协议

并且实现(id)copyWithZone:(NSZone *)zone(id)mutableCopyWithZone:(NSZone *)zone两个方法即可。

深入了解可看我的其他文章

7.可变集合类不可变集合类copymutablecopy 有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么?

源对象类型 拷贝方式 副本对象类型 是否有新的对象
NSArray Copy NSArray NO
NSMutableArray Copy NSArray YES
NSMutableArray MutableCopy NSMutableArray YES
NSArray MutableCopy NSMutableArray YES

如果是集合内容复制,它的内容复制也分两种,一种是单层复制,一种是完全复制。上表的后三种全都是单层内容复制,只有最外面的容器被复制了,里面存储对象的指针地址不变。

8.为什么IBOutlet修饰的UIView使用weak关键字?

关于IBOutlet修饰的属性究竟是使用strong还是weak,网上的不同意见还是挺多的。

但我认为这可以分为两种情况:

1.如果从storyBoard或者nib拖出来的插座属性storyBoard或者nib所直接拥有的,这个时候应该使用Strong修饰

2.如果是一个storyBoard或者nib子控件再添加子控件,这个时候就应该用weak

这么说可能比较不好理解。

此图控制器的View拖出来的线就是strong。 而如果往View上再次添加子控件的话,拖出来的线就是weak

9.nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?

1.nonatomic和atomic的区别?

atomic-原子性

  • 默认的属性
  • 保证CPU在别的线程来访问这个属性之前,先执行完当前线程
  • 速度较慢,因为要保证整体完成。

nonatomic-非原子性

  • 非默认的属性
  • 线程不安全,如果两个线程同时访问,会出问题
  • 速度快
2.atomic是绝对的线程安全么?如果不是,那应该如何实现?

很遗憾,并不是。。虽然atomic-原子性能保证不同的线程同时访问一个属性的时候,它的Settergetter方法会有序执行,但如果此时有另一个线程调用该属性的Release方法,还是会出问题的,因为atomic-原子性只能管好它的Settergetter方法。

再者开锁是很耗性能的,所以在移动端,一般使用nonatomic,而Mac OS不涉及到性能瓶颈,所在在Mac OS上使用atomic

至于在iOS上保证属性在不同线程间访问的绝对安全,这块儿我暂时没有研究过,希望知道的朋友指教。

10.UICollectionView自定义layout如何实现?

自定义Layout需要实现以下几个步骤。

// 1.collectionView每次需要重新布局(初始, layout 被设置为invalidated ...)的时候会首先调用这个方法prepareLayout()
func prepareLayout()

// 2.然后会调用layoutAttributesForElementsInRect(rect: CGRect)方法获取到rect范围内的cell的所有布局, 这个rect大家可以打印出来看下, 和collectionView的bounds不一样, size可能比collectionView大一些, 这样设计也许是为了缓冲
func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?

// 3.当collectionView的bounds变化的时候会调用shouldInvalidateLayoutForBoundsChange(newBounds: CGRect)这个方法
public func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool

// 4.需要设置collectionView 的滚动范围 collectionViewContentSize()
// 自定义的时候, 必须重写这个方法, 并且返回正确的滚动范围, collectionView才能正常的滚动
public func collectionViewContentSize() -> CGSize

//  5.以下方法, Apple建议我们也重写, 返回正确的自定义对象的布局,因为有时候当collectionView执行一些操作(delete insert reload)等系统会调用这些方法获取布局, 如果没有重写, 可能发生意想不到的效果    
// 自定义cell布局的时候重写
public func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?
// 自定义SupplementaryView的时候重写
public func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?
// 自定义DecorationView的时候重写
public func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?

// 6.这个方法是当collectionView将停止滚动的时候调用,得到最终偏移量。我们可以重写它来实现, collectionView停在指定的位置(比如照片浏览的时候, 你可以通过这个实现居中显示照片...)
public func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint

11.用StoryBoard开发界面有什么弊端?如何避免?

其实关于用StoryBoard还是纯代码的开发方式,争吵声一直都存在,其实我个人并不反感StoryBoard,反而还挺喜欢。开发速度快,如果协调好,可以减轻很多工作量。不过关于StoryBoard这个话题如果展开的话还是比较大,建议大家读一下。喵神最近写的一篇文章,附上原文链接,有异议的话也欢迎大家积极讨论。

12.进程和线程的区别?同步异步的区别?并行和并发的区别?

1.进程和线程的区别?

进程:进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

线程:线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。1个进程要想执行任务,必须得有线程,例如默认就是主线程。

2.同步异步的区别?

同步函数:不具备开线程的能力,只能串行按顺序执行任务

异步函数:具备开线程的能力,但并不是只要是异步函数就会开线程。

3.并行和并发的区别?

并行:并行即同时执行。比如同时开启3条线程分别执行三个不同人物,这些任务执行时同时进行的。

并发:并发指在同一时间里,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。

13.线程间通信?

1.NSThread
// 第一种方式。
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];

// 第二种方式
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
2.GCD
//0.获取一个全局的队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

//1.先开启一个线程,把下载图片的操作放在子线程中处理
dispatch_async(queue, ^{
//2.下载图片
NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/zhidao/pic/item/6a63f6246b600c3320b14bb3184c510fd8f9a185.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
NSLog(@"下载操作所在的线程--%@",[NSThread currentThread]);
//3.回到主线程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
//打印查看当前线程
NSLog(@"刷新UI---%@",[NSThread currentThread]);
});
});

// GCD通过嵌套就可以实现线程间的通信。
3.NSOperationQueue
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];

//2.使用简便方法封装操作并添加到队列中
[queue addOperationWithBlock:^{

//3.在该block中下载图片
NSURL *url = [NSURL URLWithString:@"http://news.51sheyuan.com/uploads/allimg/111001/133442IB-2.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
NSLog(@"下载图片操作--%@",[NSThread currentThread]);

//4.回到主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
NSLog(@"刷新UI操作---%@",[NSThread currentThread]);
}];
}];

14.GCD的一些常用的函数?

1.栅栏函数(控制任务的执行顺序)
dispatch_barrier_async(queue, ^{

NSLog(@"barrier");
});
2.延迟执行(延迟·控制在哪个线程执行)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"---%@",[NSThread currentThread]);
});
3.一次性代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

NSLog(@"-----");
});
4.快速迭代(开多个线程并发完成迭代操作)
dispatch_apply(subpaths.count, queue, ^(size_t index) {
});
5.队列组(同栅栏函数)
dispatch_group_t group = dispatch_group_create();
// 队列组中的任务执行完毕之后,执行该函数
dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);

// 进入群组和离开群组
dispatch_group_enter(group);//执行该函数后,后面异步执行的block会被gruop监听
dispatch_group_leave(group);//异步block中,所有的任务都执行完毕,最后离开群组
//注意:dispatch_group_enter|dispatch_group_leave必须成对使用
6.信号量(并发编程中很有用)

15.如何使用队列来避免资源抢夺?

可以用串行队列或者是同步锁。保证在同一时间内,只有一条线程在访问资源。

16.数据持久化的几个方案

  • plist文件(属性列表)
  • preference(偏好设置)
  • NSKeyedArchiver(归档)
  • SQLite 3 (FMDB)
  • CoreData

在此不展开了,篇幅比较大,详情见我另一篇文章

17.说一下AppDelegate的几个方法?从后台到前台调用了哪些方法?第一次启动调用了哪些方法?从前台到后台调用了哪些方法?

1.应用程序启动,并进行初始化时候调用该方法:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
}

2、应用进入前台并处于活动状态时候调用:

- (void)applicationDidBecomeActive:(UIApplication *)application {}

3、应用从活动状态进入到非活动状态:

- (void)applicationWillResignActive:(UIApplication *)application {}

4、应用进入到后台时候调用的方法:applicationDidEnterBackground:

- (void)applicationDidEnterBackground:(UIApplication *)application {}

5、应用进入到前台时候调用的方法:

- (void)applicationWillEnterForeground:(UIApplication *)application {}

6、应用被终止的状态:

- (void)applicationWillTerminate:(UIApplication *)application {}

18.NSCache优于NSDictionary的几点?

在做缓存时,优先使用NSCache而不是NSDictionary,我们熟悉的框架SDWebimage就是采用的NSCache

NSCache优点如下:

  1. 系统资源将要耗尽时,它可以自动删减缓存。
  2. 可以设置最大缓存数量。
  3. 可以设置最大占用内存值。
  4. NSCache线程是安全的。

19.知不知道Designated Initializer?使用它的时候有什么需要注意的问题?

基本遵循以下三个规则(约束条件)

  1. 子类如果有指定初始化函数,那么指定初始化函数实现时必须调用它的直接父类的指定初始化函数。
  2. 如果子类有指定初始化函数,那么便利初始化函数必须调用自己的其它初始化函数(包括指定初始化函数以及其他的便利初始化函数),不能调用super的初始化 函数。
  3. 如果子类提供了指定初始化函数,那么一定要实现所有父类的指定初始化函数。

这个问题没有想好该如何回答,希望大家指教。

20.实现description方法能取到什么效果?

举例来说明吧

1.我们创建一个自定义对象
@interface Person : NSObject

@property (nonatomic,copy  ) NSString *name;
@property (nonatomic,copy  ) NSString *hobbies;

@end
2.在ViewController中,我们引用了Person
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
Person *p = [[Person alloc] init];

p.name = @"lili";

p.hobbies = @"paint";

NSLog(@"%@",p);
}

此时打印出来的结果如图:

2017-06-15 13:12:00.471 cop[1561:258406] <Person: 0x60800003dc80>

是不是和你预计的效果还是差了一些?

此时我们就需要重写对象的**description**方法

3.此时在Person.m文件中,我们进行如下操作:
#import "Person.h"

@implementation Person

- (NSString *)description {

return [NSString stringWithFormat:@"_name = %@,_hobbies = %@",_name,_hobbies];
}
@end

再次打印

2017-06-15 13:21:20.132 cop[1593:275015] _name = lili,_hobbies = paint

通过对比之后,大家一定就明白了

21.objc使用什么机制管理对象内存?

Objective-C使用AEC自动引用计数来有效的管理内存。

他遵循的原则是,谁引用,谁销毁。

Retain,Copy,Alloc,New等必然对应Release