最新iOS开发常见面试题总结一!(附答案)

1.iOS 类(class)和结构体(struct)有什么区别?

Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。

举个简单的例子,代码如下

class Temperature {
  var value: Float = 37.0
}

class Person {
  var temp: Temperature?

  func sick() {
    temp?.value = 41.0
  }
}

let A = Person()
let B = Person()
let temp = Temperature()

A.temp = temp
B.temp = temp

A.sick() 上面这段代码,由于 Temperature 是 class ,为引用类型,故 A 的 temp 和 B 的 temp指向同一个对象。A 的 temp修改了,B 的 temp 也随之修改。这样 A 和 B 的 temp 的值都被改成了41.0。如果将 Temperature 改为 struct,为值类型,则 A 的 temp 修改不影响 B 的 temp。

内存中,引用类型诸如类是在堆(heap)上,而值类型诸如结构体实在栈(stack)上进行存储和操作。相比于栈上的操作,堆上的操作更加复杂耗时,所以苹果官方推荐使用结构体,这样可以提高 App 运行的效率。

class有这几个功能struct没有的:

class可以继承,这样子类可以使用父类的特性和方法 类型转换可以在runtime的时候检查和解释一个实例的类型 可以用deinit来释放资源 一个类可以被多次引用 struct也有这样几个优势:

结构较小,适用于复制操作,相比于一个class的实例被多次引用更加安全。 无须担心内存memory leak或者多线程冲突问题

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130595548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

2.iOS自动释放池是什么,如何工作 ?

当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放池。它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放。

1.object-c 是通过一种"referring counting"(引用计数)的方式来管理内存的, 对象在开始分配内存(alloc)的时候引用计数为一,以后每当碰到有copy,retain的时候引用计数都会加一, 每当碰到release和autorelease的时候引用计数就会减一,如果此对象的计数变为了0, 就会被系统销毁.

2.NSAutoreleasePool 就是用来做引用计数的管理工作的,这个东西一般不用你管的.

3.autorelease和release没什么区别,只是引用计数减一的时机不同而已,autorelease会在对象的使用真正结束的时候才做引用计数减一.

3.iOS你在项目中用过 runtime 吗?举个例子

Objective-C 语言是一门动态语言,编译器不需要关心接受消息的对象是何种类型,接收消息的对象问题也要在运行时处理。

pragramming 层面的 runtime 主要体现在以下几个方面:

1.关联对象 Associated Objects
2.消息发送 Messaging
3.消息转发 Message Forwarding
4.方法调配 Method Swizzling
5.“类对象” NSProxy Foundation | Apple Developer Documentation
6.KVC、KVO About Key-Value Coding

4.KVC /KVO的底层原理和使用场景

1 KVC(KeyValueCoding)
1.1 KVC 常用的方法

(1)赋值类方法
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

(2)取值类方法
// 能取得私有成员变量的值
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

1.2 KVC 底层实现原理

当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:
 1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值
 2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值
 3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值
 4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法

1.3 KVC 的使用场景
1.3.1 赋值
(1) KVC 简单属性赋值

Person *p = [[Person alloc] init];
//    p.name = @"jack";
//    p.money = 22.2;
使用setValue: forKey:方法能够给属性赋值,等价于直接给属性赋值
[p setValue:@"rose" forKey:@"name"];
[p setValue:@"22.2" forKey:@"money"];

(2) KVC复杂属性赋值

//给Person添加 Dog属性
   Person *p = [[Person alloc] init];
   p.dog = [[Dog alloc] init];
  // p.dog.name = @"阿黄";

1)setValue: forKeyPath: 方法的使用
  //修改p.dog 的name 属性
    [p.dog setValue:@"wangcai" forKeyPath:@"name"];
    [p setValue:@"阿花" forKeyPath:@"dog.name"];

2)setValue: forKey: 错误用法
    [p setValue:@"阿花" forKey:@"dog.name"];
    NSLog(@"%@", p.dog.name);

3)直接修改私有成员变量
[p setValue:@"旺财" forKeyPath:@"_name"];

(3) 添加私有成员变量

Person 类中添加私有成员变量_age
[p setValue:@"22" forKeyPath:@"_age"];

1.3.2 字典转模型

