FAQ-常见问题
1、VC的生命周期
- -initWithCoder: //VC初始化
- -initWithNibName:bundle:
- -loadView //从nib载入视图或返回一个自定义视图
- -viewDidLoad //视图载入完成并开始进一步的设置
- -viewWillAppear: //视图即将出现在屏幕上
- -updateViewConstraints //更新约束
- -viewWillLayoutSubviews //视图布局
- -viewDidLayoutSubviews
- -viewDidAppear: //视图已展示在屏幕上
- -viewWillDisappear://视图从屏幕上移除
- -viewDidDisappear:
- -dealloc
通过addChildViewController
将子VC添加到父VC容器中后,子VC与父VC的生命周期会同步进行,如:父VC的-viewDidAppear
等触发时子VC的也会触发。
VC的生命周期是根据其self.view
所处的状态而定的:在父VC中第一次访问子VC的.view
属性时会先调用子VC中的-loadView
,随后子VC中的-viewDidLoad
也会触发,以便做进一步的设置;如果.view
属性没被添加到一个已经展示的视图上,则其所属VC的-viewWillAppear
不会调用。
2、VC跳转时生命周期调用顺序
- 情形1:由 A push 到 B 时:
1 |
|
- 情形2:由 A present 到 B 时:
1 |
|
3、宏定义使用错误案例
1 |
|
输出结果都不是设想的值?这里涉及到编译器对“#”开头的行、自定义宏等宏的处理逻辑。
宏定义是在预编译阶段把宏的内容拷贝的源代码的相应位置,所以case1中MATH_MAX(a,b)+1
就展开为a>b?a:b+1
,冒号后面变成了b+1
。。这就跟设计之初的愿望相违背了~
同理,case2和case3在编译时,会变成:
1 |
|
这里i做了两次++运算,显然也不是设想之初的结果。
#修改case1:
宏定义部分应该加上括号:
1 |
|
#修改case2和case3:
不要在需要预处理的代码中加入内联代码逻辑。
4、指针\地址
一般变量
存放的是数据本身;指针变量
存放的是数据的地址。
1 |
|
输出日志:
1 |
|
*
是指针的标识,表示接下来的变量是一个指针变量
;&
用来取变量的地址。
上面的示例中,a
是一般变量,保存数值68
;而a
作为变量,系统会为其分配一个地址0x7ffee43cec64
;p
是初始值为空的指针变量,随后指向&a
,即变量p
保存的是变量a
的地址(0x7ffee43cec64
);p
作为指针变量,其保存的指针所指向的值为 68,变量p
自己的内存地址为0x7ffee43cec58
。
5、NULL、nil、Nil、NSNull
1、前三者NULL
、nil
、Nil
从本质上来讲都是(void *)0
:
- NULL,表示C类型的指针为空;
1 |
|
- nil,表示OC中对象的指针为空;
1 |
|
- Nil,表示OC中类类型变量的值为空;
1 |
|
2、NSNull
与以上三者不同,它是一个OC类,用于创建空对象:
1 |
|
示例中1处即为刚创建的空对象nsnullObj
,它的内存地址如下:
示例中3处将空对象置为nil后,其内存地址如下:
所以可以看出NSNull
仍然是一个对象,只是此对象中什么都没有而已~
6、对象的比较==与isEqual
- ==
比较的是值
。
基本数据类型
比较数值,数值相等即为真;指针变量
比较变量内保存的地址
,地址相同,即指向同一个数据对象,返回真。
#示例1:
1 |
|
输出:
1 |
|
- isEqual:
This method defines what it means for instances to be equal.
isEqual:
默认情况下,效果与==
一样,两个对象指向的地址
相同时才返回真。
有时在创建子类时,需要你自己重写此方法的实现:
1 |
|
实际上NSString
就重写了此方法的实现,只要两个字符串对象中的值相同即返回真,参考示例1
。
If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual: in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash in your subclass.
两个对象相等时,它们的hash
值一定相等,所以你往往也需要重写hash
方法~
7、集合与对象的引用关系
常用的集合容器NSArray
、NSDictionary
、NSSet
都是强引用容器
,这些容器会强引用其内部的对象。
1 |
|
输出日志:
1 |
|
当对象被添加到集合中时,其引用计数会 +1;对象被移出集合时,其引用计数 -1。所以,实践中我们需要注意这种强引用关系及其可能引起的对象释放问题~
那么,怎么实现弱引用容器呢?
- 方案1:使用 NSValue 提供的两个类方法类存取对象:
1 |
|
#示例:
1 |
|
输出日志:
1 |
|
通过NSValue
包装obj
之后,即使添加到数组,obj
的引用计数也不会增加,且当在数组外部将obj
置为 nil 之后,obj
对象会自动销毁。
8、静态变量、静态常量
- static
1 |
|
修饰局部变量
时,静态局部变量的作用范围为该函数体。它的值在编译期就会确定下来,并被存储到全局变量区。因此静态局部变量只会生成一份内存、只会初始化一次并供所有对象使用;静态局部变量的生命周期和程序相同,直到程序结束这个局部变量才会销毁。
修饰全局变量
时,静态全局变量的作用域仅限于当前文件(.m),它也是被存储到全局变量区,生命周期与程序相同,程序结束时才会销毁。使用静态全局变量能避免在同一个文件中重复定义全局变量。
static 强调的是静态,变量只创建一次,直到程序结束才销毁。静态变量的值是可以更新的,并且只要某个对象对静态变量做了修改,所有的对象都能访问更新后的值。
- const
const
修饰的是常量,强调的是变量的值不可变。常量在第一次赋值之后不能再修改。
在 OC 中一般是结合static
使用,示例:
1 |
|
示例中,用法2
和3
实质上是一样的。用法1
和2
的区别,用英语表达会更直观一些:
1 |
|
指针常量
,A constant pointer (not modifiable) to an NSString object (its value can be modified)。这里的s1
是一个字符串对象的指针,因此 const 修饰的是指针,即指针为常量不能修改,但指针指向的值是可以修改的。
1 |
|
常量指针
,A modifiable pointer to a constant NSString object (its value can’t be modified)。这里s2
和s3
是指针,*s2
和*s3
是指针指向的值,const 修饰的是值,即值不可变,但字符串对象可以修改其指针,重新取别的值。
9、分类与扩展
代码组织形式上:
扩展通常是定义在原类的m
文件中;而分类既可以定义在原类的h
和m
文件中,又可以存在于单独的h
和m
文件中,分类组织的方式不同导入的方式也会不同。从命名上来看,扩展相当于未命名的分类。
方法定义上:
分类和扩展中都可以定义方法以达到扩展现有类的方法列表之目的,不同的是扩展中定义的方法必须在原类的m
文件中提供实现;分类中定义的方法则不依赖于原类的实现文件,而是在自己的@implementation
中提供实现,也就是说分类可以在不知道原类具体实现的情况下对原类进行扩展。另外,分类中可以重写原类的方法,重写后实例就不能访问原来的方法了。
成员变量定义上:
扩展中可以定义成员变量,并且这些变量只对本类可见;分类中则不能定义成员变量,因为分类与原类的内存空间相互独立,原类初始化后实例大小已确定,不能再添加成员变量。
属性定义上:
分类和扩展都可以定义属性,不同的是扩展中的属性会由编译器自动提供存取器;而分类中虽然定义了属性,编译器却不会为其提供存取器,需要我们自己通过对象绑定机制
主动实现。
10、URL缓存策略
NSURLRequestCachePolicy 用来指定网络请求的缓存策略,具体的枚举和作用如下:
1 |
|
默认缓存策略,当客户端发起一个请求时,首先检查本地是否有缓存(NSCachedURLResponse)。如果没有缓存,则直接从服务器处获取;如果有缓存,则继续检查缓存是否过期(通过Cache-Control:max-age或者Expires)。如果没有过期,则直接使用缓存数据;如果缓存过期了,则向服务器发起一个请求,服务器会对比它保存的资源的 Last-Modified 或者 Etags 字段(二者都存在的情况下下如果有一个不同则认为缓存已过期),如果不同则返回新数据,否则返回 304 Not Modified 并继续使用缓存数据(客户端可以再使用”max-age”秒缓存数据)。
1 |
|
不使用缓存,直接从服务器请求原始数据。
1 |
|
无论缓存是否过期,有缓存则使用缓存,否则重新请求原始数据。
1 |
|
有缓存则使用缓存,无论缓存是否过期;无缓存则视为失败,不会重新请求原始数据,类似于离线模式。
1 |
|
本地缓存、代理和其他中介都要忽视他们的缓存,直接加载源数据。
1 |
|
向服务器发送一个请求,如果服务器确认缓存有效,则继续使用缓存,否则从源段加载数据。
使用缓存的目的是为了降低对网络连接的依赖,减少对相同 URL 的多次请求,提高应用的响应速度。这里所说的缓存是指对 URL 请求响应体(NSCachedURLResponse)的缓存。iOS 中响应的缓存是通过 NSURLCache 来实现的,它会将NSURLRequest
与NSCachedURLResponse
进行映射,并保存在内存和磁盘中。你甚至可以指定两片缓存的容量大小和存储路径。
相关参考: