1.Runloop和线程的关系
1.一一对应,主线程的runloop已经创建,子线程必须手动创建。
2.runloop在第一次获取时创建,在线程结束时销毁。
在runloop中有多个运行模式,但是只能选择一种模式运行,mode中至少要有一个
timer
或者source
Mode:
系统默认注册5个Mode:
kCFRunLoopDefaultMode
:App默认的mode,通常主线程在这个mode下运行UITrackingRunLoopMode
:界面跟踪mode,用于ScrollView追踪触摸滑动,保证滑动时,不受其它mode影响kCFRunLoopCommonModes
:占位用的mode,不是一个真正的mode,相当于NSDefaultRunLoopMode
+UITrackingRunLoopMode
UIInitializationRunLoopMode
:刚启动App时进入的第一个mode,启动完成之后不再使用。GSEventReceiveRunLoopMode
:接受系统事件的内部mode,通常用不到
2.自动释放池什么时候创建与释放?
第一次创建:启动runloop时候
最后一次销毁:runloop退出的时候
其它时候的创建和销毁:当runloop即将睡眠时销毁之前的释放池,重新创建一个新的
3.什么情况下使用weak关键字,和assign的区别?
1.ARC中,有可能出现循环引用的地方使用,比如delegate
属性
2.自定义IBOutlet
控件属性一般也是使用weak
区别:weak
表明一种非持有关系,必须用于OC对象;assign
用于基本数据类型
4.怎么用copy关键字?
1.NSString
、NSArray
、NSDictionary
等等经常使用copy
关键字,是因为他们有对应的可变类型:NSMutableString
、NSMutableArray
、NSMutableDictionary
;他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
5.@property(copy) NSMutableArray *array;这种写法会出什么问题?
1.添加、删除、修改数组内元素的时候,程序会因为找不到对应的方法而崩溃,因为copy
就是复制一个不可变NSArray
的对象
2.默认使用了atomic
属性会严重影响性能。
6.如何让自己的类用copy修饰符?即让自己写的对象具备拷贝功能?
1.需要生命该类遵循NSCopying
或NSMutableCopying
协议。
2.实现NSCopying
协议。该协议只有一个方法
- (id)copyWithZone:(NSZone *)zone;
7.@property的本质是什么?ivar、getter、setter如何生成并添加到这个类中?
本质:
@property = ivar + getter + setter;
实例变量 + getter方法 + setter方法
在编译期自动生成getter
、setter
,还自动向类中添加适当类型的实例变量,也可以用@synthesize
语法来制定实例变量的名字。
8.多线程
1.NSThread
线程的生命周期:线程任务执行完毕后被释放
//NSThread开启子线程的3种方式
- (void)createChildThread1 {
//创建线程
NSThread *thread = [NSThread alloc] initWithTarget:self selector:@selector(run:) object@"abc"];
//启动线程
[thread start];
}
- (void)createChildThread2 {
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"分离子线程"];
}
- (void)createChildThread3 {
[self performSelectorInBackground:@selector(run:) withObject:@"开启后台线程"];
}
2.线程安全(加互斥锁):
格式:@synchronized(self) {//需要锁定的代码}
注意:
- 锁必须全局唯一;
- 加锁的位置;
- 加锁的前提条件;
- 加锁的结果:线程同步
优点:防止多线程抢夺资源造成的数据安全问题
缺点:耗费CPU性能
使用前提:多线程使用同一块资源
atomic
:原子属性,为setter
方法加锁(默认就是atomic
),线程安全,耗费资源
nonatomic
:非原子属性,不会为setter
方法加锁,非线程安全
3.线程间通信
<1>. NSThread实现
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(Bool)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(Bool)wait;
<2>.GCD实现
子线程和主线程相互切换
<3>.NSOperationQueue实现
4.GCD(任务、队列)
<1>.同步和异步的区别
同步:只能在当前线程中执行任务,不能开启新线程
异步:可在新的线程中执行任务,能开启新线程
<2>队列
并发队列:可多个任务并发(同时)执行(会开启多个线程同时执行任务);只在异步函数(dispatch_async
)下有效
串行队列:一个任务执行完毕后,再执行下一个任务
主队列:主队列中的任务都在主线程中执行
//异步函数+并发队列:会开启多条新线程,队列中的任务是并发执行的
- (void)asyncConcurrent {
//创建队列
dispatch_queue_t queue = dispatch_queue_create("com.iloveu.download", DISPATCH_QUEUE_CONCURRENT);
//封装操作---添加任务到队列中
dispatch_async(queue, ^{
NSLog(@"download1---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download2--%@", [NSThread currentThread]);
});
}
//异步函数+串行队列:会开线程,开一条线程,队列中的任务是串行执行
- (void)asyncSerial {
//创建队列
dispatch_queue_t queue = dispatch_queue_create("com.iloveu.download", DISPATCH_QUEUE_SERIAL);
//封装操作---添加任务到队列中
dispatch_async(queue, ^{
NSLog(@"download1---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download2--%@", [NSThread currentThread]);
});
}
//同步函数+并发队列:不会开线程,任务是串行执行
- (void)syncConcurrent {
//创建队列
dispatch_queue_t queue = dispatch_queue_create("com.iloveu.download", DISPATCH_QUEUE_CONCURRENT);
//封装操作---添加任务到队列中
dispatch_sync(queue, ^{
NSLog(@"download1---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download2--%@", [NSThread currentThread]);
});
}
//同步函数+串行队列:不会开线程,任务是串行执行
- (void)syncSerial {
//创建队列
dispatch_queue_t queue = dispatch_queue_create("com.iloveu.download", DISPATCH_QUEUE_SERIAL);
//封装操作---添加任务到队列中
dispatch_sync(queue, ^{
NSLog(@"download1---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download2--%@", [NSThread currentThread]);
});
}
//异步函数+主队列:不会开线程,所有任务都在主线程中执行
- (void)asyncMain {
//创建队列
dispatch_queue_t queue = dispatch_get_main_queue();
//封装操作---添加任务到队列中
dispatch_async(queue, ^{
NSLog(@"download1---%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"download2--%@", [NSThread currentThread]);
});
}
//同步函数+主队列:死锁
- (void)syncMain {
//创建队列
dispatch_queue_t queue = dispatch_get_main_queue();
NSLog(@"start ---")
//封装操作---添加任务到队列中
dispatch_sync(queue, ^{
NSLog(@"download1---%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"download2--%@", [NSThread currentThread]);
});
NSLog(@"end ---")
}
方法
- (void)syncMain
如果在主线程上调用,则会发生死锁,即打印start ---
后,就会产生死锁;如果在子线程中调用,则不会发生死锁,所有NSLog
都能打印。
主队列特点:如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务,直到主线程空闲为止。
5.Runloop的应用:
NSTimer在子线程开启一个定时器;控制定时器在特定模式下执行
imageView的显示
performSelector
常驻线程(让一个子线程不进入消亡状态,等待其它线程发来消息,处理其它事件)
自动释放池
9.@protocol和category中如何使用@property?
1.在protocol
中使用@property
中会生成setter
和getter
方法生命,使用属性的目的,是希望遵守该协议的对象能实现该属性
2.category
使用@property
也是只会生成setter
和getter
方法声明,如果真的需要给category
增加属性的实现,需要借助于运行时的两个函数:
objc_setAssociatedObject
和objc_getAssociatedObject
10.@property中有哪些属性关键字?
1.原子性:nonatomic
特质
2.读/写权限:readwrite
、readonly
3.内存管理语义:assign
、strong
·、weak
、unsafe_unretained
、copy
4.方法名:getter=<name>
、setter=<name>
11.weak属性需要在dealloc中设置为nil么?
不需要,在ARC环境下,无论是强指针还是弱指针都无需在dealloc
设置为nil
,ARC会自动帮我们处理,即便是编译器不帮我们做这些,weak
也不需要在dealloc
中置nil
,runtime
内部已经帮我们实现了
12.@synthesize和@dynamic分别有什么作用?
1.@property
有两个对应的词,一个是@synthesize
,一个是@dynamic
。如果@synthesize
和@dynamic
都没写,那么默认的就是@synthesize var = _var;
2.@synthesize
的语义是如果你没有手动实现setter
和getter
方法,那么编译器会自动为你加上这两个方法
3.@dynamic
告诉编译器,属性的setter
和getter
方法由用户自己实现,不自动生成。(当然对于readonly
的属性只需要提供getter
方法即可)。假如一个属性被生命为@dynamic var
,然后你没有提供setter
和getter
方法,编译的时候没有问题,但是当程序运行到instance.var = someVar
,由于缺少setter
方法会导致程序崩溃;或者当运行到someVar = var
时,由于缺少getter
方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
13.ARC下,不显示指定任何属性关键字时,默认的关键字都有哪些?
1.基本数据类型:atomic
、readwrite
、assign
2.普通oc对象:atomic
、readwrite
、strong
14.用@property声明NSString(NSArray、NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
1.因为父类指针可以指向子类对象,使用copy
的目的是为了让本对象的属性不受外界影响,使用copy
无论给我传入是一个可变对象还是不可变对象,我本身持有的就是一个不可变的副本;
2.如果使用strong
,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
- 浅复制就是指针拷贝
- 深复制就是内容拷贝
不管是集合类对象还是非集合类对象,接收到copy
和mutableCopy
消息时,都遵循以下准则:
copy
返回不可变对象,所以如果对copy
返回值使用可变对象接口就会crash
;mutableCopy
返回可变对象
在非集合类对象中:
对不可变对象进行copy
操作,是指针复制,mutableCopy
操作时,是内容复制;对可变对象进行copy
和mutableCopy
都是内容复制,简单表示如下:
- [
不可变对象
copy] //浅复制- [
不可变对象
mutableCopy] //深复制- [
可变对象
copy] //深复制- [
可变对象
mutableCopy] //深复制
在集合类对象中,对不可变对象进行copy
是指针复制,mutableCopy
是内容复制;对可变对象进行copy
和mutableCopy
都是内容复制。
但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制,简单表示如下:
- [不可变对象 copy] //浅复制
- [不可变对象 mutableCopy] //单层深复制
- [可变对象 copy] //单层深复制
- [可变对象 mutableCopy] //单层深复制
15.@synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量么?
@synthesize
合成实例变量的规则,有以下几点:
- 如果制定了成员变量的名称,会生成一个指定的名称的成员变量
- 如果这个成员已经存在了就不再生成了
- 如果是
@synthesize foo;
还会生成一个名称为foo
的成员变量,也就是说:如果没有制定成员变量的名称会自动生成一个属性同名的成员变量- 如果是
@synthesize foo = _foo;
就不会生成成员变量了
假如property
名为foo
,存在一个名为_foo
的实例变量,那么还会自动合成新变量么?
不会
16.在有了自动合成属性实例变量后,@synthesize还有哪些使用场景?
1.同时重写了setter
和getter
时,系统就不会生成ivar
,使用@synthesize foo = _foo;
关联@property
与ivar
2.重写了只读属性的getter
时
3.使用了@dynamic
时
4.在@protocol
中定义的所有属性
5.在category
中定义的所有属性
6.重载的属性,当在子类中重载了父类中的属性,必须使用@synthesize
来手动合成ivar
17.objc中向一个nil对象发送消息将会发生什么?
在Objective-C中向nil发送消息是完全有效的,只是在运行时不会有任何作用,如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil),如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误
18.objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?
[obj foo];
在objc动态编译时,每个方法在运行时会被动态转为消息发送,即为objc_msgSend(obj, @selector(foo));
19.什么时候会报unrecognized selector的异常?
当调用该对象某个方法,而该对象上没有实现这个方法的时候,可以通过消息转发
进行解决。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉,并抛出异常unrecognized selector sent to XXX
。
但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
1.Method resolution
objc运行时会调用
resolveInstanceMethod:
或者+resolveClassMethod:
,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则运行时就会移到下一步,消息转发(Message Forwarding
)。
2.Fast forwarding
如果目标对象实现了
forwardingTargetForSelector:
,Runtime这时就会调用这个方法,给你把这个消息转发给其它对象的机会。只要这个方法返回的不是nil
和self
,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则就会继续Normal Forwarding
。这里叫Fast
,只是为了区别下一步的转发机制,因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation
对象,所以相对更快点。
3.Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送
-methodSignatureForSelector:
消息获取得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil,Runtime则会发出-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation
对象并发送-forwardInvocation:
消息给目标对象。
20.一个objc对象如何进行内存布局?(考虑有父类的情况)
1.所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储控件中。
2.每个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的:
- 对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)
- 成员变量的列表
- 属性列表
类对象内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass的指针,指向他的父类对象。
3.根对象就是NSObject,它的superclass指针指向nil
4.类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。
21.一个objc对象的isa指针指向什么?有什么作用?
指向他的类对象,从而可以找到对象上的方法
22.runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
每一个类对象中都有一个方法列表,方法列表中记录着方法名称、方法实现、参数类型,其实selector
本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现。
23.下面代码输出什么?
@implementation Son: Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案都输出Son
这个题目主要是考察关于Objective-C中对self
和super
的理解。
我们都知道self
是类的隐藏参数,指向当前调用方法的这个类的实例。其实super
是一个Magic Keyword
,它本质是一个编译器标示符,和self
是指向的同一个消息接受者.
他们两个的不同点在于:super
会告诉编译器,调用class
这个方法时,要去父类的方法,而不是本类里的。当使用self
调用方法时,会从当前类的方法列表中开始找,如果没有就从父类中再找,而当使用super
时,则从父类的方法列表中开始找,然后调用父类的这个方法。
24.使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要。
被关联的对象在生命周期内要比对象本身释放的晚很多。它们会在被NSObject -dealloc调用的object_dispose()
方法中释放
//对象内存销毁的时间表
1.调用-release:
引用基数变为零
- 对象正在被销毁,生命周期即将结束
- 不能再有新的
__weak
弱引用,否则将指向nil
- 调用
[self dealloc]
2.子类调用-dealloc
- 继承关系中最底层的子类在调用
-dealloc
- 如果是
MRC
代码,则会手动释放实例变量们(iVars)- 集成关系中每一层的父类都在调用
-dealloc
3.NSObject调用-dealloc
- 只做一件事:调用Objective-C runtime中的
object_dispose()
方法
4.调用object_dispose()
- 为
C++
的实例变量们(iVars)调用destructors
- 为ARC状态下的实例变量们(iVars)调用
-release
- 解除所有使用runtime Associate方法关联的对象
- 解除所有
__weak
引用- 调用
free()
25.objc中的类方法和实例方法有什么本质区别和联系?
类方法:
1.类方法是属于类对象的
2.类方法只能通过类对象调用
3.类方法中的self是类对象
4.类方法可以调用其它的类方法
5.类方法中不能访问成员变量
6.类方法中不能直接调用对象方法
实例方法:
1.实例方法是属于实例对象的
2.实例方法只能通过实例对象调用
3.实例方法中的self是实例对象
4.实例方法中可以访问成员变量
5.实例方法中直接调用实例方法
6.实例方法中也可以调用类方法(通过类名)
26._objc_msgForward函数是做什么的,直接调用它将会发生什么?
_objc_msgForward
是一个函数指针(和IMP
类型一样),用于消息转发的,当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发。
objc_msgSend
在"消息传递"中的作用:
在"消息传递"过程中,objc_msgSend
的动作比较清晰,首先在Class中的缓存查找IMP
(没缓存则初始化缓存),如果没找到,则向父类的Class查找,如果一直查找到根类仍旧没有实现,则用_objc_msgForward
函数指针代替IMP
。最后执行这个IMP
。
_objc_msgForward
消息转发做的几件事:
1.调用resoleInstanceMethod:
方法(或resolveClassMethod:
)。允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES
,那么重新开始objc_msgSend
流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod
。如果仍没寮现,继续下面的动作。
2.调用forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil,继续下面的动作,注意,这里不要返回self
,否则会形成死循环。
3.调用methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。如果能获取,则返回非nil
,创建一个NSInvocation
并传给forwardInvocation:
4.调用forwardInvocation:
方法,将第3步获取到的方法签名包装成Invocation
传入,如何处理就在这里面了,并返回非nil
。
5.调用doesNotRecognizeSelector
,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
上面前4个方法均是模板方法,开发者可以override
,由runtime
来调用。最常见的实现消息转发就是重写方法3和4,吞掉一个消息或者代理给其它对象都是没问题的,也就是说_objc_msgForward
在进行消息转发的过程中会涉及以下这几个方法:
resolveInstanceMethod:
方法(或resolveClassMethod:
)
2.forwardingTargetForSelector:
方法
3.methodSignatureForSelector:
方法
4.forwardInvocation:
方法
5.doesNotRecognizeSelector:
方法
一旦调用_objc_msgForward
,将跳过查找IMP
的过程,直接触发消息转发
,如果调用了_objc_msgForward
,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend:
,我没有在这个对象里找到这个方法的实现,如果用不好会直接导致程序Crash
,但是如果用的好,做很多事情,比如JSPatch
、RAC(ReactiveCocoa)
27.runtime如何实现weak变量的自动置nil?
runtime
对注册的类,会进行布局,对于weak
对象会放入一个hash
表中,用weak
指向的对象内存地址作为key
,当此对象的引用基数为0的时候会dealloc,假如weak
指向的对象内存地址是a
,那么就会以a
为键,在这个weak
表中搜索,找到所有以a
为键的weak
对象,从而设置为nil
weak
修饰的指针默认值是nil
,在Objective-C中,向nil
发送消息是安全的。
假如a和b指向同一个内存地址,在b变nil时,a变nil,此时向a发送消息不会崩溃
如果a是由assign
修饰的,则在b变nil时,a还是指向该内存地址,变野指针,此时向a发送消息极易崩溃
28.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量。
能向运行时创建的类中添加实例变量。
解释下:
- 因为编译后的类已线注册在
runtime
中,类结构体中的objc_ivar_list
实例变量的链表和instance_size
实例变量的内存大小已经确定,同时runtime
会调用class_setIvarLayout
或class_setWeakIvarLayout
来处理strong weak引用。所以不能向存在的类中添加实例变量- 运行时创建的类是可以添加实例变量,调用
class_addIvar
函数。但是得在调用objc_allocateClassPair
之后,objc_registerClassPair
之前,原因同
29.runloop和线程有什么关系?
实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop。每个线程,包括程序的主线程都有与之相应的run loop对象。
runloop和线程的关系:
1.主线程的run loop是默认启动的
iOS的应用程序里面,程序启动后会有一个如下的main()
函数
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
};
}
重点是UIApplicationMain()
函数,这个方法会为main thread
设置一个NSRunLoop
对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
2.对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互原则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
3.在任何一个Cocoa程序的线程中,都可以通过以下代码来获取到当前线程的run loop。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
30.runloop的mode作用是什么
mode主要是用来制定事件在运行循环中的优先级的,分为:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
默认,空闲状态UITrackingRunLoopMode
ScrollView滑动时UIInitializationRunLoopModel
启动时NSRunLoopCommonModes
(kCFRunLoopCommonModes)mode集合
苹果公开提供的Mode有两个
-
NSDefaultRunLoopMode
(kCFRunLoopDefaultMode)
2.NSRunLoopCommonModes
(kCFRunLoopCommonModes)
31.以+scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂停回调,为什么?如何解决?
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
的mode会切换到UITrackingRunLoopMode
来保证ScrollView的流畅滑动:只有在NSDefaultRunLoopMode
模式下处理的事件会影响ScrollView的滑动。
如果我们把一个NSTimer
对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
添加到主运行循环中的时候,ScrollView滚动过程中会因为mode
切换,而导致NSTimer
将不再被调度。
同时因为mode还是可以定制的,所以:
Timer计时会被ScrollView的滑动影响的问题可以通过将timer
添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)
来解决。
32.猜想runloop内部是如何实现的?
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
int main(int argc, char *argv[]) {
//程序一直运行状态
while(AppIsRunning) {
//睡眠状态,等待唤醒事件
id whoWakesMe = SleepForWakingUp();
//得到唤醒事件
id event = GetEvent(whoWakesMe);
//开始处理事件
HandleEvent(event);
}
return 0;
}
33.objc使用什么机制管理对象内存?
通过retainCount
的机制来决定对象是否需要释放,每次runloop
的时候,都会检查对象的retainCount
,如果retainCount
为0
,说明该对象没有地址需要使用了,可以释放掉了。
34.ARC通过什么方式帮助开发者管理内存?
ARC相对于MRC,不是在编译时添加retain/release/autorelease
这么简单,应该是编译期和运行期两部分共同帮助开发者管理内存。
在编译期,ARC用的是更底层的C接口实现的retain/release/autorelease
,这样做性能更好,也是为什么不能在ARC环境下手动retain/release/autorelease
,同时对同一上下文的同一对象的成对retain/release
操作进行优化(即忽略掉不必要的操作);ARC也包含运行期组件,这个地方做的优化比较复杂,但也不能被忽略。
35.BAD_ACCESS在什么情况下出现?
访问了野指针,比如对一个已经释放的对象执行了release
、访问已经释放对象的成员变量或发消息、死循环。
36.使用block时什么情况会发生循环引用,如何解决?
一个对象中强引用了block,在block中又强引用了该对象,就会发生循环引用。
解决办法是将该对象使用__weak
或者__block
修饰符修饰之后再在block中使用
37.在block内如何修改block外部变量?
Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。
__block
所起到的作用就是只要观察到该变量被block所持有,就将外部变量
在栈中的内存地址放到了堆中。block内部的变量会被copy
到堆区,进而block内部也可以修改外部变量的值。block也属于函数
的范畴,变量进入block
,实际就是已经改变了作用域。
38.苹果是如何实现autoreleasepool的?
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_autorelease
看函数名就可以知道,对autorelease分别执行push
和pop
操作,销毁对象时执行release
操作。
39.不手动指定autoreleasepool的前提下,一个autorelease对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
分两种情况:手动干预释放时机、系统自动去释放。
1.手动干预释放时间:指定autoreleasepool就是所谓的,当前作用域大括号结束时释放。
2.系统自动去释放:不手动指定autoreleasepool
Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的runloop迭代结束时释放。
从程序多少厘米怀到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。
所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中
但是如果每次都放进应用程序的main.m
中的autoreleasepool中,迟早有被撑满的一刻,这个过程中必定有一个释放动作。何时?在一次完整的运行循环结束之前,会被销毁。那什么时候会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。
子线程的runloop默认是不工作的,无法主动创建,必须手动创建。
自定义的NSOperation
和NSThread
需要手动创建自动释放池。比如:自定义的NSOperation
类中的main
方法里就必须添加自动释放池,否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。
但对于blockOperation
和invocationOperation
这种默认的Operation
,系统已经帮我们封装好了,不需要手动创建自动释放池。
@autoreleasepool当自动释放池被销毁或耗尽时,会向自动释放池中的所有对象发送release消息,释放自动释放池中的所有对象
如果在一个vc的viewDidLoad中创建一个Autorelease对象,那么该对象会在viewDidAppear方法执行前就被销毁了。
40.使用系统的某些block api(如UIView的block版本写动画时),是否也考虑循环引用的问题?
UIView的block版本写动画时不需要考虑,所谓『循环引用』是指双向的强引用,所以那些『单向强引用』(block强引用self)没有问题,比如这些不用考虑:
[UIView animateWithDuration:duration animations:^{
[self.superview layoutIfNeeded];
}];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.someProperty = xyz;
}];
[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification){
self.someProperty = xyz;
}];
如果你使用一些参数中可能含有ivar
的系统api,如GCD
、NSNOtificationCenter
就要小心一点:比如GCD
内部如果引用了self
,而且GCD
的其它参数是ivar
,则要考虑循环引用:
__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^{
__typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
});
类似的:
__weak __typeof__(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"tetKey" object:nil queue:nil usingBlock:^(NSNotification *note){
__typeof__(self) strongSelf = weakSelf;
[strongSelf dismissModalViewControllerAnimated:YES];
}];
self --> _observer --> block --> self 显然这也是一个循环引用。
41.GCD队列(dispatch_queue_t)分哪两种类型?
1.串行队列Serial Dispatch Queue
- 并行队列
Concurrent Dispatch Queue
42.如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
使用Dispatch Group
追加block
到Global Group Queue
,这些block
如果全部执行完毕,就会执行Main Dispatch Queue
中的结束处理的block
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1*/ });
dispatch_group_async(group, queue, ^{ /*加载图片2*/ });
dispatch_group_async(group, queue, ^{ /*加载图片3*/ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//合并图片
});
实际代码如下:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();
//下载图片1,放到线程数组中
dispatch_group_async(group, queue, ^{
NSURL *url = [NSURL URLWithString:@""];
NSData *imgData1 = [NSData dataWithContentsOfURL:url];
self.img1 = [UIImage imageWithData:imgData1];
});
//下载图片2,放到线程数组中
dispatch_group_async(group, queue, ^{
NSURL *url = [NSURL URLWithString:@""];
NSData *imgData2 = [NSData dataWithContentsOfURL:url];
self.img2 = [UIImage imageWithData:imgData2];
});
dispatch_group_notify(group, queue, ^{
NSLog(@"combie--%@", [NSThread currentThread]);
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
[self.img1 drawInRect:CGRectMake(0, 0, 200, 100)];
self.img1 = nil;
[self.img2 drawInRect:CGRectMake(0, 100, 200, 100)];
self.img2 = nil;
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"UI---%@",
[NSThread currentThread]);
});
});
43.dispatch_barrier_async的作用是什么?
在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续执行,使用barrier
来等待之前任务完成,避免数据竞争等问题。dispatch_barrier_async
函数会等待追加到Concurrent Dispatch Queue
并行队列中的操作全部执行完之后,然后再执行dispatch_barrier_async
函数追加的处理,等dispatch_barrier_async
追加的处理执行结束之后,Concurrent Dispatch Queue
才回复之前的动作继续执行。
注意:使用
dispatch_barrier_async
,该函数只能搭配自定义并行队列dispatch_queue_t
使用。不能使用:dispatch_get_global_queue
,否则dispatch_barrier_async
的作用会和dispatch_async
的作用一模一样。
44.苹果为什么要废弃dispatch_get_current_queue?
dispatch_get_current_queue
容易造成死锁
45.以下代码运行结果如何?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
只输出1。发生主线程锁死
46.addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?
添加键值观察
1.观察者,负责处理监听事件的对象
2.观察的属性
3.观察的选项
4.上下文
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
observer中需要实现一下方法:
所有的KVO
监听到事件,都会调用此方法
1.观察的属性
2.观察的对象
3.change属性变化字典(新/旧)
4.上下文,与监听的时候传递的一致
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
48.如何手动触发一个value的KVO?
所谓的『手动触发』是区别于『自动触发』:
自动触发是指类似的这种场景:在注册KVO之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。
自动触发KVO的原理:
键值观察通知依赖于NSObject的两个方法willChangeValueForKey:
和didChangeValueForKey:
。在一个被观察属性发生改变之前,willChangeValueForKey:
一定会被调用,这就会记录旧的值,而改变发生后:observeValueForKey:ofObject:change:context:
会被调用,继而didChangeValueForKey:
也会被调用。如果可以手动实现这些调用,就可以实现『手动触发』了。
手动触发的使用场景是什么?
一般我们只在希望能控制"回调的调用时机"时才会这么做。而"回调的调用时机"就是在你调用didChangeValueForKey:
方法时。
如果这个value
是表示时间self.now
,那么代码如下:最后两行代码缺一不可。
//@property (nonatomic, strong) NSDate *now;
- (void)viewDidLoad {
[super viewDidLoad];
_now = [NSDate date];
[self addObserver:self forKeyPath:@"now" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"1");
[self willChangeValueForKey@"now"];
NSLog(@"2");
[self didChangeValueForKey:@"now"];
NSLog(@"4");
}
但是平时我们一般不会这么干,我们都是等系统去"自动触发"。"自动触发"的实现原理:
比如调用:setNow:
时,系统还会以某种方式在中间插入willChangeValueForKey:
、didChangeValueForKey:
和observeValueForKeyPath:ofObject:change:context:
的调用。
49.若一个类有实例变量NSString *_foo,调用setValue:forkey:时,可以以foo还是_foo作为key?
都可以。
50.KVC的keyPath中的集合运算符如何使用?
1.必须用在集合对象上或普通对象的集合属性上
2.简单集合运算符有@avg
、@count
、@max
、@min
、@sum
3.格式@""@sum.age
或@"集合属性.@max.age"
51.KVC和KVO的keyPath一定是属性么?
KVC支持实例变量;
如果将一个对象设成属性,这个属性是自动支持KVO的;如果这个对象是一个实例变量,那么这个KVO是需要我们自己来实现的,手动支持。
手动设定实例变量的KVO实现监听:
@interface Student : NSObject
{
NSString *_age;
}
- (void)setAge:(NSString *)age;
- (NSString *)age;
@end
//手动设定KVO
- (void)setAge:(NSString *)age {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
- (NSString *)age {
return _age;
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
//如果监测到键值为age,则指定为非自动监听对象
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
52.如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?
利用Runtime 动态创建类、实现KVO
学习链接
53.apple用什么方式实现对一个对象的KVO?
KVO实现:
在使用KVC命名约定时,当你观察一个对象时,一个新的类会被动态创建,这个类继承自己该对象的原本类,并重写被观察属性的setter
方法。重写setter
方法会负责在调用原setter
方法之前和之后,通知所有观察对象,值的更改,最后通过isa
混写(isa-swizzling)把这个对象的isa
指针(isa指针告诉Runtime系统这个对象的类是什么)指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
Apple使用了isa混写(isa-swizzling)来实现KVO。
KVO在调用存取方法之前总是调用willChangeValueForKey:
,之后总是调用didChangeValueForKey:
。怎么做到的呢?答案是通过isa混写(isa-swizzling)
。第一次对一个对象调用addObserver:forKeyPath:options:context:
时,框架会创建这个类的新的KVO子类,并将被观察对象转换为新子类的对象。在这个KVO特殊子类中,Cocoa创建观察属性的setter
,大致工作原理如下:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"];
[super setValue:aDate forKey:@"now"];
[self didChangeValueForKey:@"now"];
}
54.IBOutlet连出来的视图属性为什么可以被设置成weak?
因为有外链,那么视图在xib或storyboard中肯定存在,视图已经对它有一个强引用了。不过这个回答漏了个重要知识,使用storyboard(xib不行)创建的vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryBoard
的私有数组强引用所有top level
的对象,所以这时即便outlet
声明成weak也没关系。
55.IB中User Defind Runtime Attributes如何使用?
它能够通过KVC的方式配置一些你在interface builder
中不能配置的属性。当你希望在IB中作尽可能多的事情,这个特性能够帮助你编写更加轻量级的viewController.
56.如何调试BAD_ACCESS错误
1.重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS
前访问的最后一个object
2.通过Zombie
3.设置全局断点快速定位问题代码所在行
4.Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer
。用法如下:在Edit Scheme
中的Diagnostics
中,勾选Address Sanitizer
Address Sanitizer开启之后,在debug
过程中,如遇到EXC_BAD_ACCESS
的问题,Xcode会自动中断,抛出异常。
57.lldb(gdb)常用的调试命令?
1.breakpoint
设置断点定位到某一个函数
2.n 断点指针下一步
3.po打印对象
58.iOS容易引起"循环引用"的几种场景
最简单的理解:对象A持有对象B,对象B又持有对象A,就会造成循环引用
1.parent-child相互持有、委托模式
2.block:
隐式(间接)循环引用。ObjectA持有ObjectB,ObjectB持有block。那么在block中调用ObjectA的self会造成间接循环引用;即使在你的block代码中没有显式地出现
self
,也会出现循环引用!只要你在block里用到了self所拥有的东西!
显示循环引用,编译器会报警告,在block引用self的时候最好使用weak-strong dance
技术。
3.NSTimer:
NSTimer会持有对象,所以在删除对象之前,需要[timer invalidate]
。
@interface FtKeepAlive : NSObject
{
NSTimer * _keepAliveTimer;
}
//实现文件
_keepAliveTimer = [NSTimer scheduledTimerWithTimeInterval:_expired target:self selector:@selector(keepLiveStart) userInfo:nil repeats:YES];
如上代码,类持有_keepAliveTimer,_keepAliveTimer又持有self,造成循环引用。
4.比如把self
加入array
中,也会造成循环引用
5.使用类别添加属性
有一个类A,给A动态添加属性p,如果p中再引用类A,容易造成循环引用。
59.类方法load和initialize的区别
ios会在运行期提前并且自动调用这两个方法。
区别:load
是只要类所在文件被引用就会被调用,而initialize
是在类或者其子类的第一个方法被调用前调用(runtime对+ (void)load
的调用并不视为类的第一个方法)。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用,但是没有使用,那么initialize
也不会被调用。
相同点在于:方法只会被高调用一次。(其实这是相对runtime来说的)。
方法调用的顺序:父类(Superclass)的方法优先于子类(Subclass)的方法,类中的方法优先于类别(Category)中的方法。
当类对象被引入项目时,runtime会向每一个类对象发送load消息,load方法还是非常神奇的,因为它会在每一个类甚至分类被引入时仅调用一次,调用的顺序是父类优先于子类,子类优先于分类,而且load方法不会被类自动继承,每一个类中的load方法都不需要像viewDidLoad方法一样调用父类的方法
60.HTTP和HTTPS
HTTP是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从www
服务器传输超文本到本地浏览器的传输协议。HTTP是采用明文形式进行数据传输,极易被不法分子窃取和篡改。
HTTPS是在HTTP上建立SSL
加密层,并对传输数据进行加密,是HTTP协议的安全版。HTTPS主要作用是:
1.对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;
2.对网站服务器进行真实身份认证。
区别:
1.HTTPS是加密传输协议,HTTP是明文传输协议;
2.HTTPS需要用到SSL证书,而HTTP不用;
3.HTTPS比HTTP更安全,对搜索引擎更友好
4.HTTPS标准端口443
,HTTP标准端口80
5.HTTPS基于传输层,HTTP基于应用层
6.HTTPS在浏览器显示绿色安全锁,HTTP没有显示
61.常见的Exception Type
1.EXC_BAD_ACCESS
此类型的Excpetion是我们最常碰到的Crash,通常用于访问了不该访问的内存导致。一般EXC_BAD_ACCESS后面的"()",还会带有补充信息。
SIGSEGV:通常由于重复释放对象导致,这种类型在切换了ARC以后应该已经很少见到了
SIGABRT:收到Abort信号退出,通常Foundation库中的容器为了保护状态正常会做一些检测,例如插入nil到数组中等会遇到此类错误
SEGV:(Segmentation Violation),代表无效内存地址,比如空指针,未初始化指针,浅溢出等
SIGBUS:总线错误,与SIGSEGV
不同的是,SIGSEGV
访问的是无效地址,而SIGBUS
访问的是有效地址,但总线访问异常(如地址对齐问题)
SIGILL:尝试执行非法的指令,可能不被识别或没有权限
2.EXC_BAD_INSTRUCTION
此类异常通常由于线程执行非法指令导致
3.EXC_ARITHMETIC
除零错误会抛出此类异常
处理此类异常方式:符号化Crash日志,比较常用的有Crashlytics、Flurry、友盟等。
62.Category和Extension
Category的方法不一定非要在@implementation中实现,也可以在其它位置实现,但是当调用Category方法时,依据继承树没有找到该方法的实现,程序则会崩溃。
Category理论上不能添加变量,但是可以使用@dynamic来弥补这种不足
Category为原始类添加方法,必须要小心不要去重写已经存在的方法
Extension中的方法必须在@implementation中实现,否则编译会报错
Extension管理类的私有方法
63.响应者链(responder chain)
响应者链有以下特点:
- 响应者链通常是由
initial view
开始- UIView的
nextResponder
它的superview,如果UIView已经是其所在的UIViewController的top view,那么UIView的nextResponder就是UIViewController
UIViewController如果有Super ViewController,那么它的nextResponder为其Super ViewController最表层的View;如果没有,那么它的nextResponder就是UIWindow- UIWindow的contentView指向UIApplication,将其作为nextResponder
UIApplication是一个响应者链的终点,它的nextResponder指向nil,整个responder chain结束
Hit-Test View与Hit-Testing
1.png
假设用户触摸了上图的View E区域,那么iOS将会按下面的顺序反复检测subview来寻找Hit-Test View
1.触摸区域在视图A内,所以检测视图A的subview B和C
2.触摸区域不在视图B内,但是在视图C内,所以检查视图C的subview D和E
3.触摸区域不在视图D内,在视图E中
视图E在整个视图体系中是lowest view,所以视图E就是Hit-Test View。
事件的链有两条:事件的响应链、Hit-Testing时事件的传递链
1.响应链:由离用户最近的view向系统传递。init view -> superView -> ... -> view controller -> window -> Application -> AppDelegate
2.Hit-Testing链:由系统向离用户最近的view传递。 UIKit -> active app's event queue -> window -> root view -> ... -> lowest view
64.UITableView的优化
1.重用cell
2.缓存行高
- 若cell定高,删除代理中的方法
tableView:heightForRowAtIndexPath:
方法,设置self.tableView.rowHeight == 88;
- 若cell不定高,不要设置
estimatedHeightForRow
(因为estimatedHeightForRow
不能和heightForRow
里面的layoutIfNeed
同时存在,这两者同时存在会出现"窜动"的bug),解决方法是在请求到数据的时候提前计算好行高,用个字典缓存好高度;
3.加载网络数据,下载图片,使用异步加载,并缓存,从网络搂回来图片后先根据需要显示的图片大小切成合适大小的图,每次只显示处理过大小的图片,当查看大图时在显示大图,如果服务器能直接返回预处理好的小图和图片的大小更好,图片数量多时,必要的时候要准备好预览图和高清图,需时再加加载高清图,图片的"懒加载"方法,即延迟加载,当滚动速度很快时避免频繁请求服务器数据。
4.使用局部刷新
尽量不要使用reloadData
,刷新某一分组、某一行,使用对应的方法做局部刷新
5.渲染,尽量少用或不用透明图层
将cell的opaque
值设为YES
,背景色和子view不要使用clearColor
,尽量不要使用阴影渐变、透明度也不要设置为0
6.少用addSubView
给cell动态添加子view,初始化时直接设置好,通过hidden控制显示隐藏,随着时间也在初始化时直接布局好,避免cell的重新布局
7.如果cell内显示的内容来自己web,使用异步加载,缓存请求结果
8.按需加载cell,cell滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定n行加载。滚动很快时,只加载目标范围内的cell,这样按需加载,极大地提高了流畅性
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载
- (void)scrollViewEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if (labs(cip.row - ip.row) > skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
if (velocity.y < 0) {
NSIndexPath *indexPath = [temp lastObjet];
if (indexPath.row+33) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
记得在tableView:cellForRowAtIndexPath:方法中加入判断
if (needLoadArr.count > 0 && [needLoadArr indexOfObject:indexPath] == NSNotFound) {
[cell clear];
return;
}
滚动很快时,只加载目标范围内的cell,这样按需加载,极大的提高流畅度。
9.遇到复杂界面,像朋友圈涉及图文混排的,需要异步绘制,集成UITableViewCell,给自定义的Cell添加draw
方法,在方法中利用GCD
异步绘制,或者直接重新drawRect
方法,但如果在重写drawRect
方法就不需要用GCD异步线程了,因为drawRect
本来就是异步绘制
绘制cell不建议使用UIView,建议使用CALayer
。UIView的绘制是建立在CoreGraphic
上的,使用的是CPU。CALayer使用的是Core Animation
,CPU、GPU通吃,由系统决定使用哪个。View的绘制使用的是自下向上的一层一层的绘制,然后渲染Layer处理的是Texture,利用GPU的Texture Cache和独立的浮点数计算单元加速纹理的处理,GPU不喜欢透明,所以所有的绘图一定要弄成不透明,对于圆角和阴影这些,可以截个伪透明的图然后绘制上去。在layer里的回调一定也只做绘图,不做计算!
cell被重用时,它内部绘制的内容不会被自动清除,因此需要调用setNeedsDisplay
或setNeedsDisplayInRect
方法
65.离屏渲染(Offscreen-Renderd)
下面的情况或操作会引发离屏渲染:
- 为图层设置遮罩(layer.mask)
- 将图层的layer.masksToBounds / view.clipsToBounds属性设置为true
- 将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
- 为图层设置阴影(layer.shadow *)
- 为图层设置layer.shouldRasterize = true(光栅化)
- 具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层(圆角,抗锯齿)
- 文本(任何种类,包括UILabel,CATextlayer,Core Text等)。
- 使用CGContext在drawRect: 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现
优化方案
圆角优化:使用CAShapeLayer
和UIBezierPath
设置圆角;直接覆盖一张中间为圆形透明的图片(推荐使用)
shadow优化:使用ShadowPath指定layer阴影效果路径,优化性能
使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
设置layer的opaque值为YES,减少复杂图层合成
尽量使用不包含透明通道的图片资源
尽量设置layer的大小值为整形值
65.UIView和CALayer
UIView,iOS中所有的界面元素都继承自它,它本身完全是由CoreAnimation来实现的。每一个UIView内部都默认关联着一个layer,真正绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理
UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示
UIView的layer树形在系统内部,被维护着三份copy(presentLayer Tree、modeLayer Tree、 render Tree),修改动画的属性,其实是layer的presentLayer的属性值
动画的运作:对UIView的subLayer(非主Layer)属性进行更改,系统将自动进行动画生成。
区别:
1.UIView集成自UIResponder,能接收并响应事件,负责显示内容的管理,而CALayer继承自NSObject,不能响应事件,负责显示内容的绘制
2.UIView侧重于展示内容,而CALayer则侧重于图形和界面的绘制
3.当View展示的时候,View是layer的CALayerDelegate,View展示的内容是由CALayer进行display的
4.view内容展示依赖CALayer对内容的绘制,UIView的frame也是由内部的CALayer进行绘制
5.对UIView的属性修改,不会引起动画效果,但是对于CALayer的属性修改,是支持默认动画效果的,在view执行动画的时候,view是layer的代理,layer通过actionForLayer:forkey向对应的代理view请求动画action
6.每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都邮内部的Layer所提供,layer比view多了个anchorPoint
7.一个CALayer的frame是由其anchorPoint,position,bounds,transform共同决定的,而一个UIView的frame只是简单地返回CALayer的frame,同样UIView的center和bounds也只是简单的返回CALayer的Position和Bounds对应属性
66.TCP和UDP
TCP:传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之前建立一个TCP连接,一个TCP连接必须要经过三次握手
才能建立起来,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检查数据,流量控制等功能,保证数据能从一端传到另一端。
UDP:用户数据包协议,是面向数据报的运输层协议。它是面向非连接的协议,它不与对方建产连接,而是直接把数据包发送过去!UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。UDP不提供可靠性,它只是把应用程序传给IP层的数据包发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据包前不用在客户端和服务器之间建立一个连接,且没有超时重发等机制,故而传输数独很快
tcp协议和udp协议的差别
tcp-面向连接 udp-面向非连接
tcp-传输可靠 udp-不可靠
tcp-传输大量数据 udp-少量数据
tcp-速度慢 udp-快
TCP连接的三次握手
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND
态态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个syn包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV
状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP连接都将被一直保持下去,断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过"四次握手"
67.socket和http
socket连接和http连接的区别:
http是基于socket之上的,socket是一套完整的tcp、udp协议接口。
http协议:简单对象访问协议,对应于应用层,http协议是基于TCP连接的
tcp协议:对应于传输层
ip协议:对应于网络层
TCP/IP是传输层协议,主要解决数据如何在网络中传输;而http是应用层协议,主要解决如何包装数据
socket是对tcp/ip协议的封装,socket本身并不是协议,而是一个调用接口,通过socket,我们才能使用tcp/ip协议
http连接:短连接,即客户端向服务器端发送一次请求,服务端响应后连接,请求结束后,会主动释放连接即会断掉
socket连接:长连接,理论上客户端和服务端一旦简历连接将不会主动断掉;但由于各种因素可能会断开,比如:服务端或客户端主机down了,网络故障,或两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源,所以当一个socket连接中没有数据的传输,为了维持连接需要发送心跳消息
socket建产网络连接的步骤
建立socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket
,另一个运行于服务器端,称为ServerSocket
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
1.服务器监听:服务端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
2.客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务端套接字的地址和端口号,然后就向服务端套接字提出连接请求。
3.连接确认:当服务端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务端套接字继续处于监听状态,继续接收其它客户端套接字的连接请求。
socket变成中(推荐研究GCDAsyncSocket)的断线重连机制
断线重连:使得IM软件能够长在线,或者短时间内掉线,最好可以做到用户无感知。目的是让IM软件维持在线的状态。
实现方法:
IM客户端始终尽可能的保持连接,跟服务器的连接,客户端维护已登录状态,以便断线重连。从逻辑层次上来说,断线重连的逻辑是基于登录逻辑的,首次登录成功后,都有可能断线重连。
断线重连,实质分两步:一、使客户端断线;二、让客户端重连服务器。一般来说这两步是一个有前后顺序,完整的过程。
一、使客户端断线,即让客户端处于"未连接"状态,以下情况将触发这个事件:
1.网络切换,如从wifi切换到4G,网络事件。
2.网络连接失败、网络不可用。
3.心跳失败、心跳超时,统称心跳失败。
4.IM软件后台运行即将结束
二、让客户端重连服务器,客户端根据以下几种情况实现重连服务器。
1.iOS系统"网络可用"的通知
2.IM软件切换到前台,用户触发事件
3.网络切换,如从wifi切换到4G,网络事件
4.心跳失败的事件
5.客户端重新启动事件
断线重连的场景总结为:
1.重新启动(自动登录),需要提前加载用户缓存,保证用户到达主界面后能看到历史信息
2.网络错误,网络切换,网络连接失败有很多种,不同的场景,客户端要使用不同的逻辑处理。
3.心跳失败
心跳超时,失败统称为心跳失败。这个案例说明当前客户端--服务器连接已经损坏,或者当前用户身份有变化。心跳失败后首先将客户端离线,然后进行断线重连操作,避免心跳失败和网络错误事件一并发生,造成两次登录
4.网络可达或者切换到前台
为了避免重复登录,当IM软件处于"登录成功"、"连接中"或者"已注销"的几个状态的时候,客户端忽略"网络可达或者切换到前台"事件。
客户端心跳
IM基本的底层逻辑中有"心跳"概念,即客户端定时向Server发送一个信令包,表示客户端还"活着",注意,是客户端发起的。那么心跳终止了会发生什么事情呢?分两种情况:Server主动断开socket,客户端主动断开socket。
1.Server主动断开socket
Server只是接收客户端发起的心跳,假如,Server长时间没有收到客户端的心跳,SerVer认为客户端已经"死了",主动断开这个连接,此时客户端可能就是假在线了。
2.客户端断开socket
客户端对待心跳,要比Server麻烦一些,客户端要关注两个值:
- 心跳间隔值,即客户端多长时间发一次心跳?
- 心跳的超时时间。客户端发送一次心跳,如果长时间得不到Server应答,代表网络糟糕,客户端需要断开socket,主动离线
很明显,第二点就是客户端主动断开的情况,一般情况下,超时时间为60秒
网上也有争论:到底是否需要心跳,微信是没有心跳的,qq和飞信有心跳。
也有专家说心跳包已经影响到移动网络,因为心跳是定时频繁发送
下面是"心跳失败"引起的断线重连的流程图
WX20190705-175405@2x.png
68.远程推送APNS
远程推送的基本过程
1.客户端的app需要将用户的UDID和bundleID发送给apns服务器,进行注册,apns将加密后的device Token返回给app;
2.app获得device Token后,上传到公司服务器;
3.当需要推送通知时,公司服务器会将推送内容和device Token一起发给apns服务器;
4.apns再将推送内容推送到相应的客户端app上
69.iOS应用程序生命周期
iOS程序启动原理(过程):如下
WX20190705-175806@2x.png
appdelegate执行顺序:如下
WX20190705-175830@2x.png
1、应用程序的状态
状态如下 :
Not running
未运行,程序没启动Inactive
未激活,程序在前台运行,不过没有接收到事件。在没有事件处理情况下程序通常停留在这个状态Active
激活, 程序在前台运行而且接收到了事件。这也是前台的一个正常的模式Background
后台,程序在后台而且能执行代码,大多数程序进入这个状态后会在这个状态上停留一会。时间到之后会进入挂起状态(Suspended)。有的程序经过特殊的请求后可以长期处于Background状态Suspended
挂起,程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就会把挂起的程序清除掉,为前台程序提供更多的内存。
下图是程序状态变化图:
UIApplicationMain
UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
argc
系统或者用户传入的参数个数argv
系统或用户传入的实际参数argc
、argv
直接传递给UIApplicationMain进行相关处理即可principalClassName
指定应用程序类名(app的象征),该类必须是UIApplication(或子类)。如果为nil,则系统默认UIApplication类delegateClassName
创建并设置UIApplication对象的代理,指定应用程序的代理类,该类必须遵守UIApplicationDelegate协议,UIApplicationMain函数会根据principalClassName创建UIApplication对象,根据delegateClassName创建一个delegate对象,并将该delegate对象赋值给UIApplication对象中的delegate属性- 接着会简历应用的Main Runloop(事件循环),处理与用户交互产生的事件(首先会在程序完毕后调用delegate对象的application:didFinishLaunchingWithOptions:方法),程序正常退出时UIApplicationMain函数才返回。
70.view视图生命周期
视图创建:调用viewDidLoad方法
视图即将可见:调用viewWillAppear方法
视图已经可见:调用viewDidAppear方法
视图即将不可见:调用viewWillDisappear方法
视图已经不可见:调用viewDidDisappear方法
系统低内存:调用didReceiveMemoryWarning方法和viewDidUnLoad方法
注意:
1.viewDidLoad方法在应用运行的时候只会调用一次,其它方法会被调用多次。
2.低内存情况下,iOS会调用didReceiveMemoryWarning和viewDidUnLoad方法,但是iOS6以后就不在使用viewDidUnLoad方法,仅支持didReceiveMemoryWarning方法,该方法主要用于释放内存(视图控制器中的一些成员变量和视图的释放)
71.autorelease pool
autorelease的原理是什么?
autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该Object放入了当前的Autorelease pool中,当该pool被释放时,该pool中的所有Object会被调用release。
autorelease的对象何时释放?
对于autorelease pool本身,会在如下两个条件发生时候被释放
1.手动释放autorelease pool
2.Runloop结束后自动释放
对于autorelease pool内部的对象在引用计数==0的时候释放。release和autorelease pool的drain都会触发引用计数减一。
NSAutoreleasePool是什么?
NSAutoreleasePool实际上是个对象引用计数自动处理器,在官方文档中被称为是一个类。NSAutoreleasePool可以同时有多个,它的组织是个栈,总是存在一个栈顶pool,也就是当前pool,每创建一个pool,就会往栈里压一个,改变当前pool为新建的pool,然后,每次给pool发送drain消息,就弹出栈顶pool,改当前pool为栈里的下一个pool。
drain消息,就弹出栈顶的pool,改当前pool为为栈里的下一个pool。
NSAutoreleasePool中还提到,每一个线程都会维护自己的autorelease堆栈,每一个autoreleasepool只对应一个线程。
NSAutoreleasePool和autoreleasepool的区别
当你使用ARC,就必须将NSAutoreleasePool的地方换成@autoreleasepool
,两者的作用时间不一样。AutoReleasePool对象的写法作用于运行时,@autoreleasepool作用于编译阶段,如果要启用ARC的话,在编译阶段就需要告诉编译器弃用自动引用计数管理,而不能在运行时动态添加。
autoreleased对象是被添加到了当前最近的autoreleasepool中的,只有当这个autoreleasepool自身drain的时候,autoreleasepool中的autoreleased对象才会被release。这个对象的引用计数-1,变成了0,该autoreleased对象最终被释放。
向一个对象发送-autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
自动释放池是NSAutoreleasePool的实例,其中包含了收到autorelease消息的对象。当一个自动释放池自身被销毁(dealloc)时,它会给池中每一个对象发送一个release消息(如果你给一个对象多次发送autorelease消息,那么当自动释放池销毁时,这个对象也会收到同样数目的release消息)
autorelease作用:
1.对象执行autorelease方法时会将对象添加到自动释放池中
2.当自动释放池销毁时自动释放池中所有对象作release操作
3.对象执行autorelease方法后自身引用计数器不会改变,而且会返回对象本身
4.autorelease实妹上只是把对象release的调用延迟了,对于对象的autorelease系统只是把当前对象放入当前对应的autorelease pool中,当该pool被释放时([pool drain]),该pool中的所有对象会被调用release,从而释放使用的内存,这个可以说是autorelease的优点,因为无需我们再关注他的引用计数,直接交给系统来做。
@autoreleasepool
自己开启一个线程,你需要创建啊自己的自动释放池块。如果你的应用或者线程长时间存活,并可能产生大量的自动释放的对象;你应该手动创建自动释放池块;否则,自动释放的对象会积累并占用你的内存。当使用ARC来管理内存时,在线程中使用for大量分配对象而不用autoreleasepool则可能会造成内存泄露
如果你创建的线程没有调用Cocoa,你不需要使用自动释放池块。
在ARC环境下,使用@autoreleasepool可以释放池上下文,但是如下代码,@autoreleasepool有必要么?
for (int i = 0; i < 10000000; i++) {
@autoreleasepool {
NSString *str = @"abc";
NSString *string = [str lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
}
}
有必要,在遇到需要大量创建对象的地方使用autoreleasepool可以加快对象释放的速度。
72.view layout
相关方法
- (CGSize)sizeThatFits:(CGSize)size;
- (void)sizeToFit;
---------------------------------------------
- (void)layoutSubviews;
- (void)layoutIfNeeded;
- (void)setNeedsLayout;
---------------------------------------------
- (void)setNeedsDisplay;
- (void)drawRect;
layoutSubviews在以下情况下会被调用:
1.init初始化不会触发layoutSubviews,但是用initWithFrame初始化时,当rect的值不为CGRectZero时,也会触发
2.addSubview会触发layoutSubviews
3.设置view的frame会触发layoutSubviews,前提是frame的值设置前后发生了变化
4.滚动一个UIScrollView会触发layoutSubviews
5.旋转Screen会触发父UIView上的layoutSubviews事件
6.改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
layoutSubviews,当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。
刷新子对象布局:
-layoutSubviews
:默认没有做任何事情,需要子类进行重写。
-setNeedsLayout
:标记为需要重新布局,异步调用layoutIfNeeded
刷新布局,不立即刷新,但layoutSubviews一定会被调用
- layoutIfNeeded
:如果有需要刷新的标记,立即调用layoutSubviews
进行布局(如果没有标记,不会调用layoutSubviews
)
注:setNeedsLayout方法并不会立即刷新,立即刷新需要调用layoutIfNeed
方法。如果要立即刷新,要先调用[view setNeedsLayout]
,把标记设为需要布局,然后马上调用[view layoutIfNeeded]
,实现布局
setNeedsLayout
在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews
layoutIfNeeded
UIKit会判断该receiver是否需要layout,遍历的不是superview
链,应该是subviews
链
在视图第一次显示之前,标记总是"需要刷新"的,可以直接调用[view layoutIfNeeded]
重绘
-drawRect:(CGRect)rect:重写此方法,执行重绘任务
-setNeedDisplay:标记为需要重绘,异步调用drawRect
-setNeedDisplayInRect:(CGRect)invalidRect:标记为需要局部重绘
sizeToFit
会自动调用sizeThatFits
方法;
sizeToFit
不应该在子类中被重写,应该重写sizeThatFits
sizeThatFits
传入的参数是receiver
当前的size
,返回一个适合的size
sizeToFit
可以被手动直接调用
sizeToFit
和sizeThatFits
方法都没有递归,对subviews
也不负责,只负责自己
layoutSubviews
对subviews重新布局
layoutSubviews
方法调用先于drawRect
drawRect
是对receiver
的重绘,能获得context
setNeedDisplay
在receiver
标上一个需要被重新绘图的标记,在下一个draw
周期自动重绘,
iphone device
的刷新频率是60hz
,也就是1/60秒后重绘
setNeedsDisplay
该方法在调用时,会自动调用drawRect
方法。drawRect
方法主要用来画图
总结
当需要刷新布局时,用setNeedsLayout
方法;当需要重新绘图时,调用setNeedsDisplay
方法
73.应用程序的架构MVC、MVVM
- 界面
- 数据
- 事件
- 业务
MVC
iOS应用程序都遵循Model-View-Controller
的架构,Model
负责存储数据和处理业务逻辑,View
负责显示数据和用户交互,Controller
是两者的中介,协调Model
和View
相互写作,它们的通讯规则如下:
1.Controller
能够访问Model
和View
,Model
和View
不能互相访问
2.当View
与用户交互产生事件时,使用target-action
方式来处理
3.当View
需要处理一些特殊UI逻辑或获取数据源时,通过delegate
或data source
方式交给Controller
来处理
4.Model
不能直接与Controller
通信,当Model
有数据更新时,可以通过Notification
或KVO
来通知Controller
更新View
MVVM
MVVM
就是在MVC
的基础上分离出业务处理的逻辑到viewModel
层,即:model
层,API请求的原始数据、数据持久化
View
层,视图展示,由viewController
来控制
viewModel
层,负责网络请求,业务处理和数据转化
简单来说,就是API请求完数据,解析成model
,之后在viewModel
中转化成能够直接被视图层使用的数据,交付给前端。
经过viewModel
转化之后的数据由viewModel
保存,与数据相关的处理都将在viewModel
中处理,viewModel
返回给view
层
view
层是由viewController
控制的。view
层只做展示,不做业务处理。view
层的数据由viewModel
提供
1.MVVM使用详细介绍,里面也附有Demo
2.MVVM使用详细介绍,里面也附有Demo
3.MVVM使用详细介绍,里面也附有Demo
74.FMDB、SQLite
FMDatabaseQueue
用于在多线程中执行多个查询或更新,它是线程安全的。
FMDatabase
这个类是线程不安全的,如果在多个线程中同时使用一个FMDatabase
实例,会造成数据混乱等问题
1.增(创)表
格式:
- create table 表名 (字段名1 字段类型1, 字段名2 字段类型2, ...);
- create table if not exists 表名 (字段名1 字段类型1, 字段名2 字段类型2, ...);
例如:
create table t_student (id integer, name text, age integer, score real)
2.删表
格式:
- drop table 表名;
- drop table if exists 表名;
例如:
drop table t_student;
3.插入数据(insert)
格式:
insert into 表名 (字段1, 字段2, ...) value (字段1的值, 字段2的值, ...);
例如:
insert into t_student (name, age) values ('mj', 10);
备注:数据库中的字符串内容应该用单引号''阔住
4.更新数据(update)
格式:
update 表名 set 字段1=字段1的值, 字段2=字段2的值,...;
例如:
update t_student set name = 'jack', age = 20;
备注:上面的示例会将t_student表中所有记录的name都改为jack,age都改为20.
5.删除数据(delete)
格式:
delete from 表名;
例如:
delete from t_student;
75.简书内存分区情况
1.代码区:存放函数二进制代码
2.数据区:系统运行时申请内存并初始化,系统退出时由系统释放,存放全局变量、静态变量、常量
3.堆区:通过malloc
等函数或new
等动态申请到的,需要手动申请和释放
4.栈区:函数模块内申请,函数结束时由系统自动释放,存放局部变量、函数参数
76.各属性作用
-
readwrite
可读可写,需要生成getter、setter方法 -
readonly
只读,只生成getter,不会生成setter,不希望属性在类外部被更改 -
assign
赋值,setter方法将传入的参数赋值给实例变量;仅设置变量时,assign用于基本数据类型 -
retain
表示持有特性,setter方法将传入的参数先保留,在赋值,传入参数的引用计数会+1 -
copy
表示赋值特性,setter方法将传入的对象复制一份 -
nonatomic
非原子操作,决定编译器生成的setter、getter是否是原子操作 -
atomic
表示多线程安全,一般使用nonatomic
77.简述Notification、KVC、KVO、delegate区别?
-
KVO
一对多,观察者模式,键值观察机制,提供了观察某一属性变化的方法,极大简化了代码 -
KVC
键值编码,一个对象在调用setValue
检查是否存在相应的key的set方法,存在就调用set方法
set方法不存在,就查找_key的成员变量是否存在,存在就直接赋值
如果_key没找到,就查找相同名称的key,存在就赋值
如果都没找到,就调用valueForUndefindKey和setValue:forUndefindKey
-
delegate
发送者和接收者的关系是直接、一对一的关系 -
Notification
观察者模式,发送者和接收者的关系是间接、多对多的关系
区别:
1.delegate的效率比Notification高
2.delegate比Notification更直接,需要关注返回值,常带有should关键词;Notification不关心结果,常带有did关键词
3.两个模块之间的联系不是很紧密,就用Notification传值,比如多线程之间的传值
4.KVO容易实现两个对象的同步,比如Model和View的同步
78.id和nil代表什么
1.id类型的指针可以指向任何OC对象
2.nil代表空值(空指针的值,0)
79.nil、Nil、NULL、NSNull
OC中:
1.nil、Nil、NULL都表示(void *)0
2.NSNull继承于NSObject,很特殊的类,表示是空,什么也不存储,但它确实对象,只是一个占位对象,使用场景:比如服务器端口让传空,NSDictionary *params = @{@"arg1": @"value1", @"arg2": arg2.isEmpty?[NSNull null]: arg2};
区别:
1.NULL是宏,是对于C语言指针而使用的,表示空指针
2.nil是宏,是对于OC中的对象而使用的,表示对象为空
3.Nil是宏,是对于OC中的类而使用的,表示类指向空
4.NSNull是类类型,是用于表示空的占位对象,用于JS或服务端的null类似的含义
80.向一个nil对象发送消息会发生什么?
1.向nil发送消息是完全有效的,只是在运行时不会有任何作用
2.如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)
3.如果方法返回值为指针类型,其指针大小为小于或等于sizeof(void *),float、double、long double、long long的整型标量,发送给nil的消息将返回0
4.如果方法返回值为结构体,发送给nil的消息将返回0,结构体中各个字段的值也都是0
81.self.和self->的区别
1.self.
是调用getter或setter方法
2.self
是当前本身,是一个指向当前对象的指针
3.self->
是直接访问成员变量
82.如何访问并修改一个类的私有属性
1.通过KVC
2.通过runtime访问并修改
83.如何为class定义一个对外只读、对内可读可写的属性
在头文件.h中将属性定义为readonly
,在.m中将原属性重新定义为readwrite
84.OC中,meta-class指的是什么?
meta-class
是class对象的类,为这个clas类存储类方法,当一个类发送消息时,就去这个类对应的meta-class中查找那个消息,每个class都有不同的meta-class,所有的meta-class都使用基类的meta-class(假如类继承自NSObject,那么他所对应的meta-class也是NSObject)作为他们的类。
85.NSString用copy和strong的区别
NSString为不可变字符串,用copy和strong都只分配一次内存,但是如果用copy,需要先判断字符串是不是不可变字符串,如果是不可变字符串,就不再分配控件,如果是可变字符串才分配控件。如果程序中用到的NSString特别多,每一次都要先判断就会耗费性能,用strong就不会再判断了,所以不可变字符串可以直接使用strong
86.创建一个对象的步骤
1.开辟内存控件
2.初始化参数
3.返回内存地址值
87.setter、getter
1.setter方法:为外界提供一个设置成员变量的方法,好处:不让数据暴露在外,保证数据安全性;对设置的数据过滤
2.getter方法:为调用者返回对象内部的成员变量
3.点语法的本质是对setter或getter方法的调用
88.id、instanceType是什么?区别?
1.id
万能指针,能作为参数、方法的返回类型
2.instancetype
只能作为方法的返回类型,并且返回的类型是当前定义类的类类型
89.内存管理
ARC所做的是在代码编译期自动在合适的位置插入release
或autorelease
,只要没有强指针指向对象,对象就会被释放,ARC中不能手动使用NSZone
,也不能调用父类的dealloc
调用对象的release方法会销毁对象吗?
不会,调用release只是将对象的引用计数器-1,当前的引用计数器=0时候会调用对象的dealloc方法才能释放对象的内存
objc使用什么机制管理对象内存
1.通过引用计数器(retainCount)的机制来决定对象是否需要释放,每次RunLoop完成一个循环的时候,都会检查对象的retainCount,如果retainCount为0,说明该对象没有地方需要继续使用了,就被释放了
2.ARC的判断准则:只要没有强指针指向对象,对象就会被释放
内存管理的范围?
管理所有继承自NSObject的对象,对基本数据类型无效,是因为对象和其它最基本数据类型在系统中存储的空间不一样,其它局部变量主要存储在栈区(因为最光阴民数据类型占用的存储控件是固定的,一般存放于栈区),而对象存储于堆中,当代码块结束时,这个代码快中的所有局部变量会自动弹栈清空,指向对象的指针也会被回收,这时对象就没有指针指向了,但依然存在于堆中,造成内存泄露。
内存管理研究的对象:
1.野指针:指针变量没有进行初始化或指向的空间已线被释放
- 使用野指针调用对象方法,会报异常,程序崩溃
- 在调用完release后,把保存对象指针的地址清空,赋值为nil,找OC中没有空指针异常,所以[nil retain]调用方法不会异常
2.内存泄露
- 如Person *p = [Person new];(对象提前赋值nil或清空)在栈区的p已经被释放,而堆区new产生的对象还没有释放,就会造成内存泄露
- MRC造成内存泄露的情况
1.没有配对释放,不符合内存管理原则
- 对象提前赋值nil或清空,导致release不起作用
- 僵尸对象:堆中已经被释放的对象(retainCount=0)
- 空指针:指针赋值为空,nil
alloc
、allocWithZone
、new
时,该对象引用计数+1
retain
手动为该对象引用计数+1
copy
对象引用计数+1,注意:copy的oc数据类型是否有mutable,如有为深拷贝,新对象引用计数为1,如果没有,为浅拷贝,引用计数+1
mutableCopy
生成一个新对象,新对象引用计数+1
release
手动为该对象引用计数-1
autorelease
把该对象放入自动释放池,当自动释放池释放时,向池中的对象发送release消息,其内的对象引用计数-1,只能释放自已拥有的对象
NSAutoreleasePool
是通过接收对象向它发送的autorelease消息,记录该对象的release消息,当自动释放池被销毁时,会自动向池中对象发送release消息
对象如何加入池中:调用对象的autorelease方法
多次调用对象的autorelease方法会导致:野指针异常
90.KVC的底层实现?
当一个对象调用setValue方法时,方法内部会做以下操作:
1.检查是否存在key的set方法,如果存在,就调用set方法
2.如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量_key,如果有,则直接给成员变量赋值
3.如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
4.如果还没找到,则调用valueForUndefindedKey:和setValue:forUndefindedKey:
这些方法的默认实现都是抛出异
常,可以根据需要重写它们
91.block的内存管理
1.无论ARC还是MRC,只要block没有访问外部变量,block始终在全局区
2.MRC下
- block如果访问外部变量,block在栈区
- 不能对block使用retain,否则不能保存在堆区
- 只有使用copy,才能放到堆区
3.ARC下
- block如果访问外部变量,block在堆区
- block是一个对象,可以使用copy或strong修饰,最好是使用copy
92.App的启动过程,从main说起
App启动分两类:1.有storyboard;2.无storyboard
有storyboard情况下:
1.main函数
2.UIApplicationmMain
- 创建UIApplication对象
- 创建UIApplication的delegate对象
3.根据info.plist获得Main.storyboard的文件名,加载Main.storyboard
- 创建window
- 创建和设置UIWindow的rootViewController
- 显示窗口window
没有storyboard情况下:
1.main函数
2.UIApplicationMain
- 创建UIApplication对象
- 创建UIApplication的delegate对象
3.delegate对象开始处理(监听)系统事件
- 程序启动完毕时,调用didFinishLaunching方法
- didFinishLaunching方法中创建UIWindow设置rootViewController并显示窗口window
93.tableview的cell里面如何嵌套collectionview
用自定义的继承自UITableViewCell的类,在initWithFrame方法里面初始化自定义的继承自UICollectionView的类
94.awakeFromNib和viewDidLoad的区别
-
awakeFromNib
当.nib文件被加载的时候,会发送一个awakeFromNib消息到.nib文件中每个对象,每个对象都可以定义自己的awakeFromNib来响应这个消息。即通过.nib文件创建的view对象会执行swakeFromNib -
viewDidLoad
当view被加载到内存就会执行,不管是通过nib还是代码形式
95.常见的Crash场景
1.访问僵尸对象
2.访问了不存在的方法
3.数组越界
4.在定时器下一次回调前将定时器释放,会crash
96.AFN断点续传
1.检查服务端文件信息
2.检查本地文件
3.如果本地文件比服务端文件小,断点续传,利用HTTP请求头的Range实现断点续传
4.如果比服务端文件大,重新下载
5.如果和服务端文件一样大,下载完成
afn默认超时时间是60s
97.客户端的缓存机制
4种数据持久化:属性列表(plist)、对象归档、sqlite、CoreData、NSUserDefaults用于存储配置信息
如何存储用户的一些敏感信息,如登录的token?
使用keychain来存储,即钥匙串,使用keychain需要导入Security框架
使用NSUserDefaults时,如何处理布尔的默认值?(比如返回NO,不知道是真的NO,还是没有设置过的NO)
if ([[NSUserDefaults standardUserDefaults] objectForKey:@"key"] == nil) {
NSLog(@"没有设置过");
}
99.App需要加载超大量的数据,给服务器发送请求,但服务器卡住了,如何解决?
1.设置请求超时
2.给用户提示请求超时
3.根据用户操作再次请求数据
100.网络图片处理问题中怎么解决一个相同的网络地址重复请求的问题?
利用字典(图片地址为key,下载操作为value)
101.如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
使用dispatch_group_async追加block到global queue中,这些block全部执行完毕,就会执行main dispatch queue中的结束处理的block.
代码如下:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();
//下载图片1,放线程组中
dispatch_group_async(group, queue, ^[
NSURL *url = [NSURL URLWithString:@""];
NSData *imgData1 = [NSData dataWithContentsOfURL:url];
self.img1 = [UIImage imageWithData:imgData1];
});
//下载图片2,放线程组中
dispatch_group_async(group, queue, ^[
NSURL *url = [NSURL URLWithString:@""];
NSData *imgData2 = [NSData dataWithContentsOfURL:url];
self.img2 = [UIImage imageWithData:imgData2];
});
//合成图片
dispatch_group_notify(group, queue, ^{
NSLog(@"combie---%@", [NSThread currentThread]);
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
[self.img1 drawInRect:CGRectMake(0, 0, 200, 100)];
self.img1 = nil;
[self.img2 drawInRect:CGRectMake(0, 100, 200, 100)];
self.img2 = nil;
UIImage *img = UIGraphicsGetImageFromCurrentImageContent();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"UI---%@", [NSThread currentThread]);
});
});
102.NSOperation、GCD、NSThread的区别
NSOperation与GCD的区别:
GCD:
1.GCD是iOS4.0推出的,纯C
2.GCD是将任务(block)添加到队列(串行、并行、全局、主队列),并且以同步/异步的方式执行任务的函数
3.GCD提供NSOperation不具备的功能:
- 一次性执行
- 延迟执行
- 调度组
- GCD是严格的队列,先进先出 FIFO
NSOperation:
1.NSOperation是iOS2.0推出的,iOS4.0以后又重写的
2.NSOperation是将操作(异步任务)添加到队列(并发队列),就会执行指定的函数
3.NSOperation提供的方便操作:
- 最大并发数
- 队列的暂停和继续
- 取消所有的操作
- 指定操作之间的依赖关系,可以让异步任务同步执行
- 可以利用VKO监听一个operation是否完成
- 可以设置operation的优先级,能使同一个并行队列上的任务区分先后地执行
- 对NSOperation继承,在这之上添加成员变量和成员方法,提高代码的复用度
GCD与NSThread的区别:
1.NSThread使用@selector指定要执行的方法,代码分散
2.GCD通过block指定要执行的方法,代码集中
3.GCD不用管理线程的生命周期(创建、销毁、复用)
4.如果要开多个线程NSThread必须实例化多个线程对象
5.NSThread通过performSelector方法实现线程间通信
为什么要取消/恢复队列?
1.一般内存警告后取消队列中的操作
2.为了保证scrollView在滚动的时候流畅,通常在滚动开始时,暂停队列中的所有操作,滚动结束后,恢复操作
__bridge:用于Foundation和Core Fundation之间数据的桥接
103.是否可以把比较耗时的操作放在NSNotificationCenter中?
1.如果在异步线程发的通知,可以执行耗时的操作
2.如果在主线程发的通知,则不可以执行耗时的操作
static const CGSize imageSize = {100, 100};
- (void)awakeFromNib {
if (!self.image) {
self.image = [UIImage imageNamed:@"random.jpg"];
if (NULL != UIGraphicsBeginImageContextWithOptions) {
UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
}else {
UIGraphicsBeginImageContext(imageSize);
}
[image drawInRect:imageRect];
self.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
}
这里需要判断一个UIGraphicsBeginImageContextWithOptions是否为NULL,因为它是iOS4.0才加入的。
由于JPEG图像不是透明的,所以第二个参数就设为YES。
第三个参数是缩放比例,iPhone 4是2.0,其它是1.0。虽然这里可以用[UIScreen mainScreen].scale来获取,但实际上设为0后,系统就会自动设置正确比例了。
TableView中实现平滑滚动延迟加载图片
利用CFRunLoopMode的特性,可以将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。主线程繁忙的时候performSelector:withObject:afterDelay:会延后执行,所以在发生触摸或是视图还在滚动时,这个方法不会运行;
UIImage *downloadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:) withObject:downloadImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
注意:使用masonry进行label的多行显示设置时,主要是如下两个参数的设置
@property(nonatomic) CGFloat preferredMaxLayoutWidth
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis
网友评论