(1)简单的字典转模型
+(instancetype)videoWithDict:(NSDictionary *)dict
{
   JLVideo *videItem = [[JLVideo alloc] init];
   //以前
//    videItem.name = dict[@"name"];
//    videItem.money = [dict[@"money"] doubleValue] ;
   
   //KVC,使用setValuesForKeysWithDictionary:方法,该方法默认根据字典中每个键值对,调用setValue:forKey方法
   // 缺点:字典中的键值对必须与模型中的键值对完全对应,否则程序会崩溃
   [videItem setValuesForKeysWithDictionary:dict];
   return videItem;
}

(2)复杂的字典转模型
注意:复杂字典转模型不能直接通过KVC 赋值,KVC只能在简单字典中使用,比如:
   NSDictionary *dict = @{
                      @"name" : @"jack",
                      @"money": @"22.2",
                      @"dog" : @{
                              @"name" : @"wangcai",
                              @"money": @"11.1",

                              }

                      };
  JLPerson *p = [[JLPerson alloc]init]; // p是一个模型对象
  [p setValuesForKeysWithDictionary:dict];
内部转换原理:
//    [p setValue:@"jack" forKey:@"name"];
//    [p setValue:@"22.2" forKey:@"money"];
//    [p setValue:@{
//                  @"name" : @"wangcai",
//                  @"money": @"11.1",
//
//                  } forKey:@"dog"]; //给 dog赋值一个字典肯定是不对的

(3)KVC解析复杂字典的正确步骤
  NSDictionary *dict = @{
                      @"name" : @"jack",
                      @"money": @"22.2",
                      @"dog" : @{
                              @"name" : @"wangcai",
                              @"price": @"11.1",
                              },
                      //人有好多书
                      @"books" : @[
                              @{
                                  @"name" : @"5分钟突破iOS开发",
                                  @"price" : @"19.8"
                                  },
                              @{
                                  @"name" : @"3分钟突破iOS开发",
                                  @"price" : @"24.8"
                                  },
                              @{
                                  @"name" : @"1分钟突破iOS开发",
                                  @"price" : @"29.8"
                                  }
                              ]
                      };

   XMGPerson *p = [[XMGPerson alloc] init];
    p.dog = [[XMGDog alloc] init];
   [p.dog setValuesForKeysWithDictionary:dict[@"dog"]];
   
   //保存模型的可变数组
   NSMutableArray *arrayM = [NSMutableArray array];
   
   for (NSDictionary *dict in dict[@"books"]) {
       //创建模型
       Book *book = [[Book alloc] init];
       //KVC
       [book setValuesForKeysWithDictionary:dict];
       //将模型保存
       [arrayM addObject:book];
   }
  
        p.books = arrayM;

备注:
   (1)当字典中的键值对很复杂,不适合用KVC;
   (2)服务器返还的数据,你可能不会全用上,如果在模型一个一个写属性非常麻烦,所以不建议使用KVC字典转模型

1.3.3 取值
(1) 模型转字典

 Person *p = [[Person alloc]init];
 p.name = @"jack";
 p.money = 11.1;
 //KVC取值
 NSLog(@"%@ %@", [p valueForKey:@"name"], [p valueForKey:@"money"]);

 //模型转字典, 根据数组中的键获取到值,然后放到字典中
 NSDictionary *dict = [p dictionaryWithValuesForKeys:@[@"name", @"money"]];
 NSLog(@"%@", dict);

(2) 访问数组中元素的属性值

Book *book1 = [[Book alloc] init];
book1.name = @"5分钟突破iOS开发";
book1.price = 10.7;

Book *book2 = [[Book alloc] init];
book2.name = @"4分钟突破iOS开发";
book2.price = 109.7;

Book *book3 = [[Book alloc] init];
book3.name = @"1分钟突破iOS开发";
book3.price = 1580.7;

// 如果valueForKeyPath:方法的调用者是数组,那么就是去访问数组元素的属性值
// 取得books数组中所有Book对象的name属性值,放在一个新的数组中返回
    NSArray *books = @[book1, book2, book3];
    NSArray *names = [books valueForKeyPath:@"name"];
    NSLog(@"%@", names);

//访问属性数组中元素的属性值
Person *p = [[Person alloc]init];
p.books = @[book1, book2, book3];
NSArray *names = [p valueForKeyPath:@"books.name"];
NSLog(@"%@", names);

2 KVO (Key Value Observing)
2.1 KVO 的底层实现原理

(1)KVO 是基于 runtime 机制实现的
(2)当一个对象(假设是person对象,对应的类为 JLperson)的属性值age发生改变时,系统会自动生成一个继承自JLperson的类NSKVONotifying_JLPerson,在这个类的 setAge 方法里面调用
   [super setAge:age];
   [self willChangeValueForKey:@"age"];
   [self didChangeValueForKey:@"age"];
