起初乍一看感觉问题并不是很多,通过总结才发现面试官的准备十分充分,涵盖了很多方面,在总结的过程中,我也等于是复习了一遍。
目前针对初级篇的问题大致总结了一下,我看了中级以及高级的题目,大致分为以下几类:
再加上是基础题目里也有很多值得拓展的问题
- 内存管理
- 数据持久化
- 多线程
- 属性修饰符
- 内存语义。。。。
关于中高级的问题,我会接下来做仔细的分析,我心里并没有十足的把握,或许上面的回答也是漏洞百出,但是希望各位同行能多多指教,指出我的不足,在此先行谢过。
在整理这篇答案的时候,借鉴了很多网上的资料,很杂,也很难一一列出。
喵神的关于storyBoard
那篇
此篇是根据知名博主 J-Knight 所提供的面试题目,所整理的答案,感谢 J-Knight 的分享,点击查看原文。
另外,我写此文的目的在于和广大的iOS
开发者进行沟通交流,里面的内容有自己的理解,也有很大一部分参照网上的解释。很感谢之前的分享者,文末会附上相关的链接。如果在本文有理解不正确的地方,也希望大家多多指正。
面试题分为三个部分,我们先从基础开始。
其实Objective-C
是一门动态语言的用运行时Runtime
可以更好地说明,但我看后面还有关于运行时
的问题,在此处就先不展开了。
1. 动态类型:例如“id”类型,动态类型属于弱类型,在运行时才决定消息的接收者
2. 动态绑定:程序在运行时需要调用什么代码是在运行时决定的,而不是在编译时。
3. 动态载入:程序在运行时的代码模块以及相关资源是在运行时添加的,而不是启动时就加载所有资源
MVC
模式所有的模块通信都是单向的(这一点个人持怀疑态度,希望大家提出意见)
View
传递指令给Controller
Controller
完成业务逻辑后,要求Model
改变状态Model
将新的数据发送到View
,用户得到反馈
还有一种是Controller
直接接受指令
MVP
模式将 Controller
改名为 Presenter
,同时改变了通信方向。
- 各部分之间的通信,都是双向的。
View
与Model
不发生联系,都通过Presenter
传递。View
非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而Presenter
非常厚,所有逻辑都部署在那里。
MVVM
模式将 Presenter
改名为 ViewModel
,基本上与 MVP
模式完全一致。
唯一的区别是,它采用双向绑定(data-binding)
:View
的变动,自动反映在 ViewModel
,反之亦然。Angular
和 Ember
都采用这种模式。
其实就是循环引用!!!
我们一般在声明一个协议的时候,会定义一个代理属性,如果代理用的比较溜就会知道,一般都是别的类成为当前协议的代理,也就是说,代理实际是外部的一个类。代理属性的销毁不由当前协议类控制,而是由外部代理者自己控制。
如果在定义代理属性时,使用Strong
,外界就无法销毁代理属性,造成循环引用,无法释放。
delegate
和 dataSource
常见于UITableView
和UICollectionView
。
dataSource
是数据源,决定了显示多少个区域
,每个区域显示多少行
,每行现实的具体内容
,头部,尾部
视图等。
delegate
是交互行为的代理,比如点击
,取消选中
,是否高亮
等等。
关于这个问题我有一些疑惑,比如delegate
里面也有决定头部视图显示什么,尾部视图显示什么的方法,按我的理解应该在DataSource
才对,请大家指教。
Block
是带有局部变量的匿名函数,是一个代码段,Block
更面向结果,他适合与状态无关的操作,例如直接返回某些值得时候,就比较适合用Block
。
delegate
回调则更加面向过程,例如执行的回调需要几个不同的步骤,这个时候使用delegate
则更为合适
想深入了解,可以看一下详细的总结 : https://github.com/liberalisman/2018-Interview-Preparation#04-property
@property 的本质.
@property = ivar + getter + setter;
-
原子性--- nonatomic 特质,在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性
(atomicity)
。如果属性具备nonatomic
特质,则不使用自旋锁
。请注意,尽管没有名为atomic
的特质(如果某属性不具备nonatomic
特质,那它就是“原子的” (atomic
) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。 -
读/写权限---readwrite(读写)、readonly (只读)
-
内存管理语义---assign、strong、 weak、unsafe_unretained、copy
-
方法名 - getter= 、setter=
在 ARC 下,如果如果修饰的是 Objective-C 对象。
@property (atomic,strong,readwrite) UIView *view;
如果如果修饰的是基本数据类型。
@property (atomic,assign,readwrite) int num;
完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做**“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter** 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。 也可以在类的实现代码里通过**@synthesize** 语法来指定实例变量的名字.
告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。
如果@synthesize
和@dynamic
都没写,那么默认的就是
@syntheszie var = _var;
// @synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
// @dynamic告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。
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 方法中从偏移量开始取值,
为了能够读取正确字节数,
系统对象偏移量的指针类型进行了类型强转.
NSString
有可变的子类NSMutableString
。因为父类指针可以指向子类,避免NSMutableString
给NSString
赋值,造成原有的值被无形修改,所以用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);
输出它们的地址,string
和 copyString
的地址是相同的,说明它们的指针指向同一个地址,也就是说 copy
是浅拷贝,即指针拷贝
;mutableCopyString
和其他的地址都不一样,说明新开辟了一块内存空间,也就是和前两个没有任何关系,也就是说 mutableCopy
发生了深拷贝
;
copyMutableString
和其他的地址也不一样,同 mutableCopyString
,也是发生了深拷贝
。
对于 copyString
和 copyMutableString
同是使用的 copy
,但是地址却不一样,是因为苹果对于不可变的对象执行的引用操作,而对于可变对象,相对于之前的不可变对象,那么地址肯定会不一样,所以这个时候就要拷贝一份,和之前的就没有任何关系了。
还有就是属性中的 copy 关键字,如下代码:
@property (nonatomic, copy) NSString *copyString;
@property (nonatomic, strong) NSString *strongString;
对于上面的代码,copyString
和 strongString
的 set
方法中,
-(void)setCopyString:(NSString *)copyString {
_copyString = [copyString copy]; // 调用 copy 方法,所以并不是直接赋值
}
-(void)setStrongString:(NSString *)strongString {
_strongString = strongString; // 直接引用
}
如果想要外界赋值的值对 string
有影响,那么就用 strong
,这样两者相当于还是一个对象,如果在赋值以后不想要外界再对 string
有影响,那么就用 copy
。也就是说用 strong
还是 copy
可以根据情况而定。
简单说就是遵守NSCopying
,NSMutableCopying
协议
并且实现(id)copyWithZone:(NSZone *)zone
和(id)mutableCopyWithZone:(NSZone *)zone
两个方法即可。
深入了解可看我的其他文章。
源对象类型 | 拷贝方式 | 副本对象类型 | 是否有新的对象 |
---|---|---|---|
NSArray | Copy | NSArray | NO |
NSMutableArray | Copy | NSArray | YES |
NSMutableArray | MutableCopy | NSMutableArray | YES |
NSArray | MutableCopy | NSMutableArray | YES |
如果是集合内容复制,它的内容复制也分两种,一种是单层复制
,一种是完全复制
。上表的后三种全都是单层内容复制
,只有最外面的容器被复制了,里面存储对象的指针地址不变。
关于IBOutlet
修饰的属性究竟是使用strong
还是weak
,网上的不同意见还是挺多的。
但我认为这可以分为两种情况:
1.如果从storyBoard
或者nib
拖出来的插座属性
是storyBoard
或者nib
所直接拥有的,这个时候应该使用Strong
修饰
2.如果是一个storyBoard
或者nib
的子控件
再添加子控件
,这个时候就应该用weak
。
此图控制器的View拖出来的线就是strong
。
而如果往View
上再次添加子控件的话,拖出来的线就是weak
。
atomic-原子性
- 默认的属性
- 保证CPU在别的线程来访问这个属性之前,先执行完当前线程
- 速度较慢,因为要保证整体完成。
nonatomic-非原子性
- 非默认的属性
- 线程不安全,如果两个线程同时访问,会出问题
- 速度快
很遗憾,并不是。。虽然atomic-原子性
能保证不同的线程同时访问一个属性的时候,它的Setter
和getter
方法会有序执行,但如果此时有另一个线程调用该属性的Release
方法,还是会出问题的,因为atomic-原子性
只能管好它的Setter
和getter
方法。
再者开锁是很耗性能的,所以在移动端,一般使用nonatomic
,而Mac OS
不涉及到性能瓶颈,所在在Mac OS
上使用atomic
。
至于在iOS
上保证属性在不同线程间访问的绝对安全,这块儿我暂时没有研究过,希望知道的朋友指教。
自定义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
其实关于用StoryBoard
还是纯代码的开发方式,争吵声一直都存在,其实我个人并不反感StoryBoard
,反而还挺喜欢。开发速度快,如果协调好,可以减轻很多工作量。不过关于StoryBoard
这个话题如果展开的话还是比较大,建议大家读一下。喵神最近写的一篇文章,附上原文链接,有异议的话也欢迎大家积极讨论。
进程:进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
线程:线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。1个进程要想执行任务,必须得有线程,例如默认就是主线程。
同步函数:不具备开线程的能力,只能串行按顺序执行任务
异步函数:具备开线程的能力,但并不是只要是异步函数就会开线程。
并行:并行即同时执行。比如同时开启3条线程分别执行三个不同人物,这些任务执行时同时进行的。
并发:并发指在同一时间里,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
// 第一种方式。
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
// 第二种方式
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
//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通过嵌套就可以实现线程间的通信。
//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]);
}];
}];
dispatch_barrier_async(queue, ^{
NSLog(@"barrier");
});
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]);
});
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"-----");
});
dispatch_apply(subpaths.count, queue, ^(size_t index) {
});
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必须成对使用
可以用串行队列或者是同步锁。保证在同一时间内,只有一条线程在访问资源。
- plist文件(属性列表)
- preference(偏好设置)
- NSKeyedArchiver(归档)
- SQLite 3 (FMDB)
- CoreData
在此不展开了,篇幅比较大,详情见我另一篇文章
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 {}
在做缓存时,优先使用NSCache
而不是NSDictionary
,我们熟悉的框架SDWebimage
就是采用的NSCache
。
NSCache
优点如下:
- 系统资源将要耗尽时,它可以自动删减缓存。
- 可以设置最大缓存数量。
- 可以设置最大占用内存值。
NSCache
线程是安全的。
基本遵循以下三个规则(约束条件)
- 子类如果有指定初始化函数,那么指定初始化函数实现时必须调用它的直接父类的指定初始化函数。
- 如果子类有指定初始化函数,那么便利初始化函数必须调用自己的其它初始化函数(包括指定初始化函数以及其他的便利初始化函数),不能调用super的初始化 函数。
- 如果子类提供了指定初始化函数,那么一定要实现所有父类的指定初始化函数。
这个问题没有想好该如何回答,希望大家指教。
举例来说明吧
@interface Person : NSObject
@property (nonatomic,copy ) NSString *name;
@property (nonatomic,copy ) NSString *hobbies;
@end
- (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
**方法
#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
通过对比之后,大家一定就明白了
Objective-C使用AEC自动引用计数
来有效的管理内存。
他遵循的原则是,谁引用,谁销毁。
Retain
,Copy
,Alloc
,New
等必然对应Release
。