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或者多线程冲突问题
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<NSString *, id> *)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.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<NSString *,id> *)change context:(void *)context方法,在该方法中可以拿到属性改变前后的值.
作用:能够监听某个对象属性值的改变
// 利用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<NSString *,id> *)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计数的影响。
- alloc为对象分配内存,retainCount 为1 。
- retain MRC下 retainCount + 1。
- copy 一个对象变成新的对象,retainCount为 1, 原有的对象计数不变。
- release 对象的引用计数 -1。
- 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;
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;
作用:判断当前这个点在不在方法调用者(控件)上
- (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鑫
链接:https://www.jianshu.com/p/2cc5d8b4e8d3