答面试题·答J_Knight_《2017年5月iOS招人心得(附面试题)》中的面试题(一)

离职找工作中,刷一刷网上的面试题。原文链接

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

因为OC可以在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等。所以,OC是一门动态语言。它具有相当多的动态特性,基本的,也是经常被提到和用到的有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)

  1. 动态类型(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
    //...
}
复制代码
  1. 动态绑定(Dynamic binding)
    即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。例如class_addMethod这个方法就可以动态的添加方法。

  2. 动态加载(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耦合会很严重, 像 viewDidLoadviewWillAppear这些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_enterdispatch_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。

    如果我们把信号量设置成1dispatch_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使用什么机制管理对象内存?

使用引用计数机制管理对象内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值