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
3
4
5
-[B viewDidLoad]
-[A viewWillDisappear:]
-[B viewWillAppear:]
-[A viewDidDisappear:]
-[B viewDidAppear:]
  • 情形2:由 A present 到 B 时:
1
2
3
4
5
-[B viewDidLoad]
-[A viewWillDisappear:]
-[B viewWillAppear:]
-[B viewDidAppear:]
-[A viewDidDisappear:]

3、宏定义使用错误案例

1
2
3
4
5
6
7
8
9
10
11
12
#define MATH_MAX(a,b) a > b ? a : b

- (void)defineExmple
{
//case1
printf("result: %d\n",MATH_MAX(2,1) + 1);//结果:result: 2
//case2
int i = 1;
printf("largest: %d\n", MATH_MAX(i++,0));//结果:largest: 2
//case3
printf("now i value = %d\n", i);//结果:now i value = 3
}

输出结果都不是设想的值?这里涉及到编译器对“#”开头的行、自定义宏等宏的处理逻辑。

宏定义是在预编译阶段把宏的内容拷贝的源代码的相应位置,所以case1中MATH_MAX(a,b)+1就展开为a>b?a:b+1,冒号后面变成了b+1。。这就跟设计之初的愿望相违背了~

同理,case2和case3在编译时,会变成:

1
printf("largest: %d\n", i++ > 0 ? i++ : 0);

这里i做了两次++运算,显然也不是设想之初的结果。

#修改case1:

宏定义部分应该加上括号:

1
#define MATH_MAX(a,b) (a > b ? a : b)

#修改case2和case3:

不要在需要预处理的代码中加入内联代码逻辑。

4、指针\地址

一般变量存放的是数据本身;
指针变量存放的是数据的地址。

1
2
3
4
int a = 68;
long *p = NULL;
p = &a;
NSLog(@"++a的值:%d \n++&a:%p \n++p保存的指针:%p \n++*p的值:%d \n++变量p的地址:%p",a,&a,p,*p,&p);

输出日志:

1
2
3
4
5
++a的值:68 
++&a:0x7ffee43cec64
++p保存的指针:0x7ffee43cec64
++*p的值:68
++变量p的地址:0x7ffee43cec58

*是指针的标识,表示接下来的变量是一个指针变量&用来取变量的地址。

上面的示例中,a是一般变量,保存数值68;而a作为变量,系统会为其分配一个地址0x7ffee43cec64p是初始值为空的指针变量,随后指向&a,即变量p保存的是变量a的地址(0x7ffee43cec64);p作为指针变量,其保存的指针所指向的值为 68,变量p自己的内存地址为0x7ffee43cec58

5、NULL、nil、Nil、NSNull

1、前三者NULLnilNil从本质上来讲都是(void *)0

  • NULL,表示C类型的指针为空;
1
long *p = NULL;
  • nil,表示OC中对象的指针为空;
1
NSObject *obj = nil;
  • Nil,表示OC中类类型变量的值为空;
1
Class class = Nil;

2、NSNull与以上三者不同,它是一个OC类,用于创建空对象:

1
2
3
4
5
6
7
NSNull *nsnullObj = [NSNull null]; // 1
NSArray *aArr = @[@(1),nsnullObj];
for (NSObject *item in aArr) {
if ([item isEqual:[NSNull null]]) { // 2
nsnullObj = nil; // 3
}
}

示例中1处即为刚创建的空对象nsnullObj,它的内存地址如下:
nsnullObj

示例中3处将空对象置为nil后,其内存地址如下:
pic_nsnullObj_nil

所以可以看出NSNull仍然是一个对象,只是此对象中什么都没有而已~

6、对象的比较==与isEqual

  • ==

比较的是

基本数据类型比较数值,数值相等即为真;
指针变量比较变量内保存的地址,地址相同,即指向同一个数据对象,返回真。

#示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)test{
NSString *A = @"Hello"; //常量区
NSString *B = @"Hello"; //常量区
NSString *C = [[NSString alloc] initWithString:@"Hello"]; //常量区
NSString *D = [[NSString alloc] initWithString:@"Hello"]; //常量区
NSString *E = [NSString stringWithFormat:@"Hello"]; //堆区
NSString *F = [NSString stringWithFormat:@"Hello"]; //堆区

NSLog(@"A:%p",A); //打印A对象保存的指针
NSLog(@"B:%p",B);
NSLog(@"C:%p",C);
NSLog(@"D:%p",D);
NSLog(@"E:%p",E);
NSLog(@"F:%p",F);

NSLog(@"A==B:%d",A==B); // 检测AB变量内保存的地址是否相同
NSLog(@"C==D:%d",C==D);
NSLog(@"E==F:%d",E==F);

NSLog(@"A==C:%d",A==C);
NSLog(@"A==E:%d",A==E);
NSLog(@"C==E:%d",C==E);