三个方法,而后面两个方法内部会主动调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context方法,在该方法中可以拿到属性改变前后的值.

2.2 KVO的作用
作用:能够监听某个对象属性值的改变

// 利用KVO监听p对象name 属性值的改变
   Person *p = [[XMGPerson alloc] init];
   p.name = @"jack";
   
  /* 对象p添加一个观察者(监听器)
    Observer:观察者(监听器)
    KeyPath:属性名(需要监听哪个属性)
    */
   [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew  | NSKeyValueObservingOptionOld context:@"123"];
   
/**
*  利用KVO 监听到对象属性值改变后,就会调用这个方法
*
*  @param keyPath 哪一个属性被改了
*  @param object  哪一个对象的属性被改了
*  @param change  改成什么样了
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
   // NSKeyValueChangeNewKey == @"new"
   NSString *new = change[NSKeyValueChangeNewKey];
   // NSKeyValueChangeOldKey == @"old"
   NSString *old = change[NSKeyValueChangeOldKey];
   
   NSLog(@"%@-%@",new,old);
}

5.iOS中持久化方式有哪些?

属性列表文件 -- NSUserDefaults 的存储,实际是本地生成一个 plist 文件,将所需属性存储在 plist 文件中

对象归档 -- 本地创建文件并写入数据,文件类型不限

SQLite 数据库 -- 本地创建数据库文件,进行数据处理

CoreData -- 同数据库处理思想相同,但实现方式不同

6.什么是KVC和KVO?

KVC(Key-Value-Coding)内部的实现:一个对象在调用setValue的时候
(1)首先根据方法名找到运行方法的时候所需要的环境参数。
(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。
(3)再直接查找得来的具体的方法实现。KVO(Key-Value- Observing):当观察者为一个对象的属性进行了注册,被观察对象的isa指针被修改的时候,isa指针就会指向一个中间类,而不是真实的类。所以 isa指针其实不需要指向实例对象真实的类。所以我们的程序最好不要依赖于isa指针。在调用类的方法的时候,最好要明确对象实例的类名

7.iOS中属性修饰符的作用?

ios5之前是MRC,内存需要程序员进行管理,ios5之后是ARC,除非特殊情况,比如C框架或者循环引用,其他时候是不需要程序员手动管理内存的。 ios中当我们定义属性@property的时候就需要属性修饰符,下面我们就看一下不同属性修饰符的作用。有错误和不足的地方还请大家谅解并批评指正。

主要的属性修饰符有下面几种:

  • copy
  • assign
  • retain
  • strong
  • weak
  • readwrite/readonly (读写策略、访问权限)
  • nonatomic/atomic (安全策略)

如果以MRC和ARC进行区分修饰符使用情况,可以按照如下方式进行分组:

 1. MRC: assign/ retain/ copy/  readwrite、readonly/ nonatomic、atomic  等。
 2. ARC: assign/ strong/ weak/ copy/ readwrite、readonly/ nonatomic、atomic  等。

属性修饰符对retainCount计数的影响。

  1. alloc为对象分配内存,retainCount 为1 。
  2. retain MRC下 retainCount + 1。
  3. copy 一个对象变成新的对象,retainCount为 1, 原有的对象计数不变。
  4. release 对象的引用计数 -1。
  5. autorelease 对象的引用计数 retainCount - 1,如果为0,等到最近一个pool结束时释放。

不管MRC还是ARC,其实都是看reference count是否为0,如果为0那么该对象就被释放,不同的地方是MRC需要程序员自己主动去添加retain 和 release,而ARC apple已经给大家做好,自动的在合适的地方插入retain 和 release类似的内存管理代码,具体原理如下,图片摘自官方文档。

MRC 和 ARC原理

下面就详述上所列的几种属性修饰符的使用场景,应用举例和注意事项。

8.iOS atomatic nonatomic区别和理解

第一种

atomic和nonatomic区别用来决定编译器生成的getter和setter是否为原子操作。atomic提供多线程安全,是描述该变量是否支持多线程的同步访问,如果选择了atomic 那么就是说,系统会自动的创建lock锁,锁定变量。nonatomic禁止多线程,变量保护,提高性能。

atomic:默认是有该属性的,这个属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题。

nonatomic:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率。

atomic的意思就是setter/getter这个函数,是一个原语操作。如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,可以保证数据的完整性。nonatomic不保证setter/getter的原语行,所以你可能会取到不完整的东西。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。

比如setter函数里面改变两个成员变量,如果你用nonatomic的话,getter可能会取到只更改了其中一个变量时候的状态,这样取到的东西会有问题,就是不完整的。当然如果不需要多线程支持的话,用nonatomic就够了,因为不涉及到线程锁的操作,所以它执行率相对快些。

下面是载录的网上一段加了atomic的例子:


  {lock}
                                if (property != newValue) { 
                                        [property release]; 
                                        property = [newValue retain]; 
                                }
                        {unlock}

可以看出来,用atomic会在多线程的设值取值时加锁,中间的执行层是处于被保护的一种状态,atomic是oc使用的一种线程保护技术,基本上来讲,就是防止在写入未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

第二种

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

atomic
设置成员变量的@property属性时,默认为atomic,提供多线程安全。

在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。加了atomic,setter函数会变成下面这样:

                    {lock}
                            if (property != newValue) { 
                                    [property release]; 
                                    property = [newValue retain]; 
                            }
                    {unlock}

nonatomic
3禁止多线程,变量保护,提高性能。

3atomic是Objc使用的一种线程保护技术,基本上来讲,是防止在写未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

3指出访问器不是原子操作,而默认地,访问器是原子操作。这也就是说,在多线程环境下,解析的访问器提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。如果你不指定 nonatomic ,在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值。

9.iOS UIViewController的完整生命周期

UIViewController的完整生命周期

-[ViewControllerinitWithNibName:bundle:];

-[ViewControllerinit];

-[ViewControllerloadView];

-[ViewControllerviewDidLoad];

-[ViewControllerviewWillDisappear:];

-[ViewControllerviewWillAppear:];

-[ViewControllerviewDidAppear:];

-[ViewControllerviewDidDisappear:];

1、 alloc 创建对象,分配空间

2、init(initWithNibName) 初始化对象,初始化数据

3、loadView 从nib载入视图 ,通常这一步不需要去干涉。除非你没有使用xib文件创建视图

4、viewDidLoad 载入完成,可以进行自定义数据以及动态创建其他控件

5、viewWillAppear 视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了

6、viewDidAppear 视图已在屏幕上渲染完成

当一个视图被移除屏幕并且销毁的时候的执行顺序,这个顺序差不多和上面的相反

1、viewWillDisappear 视图将被从屏幕上移除之前执行

2、viewDidDisappear 视图已经被从屏幕上移除,用户看不到这个视图了

3、dealloc 视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放

ViewController 的 loadView,、viewDidLoad,、viewDidUnload 分别是在什么时候调用的?

viewDidLoad在view从nib文件初始化时调用,loadView在controller的view为nil时调用。

此方法在编程实现view时调用,view控制器默认会注册memory warning notification,当view controller的任何view没有用的时候,viewDidUnload会被调用,在这里实现将retain的view release,如果是retain的IBOutlet view属性则不要在这里release,IBOutlet会负责release。

10.ios7 层协议,tcp四层协议及如何对应的?

11.iOS应用导航模式有哪些?

平铺模式,一般由scrollView和pageControl组合而成的展示方式。手机自带的天气比较典型。

标签模式,tabBar的展示方式,这个比较常见。

树状模式,tableView的多态展示方式,常见的9宫格、系统自带的邮箱等展现方式。

12.一个参数既可以是const还可以是volatile吗?解释为什么。

• 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

13.iOS 响应者链的事件传递过程?

如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图

在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

如果window对象也不处理,则其将事件或消息传递给UIApplication对象

如果UIApplication也不能处理该事件或消息,则将其丢弃

14.iOS 请说明并比较以下关键词:weak,block

weak与weak基本相同。前者用于修饰变量(variable),后者用于修饰属性(property)。weak 主要用于防止block中的循环引用。 block也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。block用于修饰某些block内部将要修改的外部变量。 weak和block的使用场景几乎与block息息相关。而所谓block,就是Objective-C对于闭包的实现。闭包就是没有名字的函数,或者理解为指向函数的指针。

15.iOS UIView的Touch事件注意点

如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件(掌握)
UIView不接收触摸事件的三种情况:
不接收用户交互 : userInteractionEnabled = NO
隐藏 : hidden = YES
透明 : alpha = 0.0 ~ 0.01
UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

16.iOS 说明并比较关键词:strong, weak, assign, copy等等

strong表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。

weak表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。

assign主要用于修饰基本数据类型,如NSInteger和CGFloat,这些数值主要存在于栈上。

weak 一般用来修饰对象,assign一般用来修饰基本数据类型。原因是assign修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成野指针。

copy与strong类似。不同之处是strong的复制是多个指针指向同一个地址,而copy的复制每次会在内存中拷贝一份对象,指针指向不同地址。copy一般用在修饰有可变对应类型的不可变对象上,如NSString, NSArray, NSDictionary。

Objective-C 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。

1、属性readwrite,readonly,assign,retain,copy,nonatomic 各自什么作用,他们在那种情况下用?

readwrite:默认的属性,可读可写,生成setter和getter方法。

readonly:只读,只生成getter方法,也就是说不能修改变量。

assign:用于声明基本数据类型(int、float)仅设置变量,是赋值属性。

retain:持有属性,setter方法将传入的参数先保留,再赋值,传入的参数 引用计数retaincount 会加1

在堆上开辟一块空间,用指针a指向,然后将指针a赋值(assign)给指针b,等于是a和b同时指向这块堆空间,当a不使用这块堆空间的时候,是否要释放这块堆空间?答案是肯定要的,但是这件堆空间被释放后,b就成了野指针。

如何避免这样的问题? 这就引出了引用计数器,当a指针这块堆空间的时候,引用计数器+1,当b也指向的时候,引用计数器变成了2,当a不再指向这块堆空间时,release-1,引用计数器为1,当b也不指向这块堆空间时,release-1,引用计数器为0,调用dealloc函数,空间被释放

总结:当数据类型为int,float原生类型时,可以使用assign。如果是上面那种情况(对象)就是用retain。

copy:是赋值特性,setter方法将传入对象赋值一份;需要完全一份新的变量时,直接从堆区拿。

当属性是 NSString、NSArray、NSDictionary时,既可以用strong 修饰,也可以用copy修饰。当用strong修饰的NSString 指向一个NSMutableString时,如果在不知情的情况下这个NSMutableString的别的引用修改了值,就会出现:一个不可变的字符串却被改变了的情况, 使用copy就不会出现这种情况。

nonatomic:非原子性,可以多线程访问,效率高。
atomic:原子性,属性安全级别的表示,同一时刻只有一个线程访问,具有资源的独占性,但是效率很低。
strong:强引用,引用计数+ 1,ARC下,一个对象如果没有强引用,系统就会释放这个对象。
weak:弱引用,不会使引用计数+1.当一个指向对象的强引用都被释放时,这块空间依旧会被释放掉。

使用场景:在ARC下,如果使用XIB 或者SB 来创建控件,就使用 weak。纯代码创建控件时,用strong修饰,如果想用weak 修饰,就需要先创建控件,然后赋值给用weak修饰的对象。

查找了一些资料,发现主要原因是,controller需要拥有它自己的view(这个view是所以子控件的父view),因此viewcontroller对view就必须是强引用(strong reference),得用strong修饰view。对于lable,它的父view是view,view需要拥有label,但是controller是不需要拥有label的。如果用strong修饰,在view销毁的情况下,label还仍然占有内存,因为controller还对它强引用;如果用wak修饰,在view销毁的时label的内存也同时被销毁,避免了僵尸指针出现。

用引用计数回答就是:因为Controller并不直接“拥有”控件,控件由它的父view“拥有”。使用weak关键字可以不增加控件引用计数,确保控件与父view有相同的生命周期。控件在被addSubview后,相当于控件引用计数+1;父view销毁后,所有的子view引用计数-1,则可以确保父view销毁时子view立即销毁。weak的控件在removeFromSuperview后也会立即销毁,而strong的控件不会,因为Controller还保有控件强引用。

总结归纳为:当控件的父view销毁时,如果你还想继续拥有这个控件,就用srtong;如果想保证控件和父view拥有相同的生命周期,就用weak。当然在大多数情况下用两个都是可以的。

使用weak的时候需要特别注意的是:先将控件添加到superview上之后再赋值给self,避免控件被过早释放。

17.iOS里什么是响应链,它是怎么工作的?

第一反应就是,响应链就是响应链啊,由一串UIResponder对象链接,收到响应事件时由上往下传递,直到能响应事件为止。

但其中却大有文章...

1.由一串UIResponder对象链接 ?

我们知道UIResponder类里有个属性:

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

如果我们对响应链原理不清楚的话,会很容易的认为,这条链是由 nextResponder 指针连接起来的,在寻找响应者的时候是顺着这个指针找下去直到找到响应者为止的,但这是错误的认为。 举个例子: 现在我们有这样一个场景:

E0B0C758-8B7D-4B6A-85F3-72F3A174DCEA.png

AppDelegate上的Window上有一个UIViewController *ViewController, 然后在ViewController.view 上按顺序添加viewA和viewB,viewB稍微覆盖viewA一部分用来测试, 给viewA,viewB 分别添加点击手势tapA 和 tapB,然后把viewB.userInteractionEnabled = NO,让viewB不能响应点击。

然后我们点击重复的那块区域,会发现viewA响应了tap手势,执行了tapA的事件。 我们知道viewB设置了viewB.userInteractionEnabled = NO,不响应tap手势是正常的,但怎么会透过viewB,viewA响应了手势?

我们知道nextResponder指针指向的规则:

  • UIView
  • 如果 view 是一个 view controller 的 root view,nextResponder 是这个 view controller.
  • 如果 view 不是 view controller 的 root view,nextResponder 则是这个 view 的 superview
  • UIViewController
  • 如果 view controller 的 view 是 window 的 root view, view controller 的 nextResponder 是这个 window
  • 如果 view controller 是被其他 view controller presented调起来的,那么 view controller 的 nextResponder 就是发起调起的那个 view controller
  • UIWindow
  • window 的 nextResponder 是 UIApplication 对象.
  • UIApplication
  • UIApplication 对象的 nextResponder 是 app delegate, 但是 app delegate 必须是 UIResponder 对象,并且不能使 view ,view controller 或 UIApplication 对象他本身.

那么上述情况下,viewB所在的响应者链应该是: viewB -> ViewController.view -> ViewController -> Window -> Application 这种情况下怎么也轮不到viewA去响应啊。

所以,当有事件需要响应时,nextResponder 并不是链接响应链的那根绳子,响应链的工作方式另有别的方式

2. 那么响应链是如何工作,正确找到应该响应该事件的响应者的?

UIKit使用基于视图的hit-testing来确定touch事件发生的位置。具体解释就是,UIKit将touch的位置和视图层级中的view的边界进行了比较,UIView的方法 hitTest:withEvent: 在视图层级中进行,寻找包含指定touch的最深子视图。这个视图成为touch事件的第一个响应者。

说白了就是,当有touch事件来的时候,会从最下面的视图开始执行 hitTest:withEvent: ,如果符合成为响应者的条件,就会继续遍历它的 subviews 继续执行 hitTest:withEvent: ,直到找到最合适的view成为响应者。

这里要注意几个点:

  • 符合响应者的条件包括
  • touch事件的位置在响应者区域内
  • 响应者 hidden 属性不为 YES
  • 响应者 透明度 不是 0
  • 响应者 userInteractionEnabled 不为 NO
  • 遍历 subview 时,是从上往下顺序遍历的,即 view.subviews 的 lastObject 到 firstObject 的顺序,找到合适的响应者view,即停止遍历.

所以再回看上面的例子,当我们点击中间的重复区域时,流程其实是这样:

  • AppDelegate 的 window 收到事件,并开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
  • window 上只有 viewcontroller.view ,所以viewcontroller.view 开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
  • viewcontroller.view 有两个子view, viewA 和 viewB ,但是viewB 在 viewA 上边,所以先 viewB 执行 hitTest:withEvent: ,结果发现viewB 不符合要求,因为viewB 的 userInteractionEnabled 为 NO.
  • 接下来 viewA 执行 hitTest:withEvent: ,发现符合条件,并且viewA 也没有子view可去遍历,于是返回viewA.
  • viewA成了最终事件的响应者.

这样就完美解释了,最开始例子的响应状况.

那么如果 viewB 的 userInteractionEnabled 属性为YES的话,是怎么样的呢?

如果 viewB 的 userInteractionEnabled 属性为YES,上面流程的第三部就会发现viewB是符合要求的,而直接返回viewB作为最终响应者,中断子view的遍历,viewA都不会被遍历到了.

这就是响应链相关的点,如果有什么不对的请留言提示,然后有什么别的需要补充的我会及时补充~

18.什么是iOS的动态绑定 ?

—在运行时确定要调用的方法

动态绑定将调用方法的确定也推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,您的代码每次执行都可以得到不同的结果。运行时因子负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的isa指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。而且,您不必在Objective-C 代码中做任何工作,就可以自动获取动态绑定的好处。您在每次发送消息时,

特别是当消息的接收者是动态类型已经确定的对象时,动态绑定就会例行而透明地发生。

19.iOS单元测试框架有哪些?

OCUnit 是 OC 官方测试框架, 现在被 XCTest 所取代。 XCTest 是与 Foundation 框架平行的测试框架。 GHUnit 是第三方的测试框架。github地址OCMock都是第三方的测试框架。

20.iOS ARC全解?

考查点

我记得在刚接触iOS的时候对这个ARC和MRC就讨论颇深,认为ARC是对程序员的一种福利,让我们节省了大量的代码,那么ARC是什么呢?

ARC 是苹果在 WWDC 2011 提出来的技术,因此很多新入行的同学可能对此技术细节并不熟悉。但是,虽然 ARC 极大地简化了我们的内存管理工作,但是引用计数这种内存管理方案如果不被理解,那么就无法处理好那些棘手的循环引用问题。所以,这道面试题其实是考查同学对于 iOS 程序内存管理的理解深度。 答案

自动的引用计数(Automatic Reference Count 简称 ARC),是苹果在 WWDC 2011 年大会上提出的用于内存管理的技术。

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt) 等语言也提供了基于引用计数的内存管理方式。

引用计数这种内存管理方式虽然简单,但是手工写大量的操作引用计数的代码不但繁琐,而且容易被遗漏。于是苹果在 2011 年引入了 ARC。ARC 顾名思义,是自动帮我们填写引用计数代码的一项功能。

ARC 的想法来源于苹果在早期设计 Xcode 的 Analyzer 的时候,发现编译器在编译时可以帮助大家发现很多内存管理中的问题。后来苹果就想,能不能干脆编译器在编译的时候,把内存管理的代码都自动补上,带着这种想法,苹果修改了一些内存管理代码的书写方式(例如引入了 @autoreleasepool 关键字)后,在 Xcode 中实现了这个想法。

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1 的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

但是也有人认为,ARC 也附带有运行期的一些机制来使 ARC 能够更好的工作,他们主要是指 weak 关键字。weak 变量能够在引用计数为 0 时被自动设置成 nil,显然是有运行时逻辑在工作的。我通常并没有把这个算在 ARC 的概念当中,当然,这更多是一个概念或定义上的分歧,因为除开 weak 逻辑之外,ARC 核心的代码都是在编译期填充的。

21.iOS内存的使用和优化的注意事项

重用问题:

如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews

设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:

当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:

载入时就会将XIB/storyboard需要的所有资源,

包括图片全部载入内存,即使未来很久才会使用。

那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:

学会选择对业务场景最合适的数组结构是写出高效代码的基础。

比如,数组: 有序的一组值。

使用索引来查询很快,使用值查询很慢,插入/删除很慢。

字典: 存储键值对,用键来查找比较快。

集合: 无序的一组值,用值来查找很快,插入/删除很快。

gzip/zip压缩:

当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:

对于不应该使用的数据,使用延迟加载方式。

对于不需要马上显示的视图,使用延迟加载方式。

比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:

对于cell的行高要缓存起来,使得reload数据时,效率也极高。

而对于那些网络数据,不需要每次都请求的,应该缓存起来,

可以写入数据库,也可以通过plist文件存储。

处理内存警告:

一般在基类统一处理内存警告,将相关不用资源立即释放掉

重用大开销对象:

一些objects的初始化很慢,

比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。

通常是作为属性存储起来,防止反复创建。

避免反复处理数据:

许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。

在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:

在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

22.什么是iOS的目标-动作机制 ?

目标是动作消息的接收者。一个控件,或者更为常见的是它的单元,以插座变量(参见"插座变量"部分)

的形式保有其动作消息的目标。

动作是控件发送给目标的消息,或者从目标的角度看,它是目标为了响应动作而实现的方法。

程序需要某些机制来进行事件和指令的翻译。这个机制就是目标-动作机制。

23.iOS 事件传递的完整过程?

先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。

调用最合适控件的touches….方法

如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者
接着就会调用上一个响应者的touches….方法

如何判断上一个响应者:
如果当前这个view是控制器的view,那么控制器就是上一个响应者
如果当前这个view不是控制器的view,那么父控件就是上一个响应者

24.什么是iOS的响应者链?

  • 响应者链条:是由多个响应者对象连接起来的链条
  • 作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
  • 响应者对象:能处理事件的对象

25.iOS UIView的Touch事件有哪几种触摸事件?

处理事件的方法

UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件

  //一根或者多根手指开始触摸view
  - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  //一根或者多根手指在view上移动
  - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  //一根或者多根手指离开view
  - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  //触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程
  - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

26.iOS开发:Objective-C中通知与协议的区别?

what is difference between NSNotification and protocol? (通知和协议的不同之处?)

我想大家都知道这个东西怎么用,但是更深层次的思考可能就比较少了吧,众所周知就是代理是一对一的,但是通知是可以多对多的.但是为什么是这个样子,有没有更深的思考过这个问题?

今天看了下网上的几个视频教程,KVO、KVC、谓词、通知,算是开发中的高级点的东西了。通知和协议都是类似于回调一样,于是就在思考通知和协议到底有什么不同,或者说什么时候该用通知,什么时候该用协议。

下面是网上摘抄的一段解释:

协议有控制链(has-a)的关系,通知没有。首先我一开始也不太明白,什么叫控制链(专业术语了~)。但是简单分析下通知和代理的行为模式,我们大致可以有自己的理解简单来说,通知的话,它可以一对多,一条消息可以发送给多个消息接受者。代理按我们的理解,到不是直接说不能一对多,比如我们知道的明星经济代理人,很多时候一个经济人负责好几个明星的事务。只是对于不同明星间,代理的事物对象都是不一样的,一一对应,不可能说明天要处理A明星要一个发布会,代理人发出处理发布会的消息后,别称B的发布会了。但是通知就不一样,他只关心发出通知,而不关心多少接收到感兴趣要处理。因此控制链(has-a从英语单词大致可以看出,单一拥有和可控制的对应关系。

1.通知:

通知需要有一个通知中心:NSNotificationCenter,自定义通知的话需要给一个名字,然后监听。

优点:通知的发送者和接受者都不需要知道对方。可以指定接收通知的具体方法。通知名可以是任何字符串。

缺点:较键值观察(KVO)需要多点代码,在删掉前必须移除监听者。

2.协议

通过setDelegate来设置代理对象,最典型的例子是常用的TableView.

优点:支持它的类有详尽和具体信息。

缺点:该类必须支持委托。某一时间只能有一个委托连接到某一对象。

相信看到这些东西,认真思考一下,就可以知道在那种情况下使用通知,在那种情况下使用代理了吧.

27.写一个NSString类的实现

 + (id)initWithCString:(c*****t char *)nullTerminatedCString  encoding:(NSStringEncoding)encoding;** 

  + (id) stringWithCString: (c*****t char*)nullTerminatedCString  

        encoding: (NSStringEncoding)encoding 

  { 

   NSString *obj; 

   obj = [self allocWithZone: NSDefaultMallocZone()]; 

   obj = [obj initWithCString: nullTerminatedCString encoding:  encoding]; 

   return AUTORELEASE(obj); 

  } 

28.iOS 事件的产生和传递流程

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理 touchesBegan… touchesMoved… touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递(不实现touches方法,系统会自动向上一个响应者传递),将事件交给上一个响应者进行处理
如果一个事件既想自己处理也想交给上一个响应者处理,那么自己实现touches方法,并且调用super的touches方法,[super touches、、、];

29.关键字volatile有什么含意?并给出三个不同的例子?

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到

这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

• 并行设备的硬件寄存器(如:状态寄存器)

• 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

• 多线程应用中被几个任务共享的变量

30.iOS hitTest方法&pointInside方法

hitTest方法
当事件传递给控件的时候,就会调用控件的这个方法,去寻找最合适的view
point:当前的触摸点,point这个点的坐标系就是方法调用者

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

pointInside方法
作用:判断当前这个点在不在方法调用者(控件)上

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:的实现原理

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{

   // 1.判断当前控件能否接收事件
   if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;

   // 2. 判断点在不在当前控件
   if ([self pointInside:point withEvent:event] == NO) return nil;

   // 3.从后往前遍历自己的子控件
   NSInteger count = self.subviews.count;

   for (NSInteger i = count - 1; i >= 0; i--) {
       UIView *childView = self.subviews[i];

       // 把当前控件上的坐标系转换成子控件上的坐标系
       CGPoint childP = [self convertPoint:point toView:childView];

       UIView *fitView = [childView hitTest:childP withEvent:event];


       if (fitView) { // 寻找到最合适的view
           return fitView;
       }
   }
   // 循环结束,表示没有比自己更合适的view
   return self;
}

你可能感兴趣的:(最新iOS开发常见面试题总结一!(附答案))