我是一只勤劳的小蜜蜂。闲暇之余,总结下一直以来自己面试过程中所遇到的一些高概率问题,分享给大家,共同成长。该贴后续将会持续更新~~
1.BAD_ACCESS在什么情况下出现?
①访问一个僵尸对象,访问僵尸对象的成员变量或者向其发消息;②死循环。
2.简述下Objective-C中调用方法的过程(runtime)
Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:
①objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
②然后在该类中的方法列表以及其父类方法列表中寻找方法运行
③如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
④但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会,这三次拯救程序奔溃的说明见问题《什么时候会报unrecognized selector的异常》中的说明
• 补充说明:Runtime 铸就了Objective-C 是动态语言的特性,使得C语言具备了面向对象的特性,在程序运行期创建,检查,修改类、对象及其对应的方法,这些操作都可以使用runtime中的对应方法实现。
3.什么是method swizzling(俗称黑魔法)
简单说就是进行方法交换。
①在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
②每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就
![Uploading 4a6c4741-9186-4ba2-b354-cde08612df6c_550019.png . . .]可以找到对应的IMP
- 交换方法的几种实现方式
- 利用 method_exchangeImplementations 交换两个方法的实现
- 利用 class_replaceMethod 替换方法的实现
-
利用 method_setImplementation 来直接设置某个方法的IMP
4.objc中向一个nil对象发送消息将会发生什么?
在Objective-C中向nil发送消息是完全有效的——只是在运行时不会有任何作用
①如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
② 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*);
③ float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
④ 如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
⑤ 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
• 具体原因分析
①objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector);
② 为了方便理解这个内容,还是贴一个objc的源代码:
struct objc_class
{
// isa指针指向Meta Class,因为Objc的类的本身也是一个Object,
// 为了处理这个关系,runtime就创造了Meta Class,
// 当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
// 方法缓存,对象接到一个消息会根据isa指针查找消息对象,
// 这时会在method Lists中遍历,
// 如果cache了,常用的方法调用时就能够提高调用的效率。
// 这个方法缓存只存在一份,不是每个类的实例对象都有一个方法缓存
// 子类会在自己的方法缓存中缓存父类的方法,父类在自己的方法缓存中也会缓存自己的方法,而不是说子类就不缓存父类方法了
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
分析:
①objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后再发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。
② 如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。
5.objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?
[obj foo];在objc动态编译时,会被转意为:
objc_msgSend(obj, @selector(foo));
6.什么时候会报unrecognized selector的异常?
当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决,如果还是不行就会报unrecognized selector异常。objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:
①objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类;
②然后在该类中的方法列表以及其父类方法列表中寻找方法运行
③如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
○ Method resolution
§ objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。
§ 如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程
§ 如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发
○ Fast forwarding
§ 如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会
§ 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。
§ 否则,就会继续Normal Fowarding。
§ 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但Normal forwarding转发会创建一个NSInvocation对象,相对Normal forwarding转发更快点,所以这里叫Fast forwarding
○ Normal forwarding
§ 这一步是Runtime最后一次给你挽救的机会。
§ 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。
§ 如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。
§ 如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象
7.HTTP协议中POST方法和GET方法有那些区别?
①GET用于向服务器请求数据,POST用于提交数据;
②GET请求,请求参数拼接形式暴露在地址栏,而POST请求参数则放在请求体里面,因此GET请求不适合用于验证密码等操作;
③GET请求的URL有长度限制,POST请求不会有长度限制。
8.猜想runloop内部是如何实现的?
从字面意思看:运行循环、跑圈;
本质:内部就是do-while循环,在这个循环内部不断地处理各种事件(任务),比如:Source、Timer、Observer;
• 每条线程都有唯一一个RunLoop对象与之对应,主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动;
• 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode,如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样做主要是为了隔离不同Mode中的Source、Timer、Observer,让其互不影响;
9.苹果是如何实现autoreleasepool的?
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成。
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_aurorelease
10.这个写法会出什么问题: @property (copy) NSMutableArray *array;
因为copy策略拷贝出来的是一个不可变对象,然而却把它当成可变对象使用,很容易造成程序奔溃。
这里还有一个问题,该属性使用了同步锁,会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明nonatomic可以节省这些虽然很小但是不必要额外开销,在iOS开发中应该使用nonatomic替代atomic。
11.如何让自定义类可以用 copy 修饰符?如何重写带 copy 关键字的 setter?
若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyiog与NSMutableCopying协议,不过一般没什么必要,实现NSCopying协议就够了
实现不可变版本拷贝
- (id)copyWithZone:(NSZone *)zone;
实现可变版本拷贝
- (id)mutableCopyWithZone:(NSZone *)zone;
重写带 copy 关键字的setter
- (void)setName:(NSString *)name
{
_name = [name copy];
}
12.+(void)load; +(void)initialize;有什么用处?
+(void)load;```
① 当类对象被引入项目时, runtime会向每一个类对象发送 load 消息;
②load 方法会在每一个类甚至分类被引入时仅调用一次,调用的顺序:父类优先于子类, 子类优先于分类;
③由于load 方法会在类被import 时调用一次,而这时往往是改变类的行为的最佳时机,在这里可以使用例如method swizlling 来修改原有的方法;
④load 方法不会被类自动继承。
+(void)initialize;```
也是在第一次使用这个类的时候会调用这个方法,也就是说 initialize也是懒加载。
• 总结:
① 在Objective-C中,runtime会自动调用每个类的这两个方法;
② +load会在类初始加载时调用;
③+initialize会在第一次调用类的类方法或实例方法之前被调用;
④这两个方法是可选的,且只有在实现了它们时才会被调用;
⑤两者的共同点:两个方法都只会被调用一次。
13.如何关闭默认的KVO的默认实现,KVO的实现原理?
所谓的“手动触发”是区别于“自动触发”:
自动触发是指类似这种场景:在注册 KVO 之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。
想知道如何手动触发,必须知道自动触发 KVO 的原理:
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。如下所示:
14.若一个类有实例变量NSString *_foo,调用setValue:forKey:时,是以foo还是_foo作为key?
• 都可以
15.KVC和KVO的keyPath一定是属性么?
• 可以是成员变量
16.直接调用_objc_msgForward函数将会发生什么?
_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
直接调用_objc_msgForward是非常危险的事,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。
一旦调用_objc_msgForward,将跳过查找 IMP 的过程,直接触发“消息转发”,如果调用了_objc_msgForward,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend:“我没有在这个对象里找到这个方法的实现”。
17.断点续传如何实现的?
断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当 某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。
使用多线程断点续传下载的时候,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,多个线程并发可以占用服务器端更多资源,从而加快下载速度。
在下载(或上传)过程中,如果网络故障、电量不足等原因导致下载中断,这就需要使用到断点续传功能。下次启动时,可以从记录位置(已经下载的部分)开始,继续下载以后未下载的部分,避免重复部分的下载。断点续传实质就是能记录上一次已下载完成的位置。
断点续传的过程:
①断点续传需要在下载过程中记录每条线程的下载进度;
②每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库;
③在每次向文件中写入数据之后,在数据库中更新下载进度;
④下载完成之后删除数据库中下载记录。
18.当 TableView 的 Cell 改变时,如何让这些改变以动画的形式呈现?
@interface ViewController ()
@property (nonatomic, strong) NSIndexPath *index;
@end
@implementation ViewController
static NSString *ID = @"cell";
- (void)viewDidLoad {
[super viewDidLoad];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
cell.textLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 10;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.index == indexPath){
return 120;
}
return 60;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.index = indexPath;
[tableView deselectRowAtIndexPath:indexPath animated:TRUE];
//重点是这2句代码实现的功能
[tableView beginUpdates];
[tableView endUpdates];
}
19.如何把一个包含自定义对象的数组序列化到磁盘?
//自定义对象只需要实现NSCoding协议即可
- (void)viewDidLoad
{
[super viewDidLoad];
User *user = [User new];
Account *account = [Account new];
NSArray *userArray = @[user, account];
// 存到磁盘
NSData * tempArchive = [NSKeyedArchiver archivedDataWithRootObject: userArray];
}
//代理方法
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.user = [aDecoder decodeObjectForKey:@"user"];
self.account = [aDecoder decodeObjectForKey:@"account"];
}
return self;
}
// 代理方法
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.user forKey:@"user"];
[aCoder encodeObject:self.account forKey:@"account"];
}
20.内存管理的几条原则时什么?按照默认法则.那些关键字生成的对象需要手动释放?在和property结合的时候怎样有效的避免内存泄露?
①谁申请,谁释放;
②遵循Cocoa Touch的使用原则;
③内存管理主要要避免“过早释放”和“内存泄漏”,对于“过早释放”需要注意@property设置特性时,一定要用对特性关键字,对于“内存泄漏”,一定要申请了要负责释放,要细心。
④关键字alloc 或new 生成的对象需要手动释放;
⑤设置正确的property属性,对于retain需要在合适的地方释放。
21.Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么?
线程创建有三种方法:使用NSThread创建、使用GCD的dispatch、使用子类化的NSOperation,然后将其加入NSOperationQueue;在主线程执行代码,方法是performSelectorOnMainThread,如果想延时执行代码可以用performSelector:onThread:withObject:waitUntilDone:
22.IBOutlet连出来的视图属性为什么可以被设置成weak?
因为父控件的subViews数组已经对它有一个强引用。
23.XIB中User Defined Runtime Attributes如何使用?
①User Defined Runtime Attributes是一个不被看重但功能非常强大的的特性,它能够通过KVC的方式配置一些你在interface builder中不能配置的属性。
② 当你希望在IB中作尽可能多得事情,这个特性能够帮助你编写更加轻量级的viewcontroller。
24.请简述UITableView的复用机制。
①每次创建cell的时候通过dequeueReusableCellWithIdentifier:方法创建cell,它先到缓存池中找指定标识的cell,如果没有就直接返回nil。
②如果没有找到指定标识的cell,那么会通过initWithStyle:reuseIdentifier:创建一个cell。
③ 当cell离开界面就会被放到缓存池中,以供下次复用
25.如何高性能的给UIImageView 加个圆角?
• 不好的解决方案
○ 使用下面的方式会强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响,会有卡顿的现象出现 。
self.view.layer.cornerRadius = 5;
self.view.layer.masksToBounds = YES;
• 正确的解决方案:使用绘图技术。
- (UIImage *)circleImage
{
NO代表透明
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
获得上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
添加一个圆
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextAddEllipseInRect(ctx, rect);
裁剪
CGContextClip(ctx);
将图片画上去
[self drawInRect:rect];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
关闭上下文
UIGraphicsEndImageContext();
return image;
}
• 还有一种方案:使用了贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
26.使用drawRect有什么影响?
• drawRect方法依赖Core Graphics框架来进行自定义的绘制
• 缺点:它处理touch事件时每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了
• 这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制。
27.如何渲染UILabel的文字?
通过NSAttributedString/NSMutableAttributedString(富文本)
28.控制器的生命周期
• 就是问的view的生命周期,下面已经按方法执行顺序进行了排序
自定义控制器view,这个方法只有实现了才会执行
- (void)loadView
{
self.view = [[UIView alloc] init];
self.view.backgroundColor = [UIColor orangeColor];
}
view是懒加载,只要view加载完毕就调用这个方法
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"%s",__func__);
}
view即将显示
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s",__func__);
}
view即将开始布局子控件
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
NSLog(@"%s",__func__);
}
view已经完成子控件的布局
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
NSLog(@"%s",__func__);
}
view已经出现
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s",__func__);
}
view即将消失
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
NSLog(@"%s",__func__);
}
view已经消失
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
NSLog(@"%s",__func__);
}
收到内存警告
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
NSLog(@"%s",__func__);
}
方法已过期,即将销毁view
- (void)viewWillUnload
{
}
方法已过期,已经销毁view
- (void)viewDidUnload
{
}
29.iOS本地数据存储都有几种方式?
①.NSkeyedArchiver:采用归档的形式来保存数据,该数据对象需要遵守NSCoding协议,并且该对象对应的类必须提供encodeWithCoder:和initWithCoder:方法.前一个方法告诉系统怎么对对象进行编码,而后一个方法则是告诉系统怎么对对象进行解码。
②.NSUserDefaults:用来保存应用程序设置和属性,用户保存的数据.用户再次打开程序或者开机后这些数据仍然存在.NSUserDefaults可以存储的数据类型包括:NSData,NSString,NSNumber,NSDate,NSArray.NSDictionary,其他类型的数据需要先行转换。
③.Write写入方式:永久保存在磁盘中.具体:a.获得文件保存的路径.b.生成该路径下的文件,c,往文件中写入数据.d.从文件中读出数据。
④.SQLite:采用SQLite数据库来存储数据,SQLite作为一种轻量级数据库.具体:a.添加SQLite相关的库以及头文件,b.使用数据库存数数据:打开数据库,编写数据库语句,执行,关闭数据库.另:写入数据库,字符串可以采用char方式,而从数据库中取出char类型,当char类型有表示中文字符时,会出现乱码,这是因为数据库默认使用ascII编码方式,所以想要正确从数据库中取出中文,需要使用NSString来接受从数据库取出的字符串。
⑤.CoreData:原理是对SQLite的封装,开发者不需要接触sql语句,就可以对数据库进行操作。
30.简述应用程序按HOME键进入后台时的生命周期,以及从后台进入前台时的生命周期.
//前者:
- (void)applicationWillResignActive:(UIApplication *)application
- (void)applicationDidEnterBackground:(UIApplication *)application
//后者:
- (void)applicationWillEnterForeground:(UIApplication *)application
- (void)applicationDidBecomeActive:(UIApplication *)application
补充各个程序运行状态时代理的回调:
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
//告诉代理进程启动但还没进入状态保存
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
//告诉代理启动基本完成程序准备开始运行
- (void)applicationWillResignActive:(UIApplication *)application
//当应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件,比如来电话了
- (void)applicationDidBecomeActive:(UIApplication *)application
//当应用程序入活动状态执行,这个刚好跟上面那个方法相反
- (void)applicationDidEnterBackground:(UIApplication *)application
//当程序被推送到后台的时候调用。所以要设置后台继续运行,则在这个函数里面设置即可
- (void)applicationWillEnterForeground:(UIApplication *)application
//当程序从后台将要重新回到前台时候调用,这个刚好跟上面的那个方法相反。
- (void)applicationWillTerminate:(UIApplication *)application
//当程序将要退出是被调用,通常是用来保存数据和一些退出前的清理工作。这个需要要设置UIApplicationExitsOnSuspend的键值。
- (void)applicationDidFinishLaunching:(UIApplication*)application
//当程序载入后执行
在上面8个方法对应的方法中键入NSLog打印。
现在启动程序看看执行的顺序:
//启动程序
lifeCycle[40428:11303] willFinishLaunchingWithOptions
lifeCycle[40428:11303] didFinishLaunchingWithOptions
lifeCycle[40428:11303] applicationDidBecomeActive
//按下home键
lifeCycle[40428:11303] applicationWillResignActive
lifeCycle[40428:11303] applicationDidEnterBackground
//双击home键,再打开程序
lifeCycle[40428:11303] applicationWillEnterForeground
lifeCycle[40428:11303] applicationDidBecomeActive