NSLog(@"A isEqual: B : %d",[A isEqual:B]);
NSLog(@"A isEqual: C : %d",[A isEqual:C]);
NSLog(@"A isEqual: E : %d",[A isEqual:E]);
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A:0x1015870e8
B:0x1015870e8
C:0x1015870e8
D:0x1015870e8 // ABCD都指向常量区的同一个对象@"Hello"
E:0xf538015112db6b7d
F:0xf538015112db6b7d
A==B:1 //指针相等返回真
C==D:1
E==F:1
A==C:1
A==E:0 //指针不同返回假
C==E:0
A isEqual: B : 1
A isEqual: C : 1
A isEqual: E : 1 //重写了isEqual方法,指针不同字符串相同即返回真
  • isEqual:

This method defines what it means for instances to be equal.

isEqual:默认情况下,效果与==一样,两个对象指向的地址相同时才返回真。

有时在创建子类时,需要你自己重写此方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (BOOL)isEqual:(id)obj {
if (obj == self){ //先比较指针
return YES;
}
if (!obj || ![obj isKindOfClass:[self class]]){ //再比较类型
return NO;
}
return [self isEqualToUser:obj]; //再比较自定义的属性
}

- (BOOL)isEqualToUser:(User *)aUser {
if (self == aUser){
return YES;
}
if (![(id)[self name] isEqual:[aUser name]]){ //比较属性
return NO;
}
}
return YES;
}

实际上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、集合与对象的引用关系

常用的集合容器NSArrayNSDictionaryNSSet都是强引用容器,这些容器会强引用其内部的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Xcode中禁用ARC以便使用引用计数
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
@autoreleasepool {
NSObject *obj = [[[NSObject alloc] init] autorelease];
NSLog(@"++1.obj:%p ++&obj:%p ++obj.rc:%lu",obj,&obj,obj.retainCount);

NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];
[array addObject:obj];
NSLog(@"++++2.obj.rc:%lu",obj.retainCount);

[array removeAllObjects];
NSLog(@"++++3.obj.rc:%lu",obj.retainCount);
}

return YES;
}

输出日志:

1
2
3
++1.obj:0x600002449230 ++&obj:0x7ffee57fac60 ++obj.rc:1
++++2.obj.rc:2
++++3.obj.rc:1

当对象被添加到集合中时,其引用计数会 +1;对象被移出集合时,其引用计数 -1。所以,实践中我们需要注意这种强引用关系及其可能引起的对象释放问题~

那么,怎么实现弱引用容器呢?

  • 方案1:使用 NSValue 提供的两个类方法类存取对象:
1
2
3
4
5
6
7
//保存对象时
NSValue *value = [NSValue valueWithNonretainedObject:myObj];
[array addObject:value];

//读取对象时
value = [array objectAtIndex:x];
myObj = [value pointerValue];

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//Person.m
@implementation Person
-(void)dealloc{
[super dealloc];
NSLog(@"++++PERSON IS DEALLOCED~");
}
@end

//调用
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
@autoreleasepool {
Person *obj = [[[Person alloc] init] autorelease];
NSLog(@"++1.obj:%p ++&obj:%p ++obj.rc:%lu",obj,&obj,obj.retainCount);

NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];
NSValue *value = [NSValue valueWithNonretainedObject:obj];
[array addObject:value];
NSLog(@"++++2.obj.rc:%lu",obj.retainCount);

for (NSValue *valueN in array) {
Person * objN = [valueN pointerValue];
NSLog(@"++3.objN:%p ++&objN:%p ++objN.rc:%lu",objN,&objN,objN.retainCount);
}

obj = nil;

for (NSValue *valueN in array) {
Person * objN = [valueN pointerValue];
NSLog(@"++4.objN:%p ++&objN:%p ++objN.rc:%lu",objN,&objN,objN.retainCount);
}
[array removeAllObjects];

NSLog(@"++5.obj:%@ ++&obj:%p ++obj.rc:%lu",obj,&obj,obj.retainCount);
}

return YES;
}

输出日志:

1
2
3
4
5
6
++1.obj:0x60000323c9c0 ++&obj:0x7ffeeb5fec60 ++obj.rc:1
++++2.obj.rc:1
++3.objN:0x60000323c9c0 ++&objN:0x7ffeeb5fec00 ++objN.rc:1
++4.objN:0x60000323c9c0 ++&objN:0x7ffeeb5febb0 ++objN.rc:1
++5.obj:(null) ++&obj:0x7ffeeb5fec60 ++obj.rc:0
++++PERSON IS DEALLOCED~

通过NSValue包装obj之后,即使添加到数组,obj的引用计数也不会增加,且当在数组外部将obj置为 nil 之后,obj对象会自动销毁。

8、静态变量、静态常量

  • static
1
static NSString *aStaticString;

修饰局部变量时,静态局部变量的作用范围为该函数体。它的值在编译期就会确定下来,并被存储到全局变量区。因此静态局部变量只会生成一份内存、只会初始化一次并供所有对象使用;静态局部变量的生命周期和程序相同,直到程序结束这个局部变量才会销毁。

