答面试题·答J_Knight_《2017年5月iOS招人心得(附面试题)》中的面试题(一)
1. 为什么说Objective-C是一门动态语言
因为OC可以在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等。所以,OC是一门动态语言。它具有相当多的动态特性,基本的,也是经常被提到和用到的有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)
- 动态类型(Dynamic typing) 运行时再决定对象的类型。简单的说,就是id类型。id可以指向任意类型的对象,然后使用的时候再确定对象本来的类型。
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
someClass *classSpecifiedInstance = (someClass *)obj;
// Do Something to classSpecifiedInstance which now is an instance of someClass
//...
}
-
动态绑定(Dynamic binding) 即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。例如
class_addMethod
这个方法就可以动态的添加方法。 -
动态加载(Dynamic loading) 让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。比如@2x,@3x资源,就是在运行的时候根据不同的设备加载不同的资源。
2. 讲一下MVC和MVP,MVVM?
它们都是MVC的变种,结构划分为:
- view : 视图
- model : 业务数据
- x(c,vm,p):业务逻辑的处理者,作为M、V的桥梁
其中mvp和mvvm中的v是包含了ViewController的。
看图说话。
1. MVC
2. iOS开发实际应用时的MVC
用MVC开发的时候,View和Controller耦合会很严重, 像
viewDidLoad
、viewWillAppear
这些view的生命周期都会在controller里面来管理。再加上controller还要负责代理、数据源、网络请求等,于是controller就变得越来越庞大,越来越混乱,很不好测试。
3. MVP
跟MVC相比,我们把所有view相关的东西都化作view模块,其余的逻辑放到一个模块,于是就有了MVP。MVP中的V包含了
UIViewController
,它负责所有跟UI相关的东西,比如view的生命周期管理,布局。所以P的责任更加单一,只是通过数据和状态更新View。由于V和P的分离,我们会写很多事件传递的代码来连接V和P。比如:
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
protocol GreetingViewPresenter {
init(view: GreetingView, person: Person)
func showGreeting()
}
class GreetingPresenter : GreetingViewPresenter {
unowned let view: GreetingView
let person: Person
required init(view: GreetingView, person: Person) {
self.view = view
self.person = person
}
func showGreeting() {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.view.setGreeting(greeting)
}
}
class GreetingViewController : UIViewController, GreetingView {
var presenter: GreetingViewPresenter!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
}
func didTapButton(button: UIButton) {
self.presenter.showGreeting()
}
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
MVP带给我们更好的可测试性的同时又带来了大量的代码。像上面这个例子里,因为V和P的分离,在需要传递事件时,View中的一个方法只调用Presenter的一个方法的情况会时常发生
func didTapButton(button: UIButton) {
self.presenter.showGreeting()
}
4. MVVM.
MVVM它跟 MVP 很像:
- 把 ViewController 看做 View。
- View 和 Model 之间没有紧耦合
MVVM和MVP的区别主要在于数据绑定这一块。通过响应式编程的框架比如ReactiveCocoa来把View和ViewModel绑定在一起,这样我们就不用写很多刷新页面的代码了。
5. VIPER
最后说一下VIPER这个框架,它不属于MV(X)架构,它更像是乐高积木一样搭建你的应用。VIPER对职责划分了5个模块。
- View(页面) - 展示给用户的界面
- Interactor(交互器) - 包括数据(Entities)或者网络相关的业务逻辑。比如创建新的 entities 或者从服务器上获取数据;要实现这些功能,你可能会用到一些服务和管理(Services and Managers):这些可能会被误以为成是外部依赖东西,但是它们就是 VIPER 的 Interactor 模块。
- Presenter(展示器) - 包括 UI(but UIKit independent)相关的业务逻辑,可以调用 Interactor 中的方法。
- Entities(实体) - 纯粹的数据对象。不包括数据访问层,因为这是 Interactor 的职责。
- Router(路由) - 负责 VIPER 模块之间的转场
实际上 VIPER 模块可以只是一个页面(screen),也可以是你应用里整个的用户使用流程(the whole user story)- 比如说「验证」这个功能,它可以只是一个页面,也可以是连续相关的一组页面。你的每个「乐高积木」想要有多大,都是你自己来决定的。
这篇文章对几个模式分析得很好。值得好好读一读。文中的几个例子也是引自这篇文章。
3. 为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?
-
避免循环引用。
@interface SubObj : NSObject @property(nonatomic,strong) id delegate; @end @interface ViewController : UIViewController @property(nonatomic,strong)SubObj * obj; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.obj = [[SubObj alloc] init]; self.obj.delegate = self; } @end
上面的代码中就出现了
ViewController
持有了SubObj
,同时因为Suobj
的delegate是强引用的,所以持有了ViewController
,出现了循环引用。 -
delegate主要是事件抛到给代理来做。dataSource主要是数据来源。
-
一般情况下,简单功能的回调用block,系列函数的回调选择delegate。
4. 属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的?
- 属性的实质就是变量+get方法+set方法
- 属性的关键字有:
- 原子性
nonatomic
,atomic
- 读写权限
readonly
,readwrite
- 指定读写方法
getter
,setter
- 持有方式
strong
,assign
,weak
,copy
,unsafe_unretained
- 是否可以为空
nullable
,nonnull
,null_resettable
,null_unspecified
- 类属性
class
- 原子性
- @synthesize 表示由系统自动生成get和set方法,如果自己实现了get或者set方法则会替换掉系统生成的。@dynamic必须自己提供get和set方法。
5. 属性的默认关键字是什么
基本数据类型默认关键字是 atomic
,readwrite
,assign
其他类型默认关键字是 atomic
,readwrite
,strong
6. NSString为什么要用copy关键字,如果用strong会有什么问题?(注意:这里没有说用strong就一定不行。使用copy和strong是看情况而定的)
防止被修改。比如:
@interface ViewController : UIViewController
@property(nonatomic,strong)NSString * text;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString *test = [NSMutableString stringWithFormat:@"123"];
self.text = test;
[test appendString:@"456"];
}
@end
这个时候打印self.text
则变成了被修改后的值123456
。
7. 如何令自己所写的对象具有拷贝功能?
实现NSCopying
协议就让对象有了拷贝功能。
8. 可变集合类 和 不可变集合类的 copy 和 mutablecopy有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么?
-
简单的说,
copy
会生成一个不可变的对象,mutableCopy
会生成一个可变对象NSArray *array = @[@1,@2]; NSArray *array2 = [array copy];//不可变 NSMutableArray *array3 = [array mutableCopy];//可变 NSArray *array4 = [array3 copy];//不可变 NSMutableArray *array5 = [array3 mutableCopy];//可变
-
集合里面的元素并没有内容拷贝。还是原来的对象。
NSArray<NSMutableString *> *array = @[[NSMutableString stringWithString:@"123"]]; NSArray<NSMutableString *> *array2 = [array copy]; NSMutableString *item = array2[0]; [item appendString:@"456"]; NSLog(@"%@",array);
打印结果是
( 123456 )
9.为什么IBOutlet修饰的UIView也适用weak关键字?
因为view
被添加到superView
上面后,就被superView
持有了。我们一般在IB里面的拖的view
都是加在了根view或者它的子view上。而根view又被它的controller
持有,所以IBOutlet
可以用weak
。如果,在IB里面拖出来的view是一个单独的view
没有被加到任何其他view
上,则需要用strong
。
10. nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?
-
nonatomic
表示非原子性,不安全,效率高。atomic
表示原子性,效率低。 -
atomic
不是绝对的线程安全的。@property (atomic, assign) int intA; //thread A for (int i = 0; i < 10000; i ++) { self.intA = self.intA + 1; NSLog(@"Thread A: %d\n", self.intA); } //thread B for (int i = 0; i < 10000; i ++) { self.intA = self.intA + 1; NSLog(@"Thread B: %d\n", self.intA); }
即使我将intA声明为atomic,最后的结果也不一定会是20000。原因就是因为
self.intA = self.intA + 1
;不是原子操作,虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作,当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值。这种场景我们也可以称之为多线程不安全。@property (atomic, strong) NSString* stringA; //thread A for (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.stringA = @"a very long string"; } else { self.stringA = @"string"; } NSLog(@"Thread A: %@\n", self.stringA); } //thread B for (int i = 0; i < 100000; i ++) { if (self.stringA.length >= 10) { NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)]; } NSLog(@"Thread B: %@\n", self.stringA); }
虽然stringA是atomic的property,而且在取substring的时候做了length判断,线程B还是很容易crash,因为在前一刻读length的时候self.stringA = @"a very long string";,下一刻取substring的时候线程A已经将self.stringA = @"string";,立即出现out of bounds的Exception,crash,多线程不安全。
这段例子引用于这篇文章
11. UICollectionView自定义layout如何实现?
继承UICollectionViewLayout
自己实现prepareLayout
,collectionViewContentSize
,layoutAttributesForElementsInRect:
这三个方法。
12. 用StoryBoard开发界面有什么弊端?如何避免?
平日里开发,复用较多的模块用的xib来写。单独的模块,如设置界面用的storyboard来开发。
关于StoryBoard的讨论可以参看喵神的这篇文章
13. 进程和线程的区别?同步异步的区别?并行和并发的区别?
进程和线程的区别
进程(process)
狭义的定义:进程就是一段程序的执行过程。
广义定义:进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。
简单来讲进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程中调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
进程状态:进程有三个状态,就绪,运行和阻塞。就绪状态其实就是获取了除cpu外的所有资源,只要处理器分配资源马上就可以运行。运行态就是获取了处理器分配的资源,程序开始执行,阻塞态,当程序条件不够时,需要等待条件满足时候才能执行,如等待I/O操作的时候,此刻的状态就叫阻塞态。
说说程序,程序是指令和数据的有序集合,其本身没有任何运动的含义,是一个静态的概念,而进程则是在处理机上的一次执行过程,它是一个动态的概念。进程是包含程序的,进程的执行离不开程序,进程中的文本区域就是代码区,也就是程序。
线程(thread)
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
引用于这篇文章
同步和异步的区别
同步是串行的顺序执行,异步是并行的同时执行
并行和并发的区别
并发和并行的区别就是一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务。
前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生.
并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。
并行(parallelism)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行
14. 线程间通信?
线程间通信指的是:1、一个线程传递数据给另一个线程,2、在一个线程中执行完特定任务后,转到另一个线程继续执行任务。
在iOS中可以用这些方法来进行线程间通信:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
dispatch_async(otherQueue, ^{
// dosth
});
15. GCD的一些常用的函数?(group,barrier,信号量,线程同步)
-
dispatch_group
:做完一组操作后再执行后续的代码它有两种用法: 一种是
dispatch_group_async
dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); NSLog(@"group one start"); dispatch_group_async(group, queue, ^{ //do something }); dispatch_group_async(group, queue, ^{ //do something }); dispatch_group_notify(group, queue, ^{ //do something });
第二种是
dispatch_group_enter
和dispatch_group_leave
:dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_enter(group); dispatch_async(queue, ^{ //do something dispatch_group_leave(group); }); dispatch_group_enter(group); dispatch_async(queue, ^{ //do something dispatch_group_leave(group); }); dispatch_group_notify(group, queue, ^{ //do something });
-
barrier
- 通过dispatch_barrier_async添加的block会等到之前添加所有的block执行完毕再执行
- 在dispatch_barrier_async之后添加的block会等到dispatch_barrier_async添加的block执行完毕再执行
- (void)barrier { dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // dosth1; }); dispatch_async(queue, ^{ // dosth2; }); dispatch_barrier_async(queue, ^{ // doBarrier; }); dispatch_async(queue, ^{ // dosth4; }); dispatch_async(queue, ^{ // dosth5; }); }
-
dispatch_semaphore
当我们多个线程要访问同一个资源的时候,往往会设置一个信号量,当信号量大于0的时候,新的线程可以去操作这个资源,操作时信号量-1,操作完后信号量+1,当信号量等于0的时候,必须等待,所以通过控制信号量,我们可以控制能够同时进行的并发数。
信号量有以下3个函数
dispatch_semaphore_create //创建一个信号量 dispatch_semaphore_signal //信号量+1 dispatch_semaphore_wait //等待,直到信号量大于0时,即可操作,同时将信号量-1
-(void)dispatchSignal{ //crate的value表示,最多几个资源可访问 dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //任务1 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 1"); sleep(1); NSLog(@"complete task 1"); dispatch_semaphore_signal(semaphore); }); //任务2 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 2"); sleep(1); NSLog(@"complete task 2"); dispatch_semaphore_signal(semaphore); }); //任务3 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 3"); sleep(1); NSLog(@"complete task 3"); dispatch_semaphore_signal(semaphore); }); }
执行结果为
run task 1 run task 2 complete task 1 complete task 2 run task 3 complete task 3
由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。
如果我们把信号量设置成1
dispatch_semaphore_create(1)
,那么执行结果就会变成顺序执行run task 1 complete task 1 run task 2 complete task 2 run task 3 complete task 3
16. 如何使用队列来避免资源抢夺?
用锁,或者把资源的操作放到单一线程中。
17. 数据持久化的几个方案(fmdb用没用过)
- NSUserDefault
- NSKeyedArchiver
- CoreData
- SQLite
- fmdb
- realm
18. 说一下AppDelegate的几个方法?从后台到前台调用了哪些方法?第一次启动调用了哪些方法?从前台到后台调用了哪些方法?
// 当应用程序启动时(不包括已在后台的情况下转到前台),调用此回调。launchOptions是启动参数,假如用户通过点击push通知启动的应用,这个参数里会存储一些push通知的信息
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions NS_AVAILABLE_IOS(3_0);
– (void)applicationDidBecomeActive:(UIApplication *)application;
//应用即将从前台状态转入后台
- (void)applicationWillResignActive:(UIApplication *)application;
– (void)applicationDidEnterBackground:(UIApplication *)application NS_AVAILABLE_IOS(4_0);
//从后台到前台调用了:
– (void)applicationWillEnterForeground:(UIApplication *)application NS_AVAILABLE_IOS(4_0);
– (void)applicationDidBecomeActive:(UIApplication *)application;
19. NSCache优于NSDictionary的几点?
- NSCache 采用LRU规则,会对超出限制的数据进行自动清除
- NSCache 在系统内存很低时,会自动释放一些对象
- NSCache 是线程安全的,在多线程操作中,不需要对 Cache 加锁
- NSCache 的 Key 只是做强引用,不需要实现 NSCopying 协议
20. 知不知道Designated Initializer?使用它的时候有什么需要注意的问题?
- 类似于Swift中的初始化方法。便捷初始化方法必须调用指定初始化方法。
- 需要注意的是当子类实现了新的指定初始化方法后,需要在子类的指定初始化方法里面用super调用父类的指定初始化方法,并且子类其他的初始化方法需要调用到该指定初始化方法。
21. 实现description方法能取到什么效果?
NSLog(@"%@")
和[NSString stringWithFormat:@"%@"]
会转换成description
返回的字符串。
22. objc使用什么机制管理对象内存?
使用引用计数机制管理对象内存。
转载自:https://juejin.cn/post/6844903581254959117