修饰全局变量时,静态全局变量的作用域仅限于当前文件(.m),它也是被存储到全局变量区,生命周期与程序相同,程序结束时才会销毁。使用静态全局变量能避免在同一个文件中重复定义全局变量。

static 强调的是静态,变量只创建一次,直到程序结束才销毁。静态变量的值是可以更新的,并且只要某个对象对静态变量做了修改,所有的对象都能访问更新后的值。

  • const

const修饰的是常量,强调的是变量的值不可变。常量在第一次赋值之后不能再修改。

在 OC 中一般是结合static使用,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//用法1
static NSString * const s1;
//用法2
static const NSString * s2;
//用法3
static NSString const * s3;

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
s1 = @"A";//编译时会报错
s2 = @"B";//编译正常
s3 = @"C";//编译正常
return YES;
}

示例中,用法23实质上是一样的。用法12的区别,用英语表达会更直观一些:

1
static NSString * const s1;

指针常量,A constant pointer (not modifiable) to an NSString object (its value can be modified)。这里的s1是一个字符串对象的指针,因此 const 修饰的是指针,即指针为常量不能修改,但指针指向的值是可以修改的。

1
static const NSString * s2;

常量指针,A modifiable pointer to a constant NSString object (its value can’t be modified)。这里s2s3是指针,*s2*s3是指针指向的值,const 修饰的是值,即值不可变,但字符串对象可以修改其指针,重新取别的值。

9、分类与扩展

代码组织形式上

扩展通常是定义在原类的m文件中;而分类既可以定义在原类的hm文件中,又可以存在于单独的hm文件中,分类组织的方式不同导入的方式也会不同。从命名上来看,扩展相当于未命名的分类。

方法定义上

分类和扩展中都可以定义方法以达到扩展现有类的方法列表之目的,不同的是扩展中定义的方法必须在原类的m文件中提供实现;分类中定义的方法则不依赖于原类的实现文件,而是在自己的@implementation中提供实现,也就是说分类可以在不知道原类具体实现的情况下对原类进行扩展。另外,分类中可以重写原类的方法,重写后实例就不能访问原来的方法了。

成员变量定义上

扩展中可以定义成员变量,并且这些变量只对本类可见;分类中则不能定义成员变量,因为分类与原类的内存空间相互独立,原类初始化后实例大小已确定,不能再添加成员变量。

属性定义上

分类和扩展都可以定义属性,不同的是扩展中的属性会由编译器自动提供存取器;而分类中虽然定义了属性,编译器却不会为其提供存取器,需要我们自己通过对象绑定机制主动实现。

10、URL缓存策略

NSURLRequestCachePolicy 用来指定网络请求的缓存策略,具体的枚举和作用如下:

1
NSURLRequestUseProtocolCachePolicy = 0

默认缓存策略,当客户端发起一个请求时,首先检查本地是否有缓存(NSCachedURLResponse)。如果没有缓存,则直接从服务器处获取;如果有缓存,则继续检查缓存是否过期(通过Cache-Control:max-age或者Expires)。如果没有过期,则直接使用缓存数据;如果缓存过期了,则向服务器发起一个请求,服务器会对比它保存的资源的 Last-Modified 或者 Etags 字段(二者都存在的情况下下如果有一个不同则认为缓存已过期),如果不同则返回新数据,否则返回 304 Not Modified 并继续使用缓存数据(客户端可以再使用”max-age”秒缓存数据)。

1
NSURLRequestReloadIgnoringLocalCacheData = NSURLRequestReloadIgnoringCacheData = 1

不使用缓存,直接从服务器请求原始数据。

1
NSURLRequestReturnCacheDataElseLoad = 2

无论缓存是否过期,有缓存则使用缓存,否则重新请求原始数据。

1
NSURLRequestReturnCacheDataDontLoad = 3, 

有缓存则使用缓存,无论缓存是否过期;无缓存则视为失败,不会重新请求原始数据,类似于离线模式。

1
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4 //Unimplemented

本地缓存、代理和其他中介都要忽视他们的缓存,直接加载源数据。

1
NSURLRequestReloadRevalidatingCacheData = 5 //Unimplemented

向服务器发送一个请求,如果服务器确认缓存有效,则继续使用缓存,否则从源段加载数据。

使用缓存的目的是为了降低对网络连接的依赖,减少对相同 URL 的多次请求,提高应用的响应速度。这里所说的缓存是指对 URL 请求响应体(NSCachedURLResponse)的缓存。iOS 中响应的缓存是通过 NSURLCache 来实现的,它会将NSURLRequestNSCachedURLResponse进行映射,并保存在内存和磁盘中。你甚至可以指定两片缓存的容量大小和存储路径。


相关参考:

#©Stack Overflow


FAQ-常见问题
https://davidlii.cn/2017/11/24/faq.html
作者
Davidli
发布于
2017年11月24日
许可协议