OC&iOS
OC语言基础
1.类与对象
类方法
OC的类方法只有2种:静态方法和实例方法两种
@interface Controller : NSObject
+ (void)thisIsAStaticMethod;静态方法
- (void)thisIsInstanceMethod;实例方法
在OC中,只要方法声明在@interface里,就可以认为是公有的。实际上,OC没有绝对的私有和保护成员方法,仅仅是对调用者隐藏某些方法
声明和实现都写在@inplementation里的方法,类的外部是看不到的。
!如果想通过Category来隐藏方法的时候,可以把实现方法放在主implementation里。如果是Extension类拓展,声明隐藏在.m文件中,调用者无法看到其声明,也就没法调用这个类拓展的方法,在ARC下会引发编译错误。
使用Extension的好处:
- 1.Extension声明的方法必须在类的主@implementation区间内实现,可以避免使用同名的Category带来的多个不必要的implementation段
- 2.如果Extension中声明的方法没有实现,编译器会给出Warning,使用Category则不会。
类变量
@property中有哪些属性关键字(后面有哪些修饰符)?
属性分为四类:
- 非原子性--
nonatomic
特性:
在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。请注意,尽管没有名为“atomic的特质(如果其属性不具备nonatomic特质那么都是原则子的),但是仍然可以在属性特质中写明这一点,编译器不会报错,若是自己定义存取方法,那么就应该从与属性特质相符的原子性” - 读写权限--
readwrite(读写)
、readonly(只读)
- 内存管理权限--
-
assign
、普通赋值,一般用于基本数据类型,不进行retain操作。常见委托设计模式,来防止循环引用(弱引用)。与weak的区别,向值为nil
的assign对象发送消息是危险的。 -
weak
、非拥有关系,表示一种弱引用
,这个引用不会增加对象的引用计数,并且当这个所指的对象被释放之后,weak
指针会被赋值为nil
,在OC中向值为nil
的weak对象发送消息是安全的 。只能修饰OC对象
-
unsafe_unretained
、相当于assign,相当于【ARC】__weak修饰block。避免循环引用 -
retain
、对象的setter方法对参数进行:release旧值,再retain新值。实现顺序。 -
copy
。copy修饰的对象进行赋值时,为了确保对象的值不会无意改变,copy了一份副本,副本的改变不会影响原来的内容,原来内容的改变不会影响副本先对旧值release,再copy出新对象,引用计数器为1,这是为了减少对上下文依赖而引入的机制。- 只有不可变的数组、字典使用
copy
----才是浅拷贝。【浅拷贝不会开辟新的内存空间、浅拷贝会使引用计数加1】 - 可变对象、或者是不可变对象使用
mutableCopy
----深拷贝。【深拷贝会开辟新的内存空间,深拷贝重新分配内存不会导致
原来对象引用计数+1】
- 只有不可变的数组、字典使用
-
strong
、拥有关系,是一种强引用,由strong修饰的指针变量,对象不会被释放。当指针指向新值或者指针不复存在了,相关联的对象就会被自动释放【ARC下】 -
nonatomic
、非原子性访问,线程安全,不加同步,多线程并发访问会提高性能。默认是原子型atomic
@property 的说明可以有以下几种:
* readwrite 是可读可写特性;需要生成 getter 方法和 setter 方法
* readonly 是只读特性,只会生成 getter 方法 不会生成 setter 方法,不希望属性在类外改变时使用
* assign 是赋值特性,setter 方法将传入参数赋值给实例变量;仅设置变量时;
* retain 表示持有特性,setter 方法将传入参数先保留,再赋值,传入参数的 retain count 会+1;
* copy 表示拷贝特性,setter 方法将传入对象复制一份;需要完全一份新的变量时。
* nonatomic 和 atomic ,决定编译器生成的 setter getter是否是原子操作。 atomic 表示使用原子操作,可以在一定程度上保证线程安全。一般推荐使用 nonatomic ,因为 nonatomic 编译出的代码更快
4.方法-- getter=<name>
、setter=<name>
getter=<name>的样式:
@property (nonatomic, getter=isOn) BOOL on;
5.ARC
下默认关键字:
- 基本数据类型默认为:
atomic
、readwrite
、assign
- 对象类型默认为:
atomic
、readwrite
、strong
property的本质:
property
有2大概念:ivar(实例变量)
、存取方法(getter
+setter
=access method
) @property会自动生成getter和setter方法,同时进行自动内存管理。
-
property
主要是用来保存对象中的数据,同时提供设置变量和读取变量的方法 - 在属性定义的时候,编译器已经帮我们向类中添加实例变量以及生成方法的
getter
和setter
@synthesize
与@dynamic
的认识
-
@synthesize
如果没有手动实现setter
方法和getter
方法,编译器会自动加上这2个方法。用来自定义我们实例变量的名称 -
@dynamic
告诉编译器:属性的setter
与getter
方法由用户自己实现,不自动生成。
@property 背后使用synthesize来生成getter和setter,对于现代OC来说,编译器默认会进行自动synthesize,把ivar和属性绑定起来:
@synthesize propertyName = _propertyName
不需要我们写任何代码,就直接使用getter和setter了。
下面几种特殊情况不会自动synthesize
- 可读写(readwrite)属性实现了自己的getter和setter
- 只读(readonly)属性实现了自己的getter
- 使用@dynamic,显式表示不希望编译器生成getter和setter
- protocol中定义的属性,编译器不会自动生成synthesize,需要手动写
- 当重载父类中的属性时,也必须手动写synthesize
类拓展--Protocol,Category和Extension
Protocol
OC是单继承的,OC中的类可以实现多个协议protocol来实现类似C++中多重继承的效果。
Protocol类似Java中的interface接口,定义了一个方法列表,这个方法列表中的方法可以使用@required
,@optional
标注,以表示该方法是否是必须要实现的方法。一个protocol可以继承其他的protocol。
@protocol TestProtocol <NSObject> //NSObject也是一个protocol,这里继承NSObject里的方法
- (void)print;
@end
@interface B : NSObject<TestProtocol>
- (void)print;//默认方法是@required的,即必须实现
@end
Delegate(委托)是Cocoa中常见的一种设计模式,其实依赖于protocol这个语言特性。
当Protocol中含有property时,编译器不会自动synthesize,需要手动处理:
方法1:再次声明property
方法2:手动synthesize @synthesize item;
Category
Category是一种灵活的拓展原有类的机制,使用Category不需要访问原有类的代码,也无需继承。Category提供了一种简单的方式,来实现类的相关方法的模块化,把不同的类方法分配到不同的类文件中。
Extension一种匿名的Category :Extension是用来给类添加私有变量和方法,用在类的内部使用。
区别:
- 1.使用Extension必须有原有类的源码
- 2.Extension可以在类中添加新的属性和实例变量,Category只能通过运行时添加新的属性,但是不能添加新的实例变量,因为内存里的数据结构已经分配好了
- 3.Extension里添加的方法必须要有实现(没有实现编译器会给出警告)
使用场景:
一般用在类的内部。例如在interface中定义
readonly
类型的属性,在实现中添加extension
,将其重新定义为readwrite
,这样我们在类的内部就可以直接修改它的值了,然而外部依然不能调用setter
方法来修改.
Person.h
@interface Person : NSObject
@property (readonly) NSString *uniqueIdentifier;
@end
Person.m
@interface Person()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation Person
....
@end
在类中添加全局变量:在.m文件中使用static变量,因为static变量在编译期就是确定的,因此对于NSObject对象来说,初始化的值只能是nil.
static NSOperationQueue * _personOperationQueue = nil;
如何进行init的初始化?通过重载initialize方法
@implementation Person
- (void)initialize{
if (!_personOperationQueue){
_personOperationQueue = [NSOperationQueue alloc] init];
}
}
@end
类的导入
导入类可以使用#include
,#import
和class三种方法,区别如下:
-
#import
是OC导入头文件的关键字,#include
是C、C++导入头文件的关键字 - 使用
#import
头文件会自动只导入一次,不会重复导入 -
@class
告诉编译器某个类的声明,解决头文件相互包含问题
@class
是放在interface中的,只是在引用一个类,将这个被引用类作为一个类型使用。在实现文件中,如果需要引用到被引用类的实体变量或者方法时,还需要使用#import
方法引入被引用类。
类的初始化
OC是建立在Runtime基础上的语言,类也不例外。OC中 类是初始化也是动态的。在OC中绝大部分类都继承自NSObject
,它有两个非常特殊的类方法load
和initilize
,用于类的初始化
+load
+load方法是当类或分类被添加到OC runtime时被调用的,实现这个方法可以在类加载的时候执行一些类相关的行为。子类的+load方法会在它的所有父类的+load方法之后执行,而分类的+load方法会在它的主类的+load方法之后执行。不同的类之间的+load方法的调用顺序是不确定的。
load方法不会被类自动继承,每一个类中的load方法都不需要像viewDidLoad方法一样调用父类的方法。子类、父类和分类中的 +load方法的实现是被区别对待的。也就是说如果子类没有实现+load方法,那么当它被加载时,runtime是不会去调用父类的+load方法的。
当一个类和它的分类都实现了+load方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些坏事,比如:混淆方法
+initialize
+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方法被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。
+initialize方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换而言之。如果子类没有实现+initialize方法,那么继承自父类的实现会被调用,如果一个类的分类实现了+initialize方法,那么就会对这个类中实现造成覆盖。
OC语法
对OC的理解与特性
- OC是一门面向对象语言,既有静态语言的特性(C++),又有动态语言的特性(动态绑定,动态加载等)。
- OC的动态特性主要表现在3个方面:动态类型(Dynamic typing),动态绑定(Dynamic binding),动态加载(Dynamic loading).动态--必须到运行时(run time)才会做的一些事情。
- 动态类型:运行时才决定对象的类型。比如【id类型】
- 动态绑定:基于动态类型,在某个实例对象被确定后,其类型便被确定了,该对象对应的属性和响应消息也就完全确定了。
- 动态加载:根据需求加载所需要的资源,最基本就是不同机型的适配。【例如:在Retina 设备上加载@2x的图片,而在老的苹果设备上加载原图,让程序在运行时添加代码模块以及其他资源,用户可根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件,可执行代码可以含有和程序运行时整合的新类。】
区分:“super”和“self”
super是一个神奇的关键字,super本身是一个编译器标识符
,self是指向的同一个消息接受者
。
不同点在于:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类的方法
。 当使用self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
而当使用super时,则从父类的方法中开始寻找。
然后调用那个父类中对应的方法。
野指针
野指针
不是NULL
空指针,而是指向"垃圾"内存的指针。
其原因主要为:
- 1.指针变量没有初始化。任何指针变量刚被创建时不会自动成为空指针,它的默认值是随机的,它不知道指向哪些。所以指针变量在创建的同时应该初始化,要么就将指针设为
NULL
,要么让它指向合法的内存。 - 2.指针P被释放free或删除delete之后,没有将值设为
NULL
。访问了野指针,比如对一个已经释放的对象执行了release
、访问已经释放对象的成员变量或者发送消息。
runtime如何实现weak属性?
runtime对注册的类,会进行布局,对于weak对象会放入一个哈希表中,用weak指向的对象内存地址作为key,为此对象的引用计数为0时,会销毁。
如何实现将weak变量的自动设置为nil?
假如weak指向的对象内存地址是a,那么就会以a为键,去这个weak哈希表中寻找所有以a为键的weak对象,从而设置为nil
类
- class 返回接收者类的对象,
- isKindOfClass:返回一个布尔值,表示接收值是不是一个给定的类或者子类的实例
- isMemberOfClass:返回一个布尔值,指示接收者是不是给定类的实例
- isProxy 返回一个布尔值,表示接收者是不是继续NSObject
消息
- respondsToSelector:返回一个布尔值,指示接收者是否实现或者继承了父类的方法。对给定的消息进行处
代理
- 代理是一种用于替代其他对象或还没出现的对象的对象。通常情况下,传给代理的消息会被传递给真正的对象,或者使代理去加载真正的对象
SET、Method、和IMP分别说下。再谈下对IMP的理解
- SET:是
selector
的一个类型,表示一个方法的名称。 - Method(方法):表示一种类型,这种类型与
selector
的实现相关 - IMP 定义为
id (*IMP)(id,SET,...)
:IMP
是一个执行函数的指针,这个被指向的函数包括id(“self”指针),调用的SET方法名,以及实现方法。 - 知名框架AFN源码涉及IMP的代码
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
Class currentClass = [localDataTask class];
while (class_getInstanceMethod(currentClass, @selector(resume))) {
Class superClass = [currentClass superclass];
IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
if (classResumeIMP != superclassResumeIMP &&
originalAFResumeIMP != classResumeIMP) {
[self swizzleResumeAndSuspendMethodForClass:currentClass];
}
currentClass = [currentClass superclass];
}
[localDataTask cancel];
[session finishTasksAndInvalidate];
isa指针的作用 --(帮助一个对象找到它的方法)
- 对象的isa-->指向类,类的isa-->指向元类【meteClass】,元类的isa-->指向元类的根类。
- isa指针是一个Class类型的指针,每个实例对象有个isa指针,它指向对象的类,而Class里也有一个isa指针,指向meteClass元类。元类保存了类方法的列表
- 当类方法被调用时,先在自身查找类方法的实现,如果没有
- 元类会向它的父类查找该方法。
- 注意:元类也是类同时也是对象,元类也有isa指针,它的isa最终指向一个根元类(root meteClass)根元类的isa指针指向自己,这样形成一个封闭的内循环。
消息机制
OC是建立在Runtime
基础上的语言,类也不例外。OC中类是初始化也是动态的。在OC中结大部分分类都继承自NSObject
,它有两个特殊的类方法load
,initialize
,用于类的初始化。
在对象上调用方法是OC中经常使用的功能,也称作传递消息
消息有”名称“或选择器”,可以接受参数,可以有返回值。
消息=【接收者+选择器+参数】构成。给某对象发送消息也就相当于在该对象上“调用方法”,发送给某对象的全部消息都要由动态消息派发系统
来处理,该系统会查出对应的方法,并执行其代码
编译器在编译代码的时候就已经知道程序中有X和Y这2个函数了,会直接生成调用这些函数的指令。而函数地址实际上是硬编码值在指令之中。
消息传递实现原理:
在OC中,如果向某对象传递消息,那就会使用动态绑定机制
来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
给对象发送消息可以这样写:
id returnValue = [someObjectName:parameter];
someObject
叫做“接收者” messageName叫做“选择器”。选择器与参数合起来称为"消息(message)"。
编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数仍是消息传递机制中的核心函数,叫做objc_msgSend
void objc_msgSend(id self,SEL cmd,...)
参数个数可变,第一个参数代表接收者,第二个参数代表选择器,后续参数表示消息中的那些参数,其顺序不变。
调用方法的过程:
- 1.调用方法时,首先会在自身
isa
指针 指向的 -->类 -
objc_msgSend
函数会根据接收者与选择子的类型来调用适当的方法,为了完成此操作,
-
- 3.该方法需要在接收者所属的类中搜寻其“方法列表”,如果找到与选择器名称相符的方法,就跳至其实现代码。
- 4.若找不到,那沿着继承体系继续向上查找,等找到合适的方法,就跳至其实现代码。若是找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。
- 5.最终还是找不到相符的方法,就执行消息"消息转发操作"。
接着下面的消息转发机制
总结如下:
objc是动态语言,每个方法在运行时会被动转为消息发送,即objc_msgSend(receiver,selector)
- objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象所属的类,
- 然后在该类中的方法列表以及其父类列表中寻找方法运行,
- 如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会崩溃并抛出
unrecognized selector sent to XXX.
在这之前, - objc的运行时会给出三次拯救程序崩溃的机会:
- Method resolution
objc运行时调用+resolveInstanceMethod:
或者+resolveClassMethod:
往上级寻找备援函数,让你有机会提供一个函数的实现。如果添加了函数,那么运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移动下一步,消息转发(Message Forwarding). - Fast forwarding
如果目标对象实现了-forwardingTargetForSelector:
,Runtime这时就会调用这个方法,给你把这个消息转发给其他对象的机会。只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象也会变成返回的那个对象。否则,会继续Normal Fowarding(下一步的转发机制)。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快。 - Normal forwarding
这一步是Runtime最后一次给你挽救的机会,首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSingnatureForSelector:
返回nil,Runtime则会发出-doesNotRecognizeSelector:
消息,程序这是也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
具体实现:消息转发分为2大阶段
-
第一阶段:
动态方法解析
==》先检查接收者,所属的类,看其是否能动态添加方法,来处理当前这个“未知的选择器”,调用下面这个方法:参数selector就是那个”未知的选择器“+ (BOOL)resolveInstanceMethod:(SEL)selector
-
第二阶段:
完整的消息转发机制
==》如果运行期系统已经把第一阶段执行完了,那么接收者自己无法再以动态新增方法的手段来响应包含该选择器的消息了,此时,运行期系统会请求接收者,用其他手段来处理与消息相关的方法调用。 -
分为2步
-
1.让接收者看看有没有其他对象(“备援的接收者”)能处理这条信息,
-
若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束。该步骤对应的处理方法如下:方法参数代表未知的选择器,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。
- (id)forwardingTargetForSelector:(SEL)selector
-
2.若没有“备援的接收者”对象能处理这条信息,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节,包括选择器,目标及参数都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。
- (void)forwardingInvocation:(NSInvocation *)invocation
这个方法只需改变调用目标,是消息在新的目标上可以调用即可。- 3.比较有用的方式:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数,或是改换选择器等。实现此方法时,若发现某调用操作不应由本类处理,那么需要调用超类的同名方法,这些继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法中,那么该方法还会继而调用“doesNotRecogn”
-
2.类的初始化
+load
+load方法是当类或分类被添加到OC runtime时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的+load方法会在它的所有后类的+load方法之后执行,而分类的+load方法会在它的主类的+load方法之后执行。但是不同的类之间的+load方法的调用顺序是不确定的。
+load方法不会被类自动继承,每个类中的load方法都不需要像viewDidLoad方法一样调用父类的方法。子类、父类和分类的+load方法的实现是被区别对待的。
也就是说如果子类没有实现+load方法,那么当它被加载时runtime时不会去调用父类的+load方法的。同理,当一个类和它的分类都实现了+load方法时,两个方法都会被调用。
+initialize
+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法 和类方法的调用。
也就是+initialize方法是以懒加载的方式被调用的,如果程序一直没有某个类或它的子类发送消息,那么这个类的+initialize方法永远不会被调用。
+initialize方法的调用与普通方法的调用时一样的,走的都是发送消息的流程。换言之,如果子类没有实现+initialize方法,那么继承自父类的实现会被调用;如果一个类的分类实现了+initialize方法,那么就会对这个类的实现造成覆盖。
2.Block编程
block:实际上是带有自动变量的匿名函数
,指向结构体的函数指针
,编译器会将block的内部代码生成对应的函数。
block和函数的相似性:可以在任何时候执行
、可以保存代码
、有返回值
、有形参
、调用方式一样
block访问外部变量:
block内部可以访问外部变量
、
默认情况下,block内部不能修改外部的局部变量
,
但是给局部变量加上__block关键字,就可以在内部进行修改了
1.默认情况下,block的内存是在栈中的,它不会对所引用的对象进行任何操作
看下面这段代码:int a = 10;a是一般局部变量,传递过去的是值,所以后面的a不管赋值多少,在栈中的值10 是不改变的。
void test1()
{
int a = 10;
void (^block)() = ^{
NSLog(@"a is %d", a);
};
a = 20;
block(); // 打印出来: a is 10
}
下面几种情况,变量就会传地址到堆中
//加上__block
void test2()
{
__block int a = 10;
void (^block)() = ^{
NSLog(@"a is %d", a);
};
a = 20;
block(); // 20
}
//静态局部变量
void test3()
{
static int a = 10;
void (^block)() = ^{
NSLog(@"a is %d", a);
};
a = 20;
block(); // 20
}
//全局变量
int a = 10;
void test4()
{
void (^block)() = ^{
NSLog(@"a is %d", a);
};
a = 20;
block();
}
2.如果对block进行一次copy操作,block的内存就会在堆中,它会对所用的对象做一次retain操作,引用计数器加1.
比如@property (nonatomic, copy) void (^block)();
,
非ARC:如果所引用的对象用了__blcok修饰,就不会做retain操作
- (id)init
{
if (self = [super init]) {
// __block Dog * dog = self;与下行意思一样:将自身的对象赋值给block对象
__block typeof(self) dog = self;
self.block = ^{
// [dog run];
NSLog(@"%d", dog->_age);
};
}
return self;
}
//做了一次copy就要有释放
- (void)dealloc
{
NSLog(@"Dog --- dealloc");
Block_release(_block);
[super dealloc];
}
- (void)run
{
NSLog(@"run");
}
ARC:如果所引用的对象用了__unsafe_unretained
或者__weak修饰,就不用做retain操作。
__unsafe_unretained
、__weak
修饰的变量,因为自己生成并持有的对象不能继续为自己所有,使用生成的对象会立即释放。
把使用__unsafe_unretained修饰的变量,赋值给__strong强引用修饰的变量时
一定要保证被赋值的对象【也就是__unsafe_unretained修饰的对象】确实存在,不然程序会崩溃
__block是复制对象的引用地址来实现访问的,源码中多了一个结构体,用来保存我们捕获并修改的变量。
block的常见的三种类型:每种类型的block都有一个isa
_NSConcreteGlobalBlock全局block:global block的isa指针指向了全局区域。block变量存放在全局的数据区
_NSConcreteStackBlock栈区block:出了作用域就会被释放,通常会将其拷贝到堆区
-
_NSConcreteMallocBlock堆区block:无法直接创建,通常是通过栈区copy到堆区
(也就是说block需要执行copy之后才能存放到堆中)
3.block也经常使用copy关键字
block使用copy是从MRC遗留下来的“传统”,在MRC中,方法内部的block在栈区,使用copy可以把它放到堆区。
在ACR中,写不写都没关系。
block使用copy还是strong效果都一样,写上block能时刻提醒我们:编译器自动对block进行了copy操作,如果不写copy,该类的调用者有可能会忘记或者根本不知道”编译器会对block进行了copy操作“,他们有可能会在调用之前自行拷贝属性值。这种操作多余而且低效。
4.使用block时什么情况会发生引用循环,如何解决?
!一个对象中强引用了block,在block中又强引用了该对象,就会发生循环引用。【2个对象互相持有对方的强引用,并且2各对象的引用计数都不是0的时候,就会造成循环引用。】
解决方法:是将该对象使用_weak或者_block修饰符修饰之后再在block中使用
1.id weak weakSelf = self;
或者 weak__typeof(&*self)weakSelf = self
该方法可以设置宏
2.id __block weakSelf = self;
3.实例变量完成工作后,其中一方强制置空 xxx = nil
.
默认情况下,在block中访问的外部变量是复制过去的。即,写操作不对原变量生效,但是你可以加上_block
来让其写操作生效
_block int a = 0;
void (^foo)(void) = ^{
a = 1;
};
foo();
Block不允许修改外部变量的值(栈中指针的内存地址)。
_block
所起到的作用就是只要观察到该变量被block所持用,就将"外部变量"在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
__block int a = 0;//声明
NSLog("定义前:%p",&a);
void (^foo)(void)=^{
a = 1;
NSLog(@"block内部:%p",&a);//堆区
};
NSLog(@"定义后:%p",&a);//堆区
foo();
2016-07-17 18:34:58.581 Block[28619:2454735] 定义前:0x7fff5fbff848
2016-07-17 18:34:58.582 Block[28619:2454735] 定义后:0x100600088
2016-07-17 18:34:58.582 Block[28619:2454735] block内部:0x100600088
“定义前”在栈区,只要进入了block区域,就变成了堆区。这就是__block
关键字的真正作用。定义后和block内部两者的内存地址是一样的,我们都知道block内部变量会被copy到堆区,"block内部“打印的是堆地址,因而”定义后“打印的也是堆的地址
a由基本数据类型,变成了对象类型。block会对对象类型的指针进行copy,copy到堆中,但并不会改变该指针所指向的堆中的地址,所以在上面的示例代码中,block体内修改的实际是a指向堆中的内容。
Block不允许修改外部变量的值,这里所说的外部变量的值,是占中指针的内存地址,栈是红灯区,堆是绿灯区。
block底层的实现
c++里面的结构体相当于OC的类,c++里面的结构体拥有自己的属性以及构造方法和方法。
block底层实现分2种:
1.不加_block,会创建一个结构体[结构体内有个isa指针,isa指针指向&NSConcreateStackBlock栈地址,实现其构造方法,来接收三个参数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;//将fp赋值给impl结构体的FuncPtr参数[存在的是形参的值]
Desc = desc;
}
2.__block实现:其中多了一个forwarding 指针存放的是自己类型的结构体的指针,存放的是自己的地址。取值的时候,拿到的是结构体的地址,只要把地址传递过去,就有了最高的操作权限,到时候再去取值就可以取到内存中最新的值。
生成的结构体如下
truct __Block_byref_a_0 {
void *__isa; //isa 类型的指针 自己的类型
__Block_byref_a_0 *__forwarding; //与自己结构体同名,是一个自己类型的结构体的指针,存放的是自己的地址
int __flags; // 标记
int __size; // 类型的大小
int a; // a 属性 保存变量的值
};
5.使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?
系统的某些block api中,UIView的block版本写动画不需要考虑,但也有一些api需要考虑,所谓”循环引用“是指双方的强引用,所以那些”单向的强引用“(block强引用了self)没有问题,
比如:[UIView animateWithDuration:duration animations:^{[self.superview layoutIfNeeded];}];
不需要考虑强引用问题。
但如果你使用了一些参数中可能含有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];
});
3.Runtime
9.1 什么是runtime(运行时机制)?
9.1.是什么?
runtime是一套比较底层的纯C语言的API,属于1个C语言库,包含了很多底层的C语言API。
如何实现?
我们平时写的OC代码,在程序运行过程中,其实最终都会转成runtime的C语言代码,调用相应的函数进行消息发送。
所以runtime是OC的幕后工作者
比如:在OC中:[[Person] alloc] init]
在runtime中会转换成:objc_msg(obj_msgSend("Person", "alloc"), "init")
9.2.runtime怎么用呢?
runtime属于OC的底层,可以进行一些非常底层的操作(是OC无法实现的)
runtime可以在程序的运行过程中,动态添加一个类(比如KVO底层的实现,其实整个OC都是)
runtime可以在程序运行过程中,动态地为某个类添加属性、方法,修改属性值、方法
runtime可以遍历一个类的所有成员变量(属性)、所有方法
9.3.相关的头文件和函数
头文件:<objc/runtime.h> <objc/message.h>
函数:Ivar:是成员变量
、Method:成员方法
objc_msgSend:给对象发送消息
class_copyIvarList:遍历某个类所有的成员变量
class_copyMethodList:遍历某个类所有的方法
9.4.相关应用:
(1)NSCoding归档和解档,利用runtime遍历模型对象的所有属性(用一个循环)
(2)字典转模型;利用runtime遍历模型对象的所有属性,根据属性名从字典中取出对应的值,设置到模型的属性上
(3)KVO:利用runtime动态产生一个类
KVO内部实现原理:是基于runtime机制实现的。
1.当我们观察一个类的对象时,系统就会在运行期间动态的创建这个类的派生类。
2.这个派生类继承自该对象的原本的类,派生类并重写了被观察属性的setter方法。
3.重写的setter方法会负责在调用原来的setter方法前后,通知所有观察对象:值的更改。
真正实现通知机制【Person->转换成NSKVONotifying_Person】
4.最后通过isa把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么) ,指向这个新创建的派生类,对象就变成了派生类的实例。
(4)runtime 如何实现 weak 属性?runtime如何实现weak变量的自动置为nil?
要实现weak属性,首先要搞清楚weak属性的特点:
weak表示的是一个弱引用,
这个引用不会增加
对象的引用计数,并且当这个所指的对象被释放之后
,weak指针会被赋值为nil
,而在OC中向为nil 的weak对象 发送消息是安全的
。
runtime如何实现weak变量的自动设置为nil?
runtime对注册的类,会进行布局,对于weak对象会放入一个哈希表中,用weak指向的对象内存地址作为key,为此对象的引用计数为0时,会销毁。假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak哈希表里搜索,找到所有以a为键的weak对象,从而设置为nil。
下面是KVO内部实现的监听代码
目的:让self.dog监听self.person的age属性的改变
@property (nonatomic, strong) MJPerson *person;
@property (nonatomic, strong) MJDog *dog;
//分配内存空间
self.person = [[Person alloc] init];
self.dog = [[Dog alloc] init];
//添加键值观察:
addObserver:观察者,负责处理监听事件的对象
forKeypath:观察的属性
options:观察的选项 NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:上下文
//让self.dog监听self.person的age属性的改变
[self.person addObserver:self.dog forKeypath:@"age" options:0 context:nil];
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person.age = 30;
}
!!!系统还会以某种方式在中间代码插入:`[self willChangeValueForKey:@"属性名"];`、`[self didChangeValueForkey:@"变量名"];`
`KVO在调用存取方法之前总是通过isa指针调用``willChangeValueForKey:`,这会记录旧的值,而当改变发生后,``observeValueForKey:ofObject:change:context:`会被调用,之后总是调用`didChangeValueForkey:`
如果需要实现手动触发一个值value的KVO,那么在实现上面的3个调用,就可以实现手动触发了。【手动触发一般只在希望能控制回调的调用时机时才这么做】
注意:所有的kvo监听到事件,都会调用此方法
1.观察的属性
2.观察的对象
3.change属性变化字典(新、旧)
4.上下文,与监听的时候传递的一致
//所以必须在Dog 类的.m文件里实现获取监听的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"狗监听了%@对象的%@属性改变了",object,keyPath);
}
(4)用于封装框架:想怎么改就怎么改
比如如下示例代码:一个person模型
MJPerson.h
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject <NSCoding>
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@property (nonatomic, copy) NSString *name;
#import "MJPerson.h"
#import <objc/runtime.h>
@implementation MJPerson
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
//copy
Ivar *ivars = class_copyIvarList([MJPerson class], &count);
//循环遍历
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档:用UTF8的格式
NSString *key = [NSString stringWithUTF8String:name];
//kvc键值编码 传入key获取key对应的值
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
//释放这个指针变量
free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([MJPerson class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 解档
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
!!!区分:键路径(keyPath)、键值编码(KVC)、键值观察(KVO)
键路径:
- 在一个给定的实体中,同一个属性的所有值具有相同的数据类型。
- KVC键值编码是一种间接访问对象属性的机制。键路径用(点语法),组成字符串用于指定一个连接在一起的对象性质序列。第一个键的性质是由先前的性质决定的,接下来每个键的值也是相对于前面的性质。
- 键路径可以独立于模型实现的方式相关对象的性质。通过键路径,可以指定对象图中的一个任意深度的路径,使其指向相关对象的特定属性。
键值编码KVC:
- 原理:通过
key
找到value
。当通过KVC调用对象时,比如:[self valueForKey:@"create"]
,程序会自动视图通过几种方式去解析这个调用.- 首先查找对象是否带有create这个方法,如果没有,会继续查找对象是否带有
create
这个实例变量(iVar)
,如果还是没有找到,程序会继续视图调用- (id)valueForUndefinedKey: 方法
。如果还是没有实现,程序会抛出一个NSUndefinedKeyException异常错误。
- 设计
valueForUndefinedKey:方法
主要是为了当使用- (id)valueForKey方法
从对象中请求值时,对象能够在错误发生前,有最后的机会响应这个请求。 - KVC查找方法的时候,不仅仅会查找
create
这个方法,还会查找getcreate
这个方法,前面加一个get
,或者_create
以_getcreate
这几种形式。同时查找实例变量的时候也会不仅仅查找create
这个变量,也会查找_create
这个变量是否存在。
- 首先查找对象是否带有create这个方法,如果没有,会继续查找对象是否带有
- KVC是一种间接访问对象属性,用字符串来标识属性,而不是通过调用
getter,setter
存取方法,直接或间接通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,简化了代码 - KVC的缺点:一旦使用KVC,编译器无法检查出错误。编译器不会对设置的键、键路径进行错误检查,执行效率也低于合成存取器
getter,setter
方法。因为使用KVC键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
4.内存管理
自动引用计数ARC
ARC由编译器在编译时自动生成release
和retain
代码,自动管理内存,ARC编译器由两部分:前端编译器和优化器
-
前端编译器:前端编译器会
拥有的
每一个对象插入相应的release语句。如果对象的所有权修饰符是_strong
,那么它就是被拥有的。- 在某个方法内创建了一个对象,前端编译器会在方法末尾自动插入
release
语句来销毁它。 - 在类用用的对象【实例变量、属性】会在
dealloc
方法内释放。ARC会自动完成这一实现。 - 由编译器生成的代码甚至会比自己写的
release
语句性能还要好,在ARC中没有类覆盖的release方法,就不会去调用了。retain也是一样,ARC对调用objc_retain
取代保留信息
- 在某个方法内创建了一个对象,前端编译器会在方法末尾自动插入
-
ARC优化器:ARC优化器负责移除多余的
retain
和release
语句,确保生成的代码运行速度高于MRC的代码
在ARC下,不需要手动管理内存,编译器会根据引用计数来帮我们管理内存,会在合适的位置帮我们release
或autorelease
。
ARC
的出现降低了出现崩溃、内存泄露等风险,减少了开发者的工作量。
ARC通过什么方式帮助开发者管理内存?
通过retainCount机制
来决定对象是否需要释放,每次runloop的时候,都会检查对象的retainCount
,如果retainCount
为0,说明该对象不需要继续使用,就可以释放了。
ARC相对于MRC,不是在编译时添加retain/release/autorelease这么简单,应该是编译期和运行期两部分共同帮助开发者管理内存。
在编译期,ARC用的是底层的C接口实现的retain/release/autorelease,这样做性能更好,也是为什么不能在ARC环境下手动retain/release/autorelease,同时对同一上下文的同一对象的成对retain/release操作进行优化(即忽略掉不必要的操作) ARC也包含运行期组件,这个地方做的优化比较复杂,但也不能被忽略。
在ARC中与内存管理有关的4种标识符
-
__strong
:默认。强指针指向某个对象,这个对象就会一直存活。 -
__weak
:声明引用不会保持被引用对象的存活,如果对象没有强引用,弱引用会被置为nil -
__unsafe_unretained
:声明引用不会保持被引用对象的存活,如果对象没有强引用,它不会被置为nil,如果它引用的对象被回收掉了,该指针就变成了野指针。 -
__autoreleasing
:标示使用引用传值的参数,在函数返回时会被自动释放掉。
我对ARC的看法:
- 1.每个对象都有一个引用计数器,每个新对象的自动计数器是1,当对象的计数为0时就会被销毁
- 2.通过retain可以让引用计数器+1,release让对象计数器-1
- 3.还可以让autorelease pool管理内存
- 4.如果用ARC,编译器会自动生成管理内存的代码
ARC原则:
- 1.谁调用谁释放,只要调用了new、alloc、copy、mutableCopy方法产生一个新对象,都必须在最后调用一次release或autorelease
- 2.只要调用了retain,都必须在最后调用一次release或autorelease
- 3.在
@property
中如果使用了copy或retain,就要对不再使用的对象做release操作
7.2引用计数的存储
isa
的指针会拿出一部分空间来存储引用计数,就相对64
位环境来说,就会拿出19
位来存储引用计数,毕竟19bit
(iOS系统)保存引用计数一定够,这时引用计数会由专门的SideTable
类来存储。
SideTable
类用来管理引用计数表和weak
表
//保证原子操作的自选锁
spinlock_t slock;
//保存引用计数的散列表
RefcountMap refcnts;
//保存weak引用的全局散列表
weak_table_t weak_table;
isa
指针不仅可以指向它的类对象,,还会指向一个isa_t
结构体,这个结构体包含类的信息以及引用计数等消息
autoreleasepool自动释放池
8.1自动释放池 autoreleasepool 底层怎么实现?
自动释放池以栈的形式实现:
当我们创建一个新的自动释放池时,它将添加到栈顶。当一个对象收到发送autorelease消息时,它被添加到当前线程的处于栈顶的自动释放池,当自动释放池被回收时,它们从栈中删除,并且会给自动释放池里面所有的对象都做一次release操作。
也就是说自动释放池NSAutoreleasePool内部包含一个可变数组NSMutableArray,用来保存对象的声明。
如果一个对象声明为autorelease,那么,系统就会把这个对象加入到自动释放池中的这个可变数组中。
自动释放池NSAutoreleasePool自身在销毁的时候,会遍历一遍这个数组,release数组中的每个成员。
总结:autoreleasepool 以一个队列的形式出现,主要通过三个函数完成。 对autorelease执行了push和pop操作,销毁对象时执行release操作。
- 1.
objc_autoreleasepoolPush
- 2.
objc_autoreleasepoolPop
- 3.
objc_autorelease
8.2自动释放池的手动创建和自动创建
手动创建
1.不是基于Application Kit的程序,比如命令行工具,则没有自动释放池的内置支持,需要手动创建
2.生成一个从属线程(子线程),一旦该线程开始执行,必须立即创建自动释放池,否则会泄露对象
3.写了一个循环,其中创建很多的临时变量,可以在循环内部创建一个自动释放池,
在达到一定数量的时候销毁这些对象,再创建一个自动释放池,帮助减少应用程序的最大内存占有量
自动创建
一般情况下,系统自动创建自动释放池--Application Kit会在一个事件周期(或事件循环迭代)的开端,
【比如点击事件,自动创建一个自动释放池,并且在事件周期的结尾释放它。】
8.3objc使用使用什么机制管理对象内存?
通过retainCount机制
来决定对象是否需要释放,每次runloop的时候,都会检查对象的retainCount,如果retainCount为0,说明该对象没有地方需要继续使用,就可以释放掉了。
8.4不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
基本信息:autorelease机制是iOS开发管理对象内存的好伙伴,在MRC中,调用[obj autorelease]来延迟内存的释放,ARC中不需要管理。
概念:NSAutoreleasePool内部包含一个数组NSMutableArray,用来保存声明,如果一个对象声明为autorelease,系统所做的工作就是把这个对象加入到这个数组中。NSAutoreleasePool自身在销毁的时候,会遍历一遍这个数组,release数组中的每个成员。
分两种情况:手动干预释放时机,系统自动去释放
1.手动干预释放时机:指定autoreleasepool就是在当前作用域大括号结束时释放。
2.系统自动去释放:不手动指定autoreleasepool
Autorelease对象除了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的runloop迭代结束时释放。
从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。
所有的autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。
- 运行循环检测到事件并启动后,就会创建自动释放池。
- 在一次完整的运行循环结束之前,会被销毁。
- 子线程的runloop不是自动创建的,需要手动创建。
- 自定义的NSOperation和NSThread需要手动创建自动释放池。比如:自定义的NSOperation类中的main方法里就必须添加自动释放出。否则除了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。
- @autoreleasepool当自动释放池被销毁或者耗尽时,会向自动释放池中的所有对象发送release消息,释放自动释放池中的所有对象。
- 如果在一个vc的viewDidLoad中创建一个Autorelease对象,那么该对象会在viewDidAppear方法执行前就被销毁了。
8.5什么情况下回帮我们加autorelease
,放进自动释放池
1.当使用alloc/new/copy/mutableCopy
方法进行初始化时,会生成并持有对象(也就是不需要pool池子
管理了),系统会自动在合适的位置对这些对象进行release
对于id类型id obj = [NSMutableArray array];
这种情况会自动将返回值的对象注册到autoreleasepool
@autoreleasepool{
id _autoreleasing obj = [NSMutableArray array];
}
2._weak
修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象可能被废弃。所以必须把对象注册到autoreleasepool
中,那么在@autoreleasepool
块结束之前都能确保对象的存在。
id __weak objweak = obj0;
NSLog(@"class=%@",[objweak class]);
对应的源代码为:
id __weak objweak = obj0;
id __autoreleasing tmp = objweak;
NSLog(@"class%@",[tmp class]);
3.id
的指针或对象的指针在没有显式指定时会被附加上_autoreleasing
修饰符
8.6怎么保证多人开发进行内存泄露的检查
1.使用Product、Analyze静态分析工具,进行代码的静态分析。
【如果有内存泄露危险会提示蓝色标识信息"Prtential leak of an object stored into “变量名”】
2.为了避免不必要的麻烦,多人开发时尽量使用ARC
8.7什么情况下会发生内存泄露和内存溢出?
1.内存泄露:
(1)当程序在申请内存后,无法释放已申请的内存空间【例如一个对象或者变量,使用完后没有释放,这个对象一直占着内存】,
一次内存泄露危害可以忽略,但是内存泄露堆积后果严重,无论多少内存,迟早都会被耗光。内存泄露最终会导致内存溢出。
(2)当2个对象互相引用的时候。【解决方法:让其中一个成为weak】
(3)循环引用导致内存泄露:
1)block中若引用对象,需将该对象修饰为【在ARC】__unsafe_unretain或者__weak
2)代理delegate不要使用retain属性,要用weak属性避免循环。
2.内存溢出:
当程序在申请内存时,没有给足够的空间供其使用,出现out of memory。
【例如申请了一个int类型的空间,但给变量存了long类型才能存下的数,这就是内存溢出了】`
8.8针对内存管理,有没有遇到内存暴增的时候
1.当列表滑动的时候内存莫名增长
原因:
1.没有使用UITableView的reuse重用机制,导致每显示一个cell都用autorelease的方式重新alloc一次,导致cell的内存不断的增加`
2.每个cell会显示一个单独的UIView,在UIView发生内存泄露,导致cell的内存不断增长
5.runloop
5.1 runloop内部实现原理
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出,如果我们需要一个机制而这个机制就是NSRunLoop
的实现机制,让线程能随时处理事件但并不退出。
int main(int argc, char * argv[]){
//程序一直运行状态
while(AppIsRuning){
//睡眠状态,等待唤醒事件
id whoWakesMe = SleepForWakingUp();
//得到唤醒事件
id event = GetEvent(whoWakesMe);
//开始处理事件
HandleEvent(event);
}
return 0;
}
5.2 runloop和线程有什么关系?
总的来说,Run loop 一直运行着的循环。实际上,run loop和线程是紧密相连的。runloop是为了线程而生,没有线程,它就没有存在的必要。
Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop,每个线程保证程序的主线程(main thread)都有与之对应的run loop对象。
1.主线程的run loop默认是启动的。
iOS应用程序里,程序启动后会有一个main()函数
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
int main(int argc, char *argv[]){
@autoreleasepool{
return UIApplicationMain(argc,argv,nil,NSStringFromClass([AppDelegate class]));
}
}
2.对其他线程来说,run loop默认是没有启动的。如果你需要更多的线程交互,需要手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要
3.在任何一个Cocoa程序的线程中,都可以通过以下代码来获得当前线程runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
5.3 NSRunLoop的实现机制,及在多线程中如何使用
1.实现机制:NSRunLoop是iOS消息机制的处理模式
(1)NSRunLoop的主要作用:控制NSRunLoop里面线程的执行和休眠,
在有事情做的时候让当前的NSRunLoop控制线程工作,没事让当前的NSRunLoop控制的线程休眠
(2)NSRunLoop就是一直在循环检测,从线程start到线程end,
检测inputsource(如点击,双击等操作)异步事件,检测timesource同步事件,检测到输入源会执行处理函数
首先会产生通知,Corefunction向线程添加runloop observes来监听事件,意在监听事件发生时来做处理
(3)runloopmode 是一个集合,包括监听:事件源和定时器,以及需通知的runloop observes
2.在多线程中使用:
(1)
只有在为程序创建子线程的时候,才需要运行runloop。对于程序的主线程而言,runloop是关键部分。Cocoa提供了运行主线程runloop的代码,同时也会自动运行runloop
iOS程序UIApplication中的run方法在程序正常启动的时候会启动runloop。
如果使用xcode提供的模板创建的程序,那永远不需要自己去启动runloop
(2)
在多线程中,需要判断是否是runloop。如果需要runloop,那么就要负责配置runloop并启动,不需要任何情况下都去启动runloop。比如,使用线程去处理一个预先定义好的耗时极长的任务时,就可以启动runloop了。
runloop 只在要和线程交互时才需要
5.4 runloop定时源和输入源
1.创建的程序不需要显式创建runloop;
每个线程,包括程序的主线程(main thread)都有与之相关的runloop对象,主线程会自行创建并运行runloop
2.runloop 处理的输入事件有2种不同的来源:输入源(input source)
和定时源(timer source)
- input source:包括
Port事件端口
、Custom
、performSelector:onThread:选择器
对应主线程Thread中的handlePort:
、customSrc:
、mySelector:
- Timer source:
timerFired
3.区别:
输入源传递异步信息,通常来自其他线程或程序。
定时源则传递同步信息,在特定的事件或者一定的时间间隔发生
5.5 runloop的mode作用是什么?
- eg:定时器,处理需要采取不同的 mode
model主要是用来指定事件在运行循环中的优先级的,分为:
- 苹果公开:NSdefaultRunLoopMode(KFCRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:ScrollView滑动时
- UIInitializationRunLoopMode:启动时
- 苹果公开:NSRunLoopCommonMode(KFCRunLoopCommonModes):Mode集合
倒计时如何实现 dispatch_source_t _timer
- NSTimer
- GCD
- RAC (不会)
/**
倒计时的实现
*/
- (void)startTimer {
//倒计时时间
__block int timeout = 30;
//全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建定时器
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0);//每秒执行
//设置事件处理
dispatch_source_set_event_handler(_timer, ^{
if (timeout <= 0) { //倒计时结束关闭
dispatch_source_cancel(_timer);
dispatch_async(dispatch_get_main_queue(), ^{
//设置界面上按钮显示,根据自己的需求设置
[_timeButton setTitle:@"发送验证码" forState:UIControlStateNormal];
_timeButton.userInteractionEnabled = YES;
});
} else {//倒计时还没结束
int seconds = timeout % 60;
NSString *timerStr = [NSString stringWithFormat:@"%.2d", seconds];
dispatch_async(dispatch_get_main_queue(), ^{
//设置界面的按钮显示,根据自己需求设置
[_timeButton setTitle:[NSString stringWithFormat:@"%@秒后重新发送", timerStr] forState:UIControlStateNormal];
_timeButton.userInteractionEnabled = NO;
});
timeout --;
}
});
//将定时器挂起
dispatch_resume(_timer);
}
Cocoa Touch
1.事件处理
iOS的响应者链UIResponder工作原理
对于iOS来说事件类型有:3种。
- 1.Touch Events(触摸事件)
- 2.Motion Events(运动事件,比如重力感应和摇一摇等)
- 3.Remote Event(远程事件,比如用耳机上的按键来控制手机)
响应者链=事件传递+事件接收。
事件传递:
- 当我们的手指触摸屏幕上的某个view时,此时就发生了触摸事件
- 系统会把事件加入UIApplication管理的事件队列中去,然后把UIApplication最前面的事件分发下去处理。发送事件给程序的主窗口(keyWindow)
- 主窗口寻找最适合的视图来响应事件
当view接收到事件时就要开始响应了(向上抛)
- 先去找父视图,如果父视图无法处理,就一直传递到视图层次的最顶级
- 如果视图的最顶层也无法处理,就会传递给window对象
- 如果还无法处理,就会交给
UIApplication
对象,如果还是无法处理,就废弃掉。
系统是怎么找到接收触摸事件发生的视图的?
在传递过程中:
-
会判断自己能否接收事件、
-
触摸点是否在自己身上【pointInside】、
-
遍历子控件,同时也会判断子控件能否接收事件,触摸点是否子控件身上、
-
如果没有符合条件的子控件,就自己处理
-
通过hitTest事件,返回 响应点击事件的 view
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
-
点击坐标 返回事件是否发生在本视图以内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
2.UIApplication
UIApplication:提供iOS运行期间的控制和协作工作。【处理用户事件】
每一个程序在运行期间必须有且仅有一个
UIApplication
的一个实例。在程序开始运行的时候,UIApplicationMain
函数是程序进入点,这个函数做了很多工作,其中一个重要的就是出创建了一个UIApplication
的单例实例,可以通过调用[UIApplication sharedApplication]来得到这个单例实例的指针
。
UIApplication
的一个主要工作是处理用户事件,它会挂起一个队列,把所有用户事件都放入队列,逐个处理,在处理的时候,它会发送当前事件到一个合适的目标控件来处理事件。
UIApplication
实例会被赋予一个代理对象,以处理应用程序的生命周期(比如程序启动和关闭)、系统事件(比如来电)。
UIApplication生命周期。
一个UIApplication可以有如下几种状态:
-
Not running 未运行
:程序没有启动 -
Inactive 未激活
:程序在前台运行,不过没有接收到事件。【程序一般处于这个状态】 -
Active 激活
:程序在前台运行而且接收到了事件,这也是一个正常的模式 -
Background 后台
:程序在后台而且能执行代码,大多数程序进入这个状态后,会在这个状态上停留一会,时间到之后会进入挂起状态(Suspended).有的程序经过特殊请求后可以长期处于background状态 -
Suspended 挂起
:程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。
3.UIView
iOS中图形显示原理
在iOS系统中所有显示的视图都是从基类UIView
继承而来的,同时UIView
负责接收用户交互,但是实际上你所看到的视图内容,包括图形等,都是由UIView
的一个实例图层属性来绘制和渲染的,那就是CALayer
.
CALayer是UIView的内部实现细节。
在每个
UIView
实例当中,都有一个默认的支持图层,UIView
负责创建并且管理这个图层。实际上这个CALayer
图层才是真正用来显示屏幕的,UIView
仅仅是对它的一层封装,实现了CALaer
的delegate
,提供了处理事件交互的具体功能,还有动画底层方法的实现API。
UIWindow、UIView、CALayer的关系
- 1.
UIWindow
是UIView
的子类,即UIView
>UIWindow
. -
UIWindow
的主要作用:一是用来显示UIView
,二是涉及到事件响应机制,负责将事件分发给UIView
- 2.
UIView
与CALayer
相同点:关系是2个相同等级关系,每个UIView
都有一个CALayer
实例的layer属性
,UIView
负责管理这个图层CAlayer
- 不同点:
UIView
继承了UIResponder
与NSObject
,UIView
可以构建用户界面,并且响应用户事件。而CALayer
只继承了NSObject
,不能处理任何的用户事件,只负责顶层内容的绘制。
简单说一下APP的启动过程,从main文件开始说起
程序启动分为2类:1.有storyboard 2.没有storyboard
有storyboard情况下:
- 1.
main函数
- 2.
UIApplicationMain
- 创建
UIApplication对象
- 创建
UIApplication的delegate对象
- 创建
- 3.根据Info.plist获得最主要storyboard的文件名,加载最主要的storyboard
- 创建
UIWindow
- 创建和设置
UIWindow
的rootViewController
- 显示窗口
- 创建
没有storyboard情况下
- 1.
main函数
- 2.
UIApplicationMain
创建UIApplication对象
创建UIApplication的delegate对象
- 3.delegate对象开始处理(监听)系统事件(没有storyboard)
- 程序启动完毕的时候,就会调用代理的
application:didFinishLaunchingWithOptions:方法
- 在
application:didFinishLaunchingWithOptions:
中创建UIWindow
- 创建和设置
UIWindow
的rootViewController
- 显示窗口
- 程序启动完毕的时候,就会调用代理的
XIB与Storyboard的优缺点
XIB:
- 优点:直观【在编译前就提供了可视化界面,可以直接拖控件,】直接给控件添加约束,减少了代码量,通常每个XIB对应一个类。
- 缺点:需求变动大,需要修改XIB可能性大,有时候需要重现添加约束,导致开发周期变长。XIB载入比纯代码慢。多人开发团队,XIB改动容易导致冲突,解决冲突相对要困难很多。
Storyboard
- 优点:开发比较直观【在编译前提供了可视化界面,可拖拉控件,可加约束】,一个storyboard可以有很多界面,每个界面对应一个类文件,通过storyboard可以直观看出整个APP结构。
- 缺点:需求变动时,需要修改storyboard对应的界面约束,会导致大量冲突。
4.UIViewController
ViewController视图控制器生命周期
实际场景
-
当我们调用UIViewController的view时,系统首先判断是否存在view,存在直接返回view
-
若不存在view,在调用loadView() 创建view
当视图为nil时,重新创建view。默认加载``Nib
文件的内容,如果是纯代码构建View,就重写loadView
方法,并且不能调用父类方法
-
若为自定义方法,则执行自定义方法
-
若不是自定义方法,判断是否有xib、Storyboard。
-
有xib、Storyboard就加载xib、Storyboard。
-
没有创建一个空白的view。
-
调用viewDidLoad():视图创建完毕进行的-->界面元素初始化操作。比如:加载视图控件、加载数据
[super loadView]
-
viewWillAppear:
窗口即将要显示,在这个方法里可以改变当前屏幕方法或状态栏风格
viewDidUnload什么时候调用?
当内存警告时,系统可能会释放view,将view赋值为nil,并且调用viewDidUnload方法。
2.@property中有哪些属性关键字(后面有哪些修饰符)?
@property 的说明可以有以下几种:
* readwrite 是可读可写特性;需要生成 getter 方法和 setter 方法
* readonly 是只读特性,只会生成 getter 方法 不会生成 setter 方法,不希望属性在类外改变时使用
* assign 是赋值特性,setter 方法将传入参数赋值给实例变量;仅设置变量时;
* retain 表示持有特性,setter 方法将传入参数先保留,再赋值,传入参数的 retain count 会+1;
* copy 表示拷贝特性,setter 方法将传入对象复制一份;需要完全一份新的变量时。
* nonatomic 和 atomic ,决定编译器生成的 setter getter是否是原子操作。 atomic 表示使用原子操作,可以在一定程度上保证线程安全。一般推荐使用 nonatomic ,因为 nonatomic 编译出的代码更快
属性分为四类:
- 非原子性--
nonatomic
特性:
在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。请注意,尽管没有名为“atomic的特质(如果其属性不具备nonatomic特质那么都是原子的),但是仍然可以在属性特质中写明这一点,编译器不会报错,若是自己定义存取方法,那么就应该从与属性特质相符的原子性”
- 读写权限--
readwrite(读写)
、readonly(只读)
- 内存管理权限--
-
assign
、普通赋值,一般用于基本数据类型,不进行retain操作。常见委托设计模式,来防止循环引用(弱引用)。与weak的区别,向值为nil
的assign对象发送消息是危险的。
-
weak
、非拥有关系,表示一种弱引用
,这个引用不会增加对象的引用计数,并且当这个所指的对象被释放之后,weak
指针会被赋值为nil
,在OC中向值为nil
的weak对象发送消息是安全的 。只能修饰OC对象
-
unsafe_unretained
、相当于assign,相当于【ARC】__weak修饰block。避免循环引用。用于对象类型,表示一个非拥有的,同时而不会在对象被销毁时被置为nil。【unsafe】状态
-
retain
、对象的setter方法对参数进行:release旧值,再retain新值。实现顺序。
-
copy
、与strong类型。【在赋值时进行copy操作,而不是retain操作】使用:【需要保留某个不可变对象,并且防止它被意外改变时使用】copy修饰的对象进行赋值时,为了确保对象的值不会无意改变,copy了一份副本,副本的改变不会影响原来的内容,原来内容的改变不会影响副本先对旧值release,再copy出新对象,引用计数器为1,这是为了减少对上下文依赖而引入的机制。 -
strong
、拥有关系,是一种强引用(直接决定对象的存亡),由strong修饰的指针变量,对象不会被释放。当指针指向新值或者指针不复存在了,相关联的对象就会被自动释放【ARC下】 当给强引用的属性设定一个新值的时候,首先这个值进行引用计数器+1retain
,旧值也不会进行release
,而进行类似assign
的操作。不过当属性指向的对象被销毁时,该属性就会被置nil。
-
nonatomic
、非原子性访问,线程安全,不加同步,多线程并发访问会提高性能。默认是原子型atomic
4.方法-- getter=<name>
、setter=<name>
getter=<name>的样式:
@property (nonatomic, getter=isOn) BOOL on;
5.ARC
下默认关键字:
- 基本数据类型默认为:
atomic
、readwrite
、assign
- 对象类型默认为:
atomic
、readwrite
、strong
2.1property的本质:
property
有2大概念:ivar(实例变量)
、存取(getter
+setter
=access method
)
-
property
主要是用来保存对象中的数据,同时提供设置变量和读取变量的方法 - 在属性定义的时候,编译器已经帮我们向类中添加实例变量以及生成方法的
getter
和setter
2.2@synthesize
与@dynamic
的认识
-
@synthesize
如果没有手动实现setter
方法和getter
方法,编译器会自动加上这2个方法。用来自定义我们实例变量的名称 不建议使用@synthesize 自定义实例变量的名字firstName = _myFirstName;
-
@dynamic
告诉编译器:属性的setter
与getter
方法由用户自己实现,不自动生成。
2.3有了自动合成属性autosynthesis,@synthsize还有哪些使用场景?
不会自动合成的情况:
- 1.同时重写了
setter
和getter
方法 - 2.重写了只读属性的getter方法
- 3.使用了
@dynamic
动态绑定 - 4.在协议
@protocol
中定义的所有属性 - 5.在分类
category
中定义的所有属性 - 6.当重载父类中的属性
当你在子类中重载了父类中的属性,你必须使用@synthesize来手动合成ivar
当你想手动管理@property的所有内容时,你就会尝试通过实现@property的所有存取方法(the accessor methods)或者使用@dynamic来达到这个目的,这时编译器就会认为你打算手动管理@property,于是编译器就禁用了autosynthesis(自动合成)。这时需要借助@synthesize来手动合成ivar。
3.客户端优化
3.1iOS安装包优化
- 资源优化:对资源文件下手,压缩图片和音频,去除不必要的资源
- 编译优化:去除符号信息(新版
xcode
默认) - 可执行文件优化:
- 查看第三方库编译之后.o文件的 大小,如果对大小影响很大的考虑替换
- 删除无用代码。删除没有引用的类和没有调用的方法。
3.2内存优化
- 延迟加载视图和资源
- 做好缓存工作,如果服务器没有更新资源,客户端没必要再多请求一次,可用
YYCache
直接加在本地资源 -
自动释放池Autorelease pool
降低内存 - 做好图片缓存工作
3.3性能优化
- 1.不要将
view
设置为透明,避免图层混合,消耗CPU资源 - 2.尽量避免重写
DrawRect
、圆角、阴影这些导致离屏渲染的情况 - 3.层次过多的视图,考虑使用
frame
布局,Autolayout
这时候的性能低于frame
布局 - 4.合理处理线程,
I/O操作
不要在主线程 - 5.预处理和延时加载:尽量提前加载可视化的内容,优先级比较低的可以延迟加载,用来提高用户体验。
- 6.使用正确的接口API
- 了解
imageNamed:
与imageWithContentsOfFile:
的差异(imageNamed:
适用于会重复加载小图片,因为系统会自动缓存加载的图片。imageWithContentsOfFile:
仅加载图片) - 选择合适的容器:例如数组就是根据
下标index
查找快,插入删除慢。字典用键查找快 - 避免日期格式转换:推荐使用C和缓存
NSDateFormatter
。 使用NSDateFormatter
很耗时。
- 了解
- 7.
TableView
的优化 (解决UItableView卡的问题)- 复用单元格合适的重用标识
reuseIdentifier
,重用cell
- 对于高度固定的cell直接设置
rowHight
,保证不必要的高度计算和调用。
-对于cell是动态行高,估算行高后缓存 - 单元格中的视图尽量不透明,单元格中尽量少用动画
- 单元格图片异步加载
- 滑动时
不加载
图片,停止滑动时开始加载 - 使用
runloop
滑动来加载图片,在页面空闲时执行计算,滑动列表时不执行计算任务不然会影响用户体验。原理:当UI
没有滑动时默认的Mode
是NSDefaultRunLoopMode
,当用户正在滑动UIScrollView
时,Runloop
切换到UITrackingRunLoopMode
接受滑动手势和处理滑动事件(包括减速和弹簧效果),此时,其他Mode
(除了NSRunLoopCommentModes
这个组合Mode
下的事件将全部暂停执行,来保证滑动事件的优先处理) -
cell
圆角处理,避免离屏渲染。 - 单元格中的内容在自定义cell类中的drawRect方法内自己绘制
- 减少reloadData全部cell,只reloadRowsIndexPath。(点击的cell)
- 复用单元格合适的重用标识
SDWebImage的原理实现机制。
https://github.com/DevDragonLi/Dev-Repo/blob/master/interview-Set(iOS)/interview-iOS-2.md
2.SDImageCache 是如何做数据管理的?
SDImageCache分为两个部分,一个是内存层面的,一个是硬盘层面的。
内存层面:相当于是个缓存器。以key-value形式存储图片,当内存不够的时候,会清除所有缓存图片。文件替换方式是以时间为单位,删除时间大于7天的图片文件。
当SDWebImageManager
向SDImageCache
要资源师,先搜素内存层面的数据,如果有直接返回。
若没,访问磁盘,将图片从磁盘读取出来,做Decoder(图片解码),将图片对象放到内存层面做备份,再返回调用层。
3.内部做Decoder的原因(以空间换时间)
由于UIImage
的ImageWithData
函数是每次画图时才将NSData
解压成ARGB
图像的,这样效率低,但是只有瞬间的内存需求。
为了提高效率通过
SDWebImageDecoder
将包装在Data
下的资源解压,然后画在另一张图片上,这样这张新图片就不再需要重复解压了。
单元格cell重用机制的理解:
- UITableView通过重用单元格来达到节省内存的目的。通过为每个单元格指定一个重用标示(reuseidentifier),即指定单元格的种类。。当单元格滚出屏幕时,允许恢复单元格以便复用。
- 当表视图单元(cell)离开屏幕时,它们会被添加到重用队列中,等待被重用
- 当有新cell要进入屏幕内时,【会随机调用已经滚出屏幕的cell所占的内存】根据单元格指定的重用标示在重用队列中查找是否有相应的cell
- 若有,就直接用,没有就根据标识符重新创建一个。
对于多变多种类型的cell,这种重用机制会导致内容出错,解决方法:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"重用标示"];
修改为:
UITableViewCell *cell = [tableView cellForRowIndexPath:indexPath];
离屏渲染:带来界面卡顿问题 【当前屏幕渲染、离屏渲染、CPU渲染】
在OpenGL中,GPU屏幕渲染有2种方式:
- On-Screen Rendering 当前屏幕渲染,在用于显示的屏幕缓冲区中进行,不需要额外创建新的缓存,也不需要开启新的上下文,所以性能好,但是受到缓存大小限制等因素,一些复杂的操作无法完成。
- Off-Screen Rendering 离屏渲染,指在GPU的当前屏幕缓冲区外开辟新的缓冲区进行操作。
- 离屏渲染比当前屏幕渲染代价高:体现如下2方面
- 创建新的缓冲区
- 上下文切换。离屏渲染整个过程,需要多次切换上下文环境,先从当前屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,这又需要将上下文环境从离屏切换到当前屏幕。
设置了以下属性就会触发【离屏渲染】
- shouldRasterize(光栅栏)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
为了避免卡顿问题,应尽可能使用当前屏幕渲染,可以不适用离屏渲染则尽量不用,
应当尽量避免使用layer的border
,corner
,shadow
,mask
等技术。必须离屏渲染时,简单的视图使用CPU渲染,相对复杂的视图使用一般的离屏渲染。
使用drawRect有什么影响
- 缺点:它处理touch事件每次按钮被点击后,都会用setNeedsDisplay进行强制重绘,而且不止一次。多个Button会造成多次强制绘制。
- drawRect的调用机制:当调用
setNeedsDisplay
时,UIKit
将会把当前图层标记为dirty
,但还是会显示原来的内容,直到下一次的视图渲染周期,才会标记为dirty
的图层重新建立Core Graphics
上下文,然后将内存中的数据恢复出来,再使用CGContextRef
进行绘制
CPU渲染与离屏渲染的区别
由于GPU的浮点运算能力 比 CPU强,CPU渲染的效率可能不如离屏渲染。但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。一个常见的 CPU 渲染的例子是:重写 drawRect
方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地完成,渲染得到的bitmap
最后再交由GPU用于显示。总之,具体使用 CPU 渲染还是使用 GPU 离屏渲染更多的时候需要进行性能上的具体比较才可以。
一个常见的性能优化的例子就是如何给 UIView/UIImageView 加圆角。
如下是三种加圆角的方式:
- 设置 cornerRadius
- UIBezierPath
- Core Graphics(为 UIView 加圆角)与直接截取图片(为 UIImageView 加圆角)
如下是这三种方法的比较:
cornerRadius
view.layer.cornerRadius = 6.0;
view.layer.masksToBounds = YES;
这种方式会触发两次离屏渲染,如果在滚动页面中这么做的话就会遇到性能问题。当然我们可以进行缓存以优化性能,如下:
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
shouldRasterize = YES 会使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存,当然要在视图内容不改变的情况下。
注意:png 图片 在 UIImageView 这样处理圆角是不会产生离屏渲染的。(ios9.0之后不会离屏渲染,ios9.0之前还是会离屏渲染)。
UIBezierPath
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
[self.image drawInRect:bounds];
}
这种方法会触发一次离屏渲染,很多资料推崇这种写法,但是这种方式会导致内存暴增,并且同样会触发离屏渲染。
Core Graphics(为 UIView 加圆角)与直接截取图片(为 UIImageView 加圆角)
正如你所期待的那样,这种方法应该是极具效率的正确的姿势。这里将为 UIView 添加圆角与为 UIImageView 添加圆角进行区分。
使用 Core Graphics 为 UIView 加圆角
这种做法的原理是利用 Core Graphics 自己画出了一个圆角矩形。
func kt_drawRectWithRoundedCorner(radius radius: CGFloat,
borderWidth: CGFloat,
backgroundColor: UIColor,
borderColor: UIColor) -> UIImage {
UIGraphicsBeginImageContextWithOptions(sizeToFit, false, UIScreen.mainScreen().scale)
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 开始位置); // 开始坐标右边开始
CGContextAddArcToPoint(context, x1, y1, x2, y2, radius); // 这种类型的代码重复四次
CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
let output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output
}
这个方法返回的是 UIImage,有了这个图片后,就可以创建一个 UIImageView 并插入到视图层级的底部:
extension UIView {
func kt_addCorner(radius radius: CGFloat,
borderWidth: CGFloat,
backgroundColor: UIColor,
borderColor: UIColor) {
let imageView = UIImageView(image: kt_drawRectWithRoundedCorner(radius: radius,
borderWidth: borderWidth,
backgroundColor: backgroundColor,
borderColor: borderColor))
self.insertSubview(imageView, atIndex: 0)
}
}
在调用时 只需要像这样写:
let view = UIView(frame: CGRectMake(1,2,3,4))
view.kt_addCorner(radius: 6)
直接截取图片为 UIImageView 加圆角
这里的实现思路是直接截取图片:
extension UIImage {
func kt_drawRectWithRoundedCorner(radius radius: CGFloat, _ sizetoFit: CGSize) -> UIImage {
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: sizetoFit)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
CGContextAddPath(UIGraphicsGetCurrentContext(),
UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.AllCorners,
cornerRadii: CGSize(width: radius, height: radius)).CGPath)
CGContextClip(UIGraphicsGetCurrentContext())
self.drawInRect(rect)
CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
let output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output
}
}
圆角路径直接用贝塞尔曲线绘制。这个函数的效果是将原来的 UIImage 剪裁出圆角。配合着这函数,我们可以为 UIImageView 拓展一个设置圆角的方法:
extension UIImageView {
/**
/ !!!只有当 imageView 不为nil 时,调用此方法才有效果
:param: radius 圆角半径
*/
override func kt_addCorner(radius radius: CGFloat) {
self.image = self.image?.kt_drawRectWithRoundedCorner(radius: radius, self.bounds.size)
}
}
在调用时只需要像如下这样写:
let imageView = let imgView1 = UIImageView(image: UIImage(name: ""))
imageView.kt_addCorner(radius: 6)
注意:需要小心使用背景颜色。因为没有设置
masksToBounds
,因此超出圆角的部分依然会被显示。因此不应该再使用背景颜色,可以在绘制圆角矩形时设置填充颜色来达到类似效果。
总结
- 如果能够只用 cornerRadius 解决问题,就不用优化。
- 如果必须设置 masksToBounds,可以参考圆角视图的数量,如果数量较少(一页只有几个)也可以考虑不用优化。
- UIImageView 的圆角通过直接截取图片实现,其它视图的圆角可以通过 Core Graphics 画出圆角矩形实现。
参考链接
- 小心别让圆角成了你列表的帧数杀手
- http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
- https://medium.com/ios-os-x-development/perfect-smooth-scrolling-in-uitableviews-fd609d5275a5
- UIKit性能调优
- http://articles.cocoahope.com/blog/2013/03/06/applying-rounded-corners
4.持久化方案(数据存储)
- 1.
NSUserDefaults
:Preference用户偏好设置NSUserDefaults,用来存储配置信息,存放临时文件 - 2.
Plist(NSArray\NSDictionary)
- 3.解档、归档
NSkeyedArchiver\NSKeyedUnarchiver
:一种序列化方式,只要遵循了NSCoding
协议的对象可以通过它实现序列化,用这种方式来存储对象 - 4.
SQLite
基于C语言的轻型的嵌入式数据库,苹果原生的数据库,灵活 - 5.
CoreData
:对SQLite数据库的封装 ,苹果自带的框架 - 6.
FMDB
:以OC的方式封装了SQLite数据库框架。FMDB更加面向对象,省去了很多C语言代码,比苹果自带的Core Data
框架更加轻量级灵活,提供多线程安全的数据库操作方法,有效地防止数据混乱
FMDatabaseQueue:用于多线程中执行多个查询或更新,线程安全
、
FMDatabase用来执行SQL语句
、FMResultSet执行查询后的结果集
iOS中常用的数据存储方式有哪些?
-
综合
-
所有的本地持久化数据存储的本质都是写文件,而且只能存到沙盒中。
-
沙盒机制是苹果的一项安全机制,本质就是系统给每个应用分配了一个文件夹来存储数据,而且每个应用只能访问分配给自己的那个文件夹,其他应用的文件夹是不能访问的。
-
数据存储的核心都是写文件。主要有四种持久化方式:属性列表,对象序列化,SQLite 数据库, CoreData
-
属性列表:应用于少量数据存储,比如登陆的用户信息,应用程序配置信息等。只有NSString ,NSArray,NSDictory,NSData,可以WriteToFile;存储的依旧是plist文件,plist文件可以存储的7种数据类型:array,dictory,string,bool,data,date,number。
-
-
详细
-
对象序列化:最终也是存为属性列表文件,如果程序中,需要存储的时候,直接存储对象比较方便,例如有一个设置类,我们可以把设置类的对象直接存储,就没必要再把里面的每一个属性单独存到文件中。对象序列化是将一个实现了NSCoding协议的对象,通过序列化(NSKeydArchiver)的形式,将对象中的属性抽取出来,转化成二进制流,也就是NSData,NSData可以选择write to file 或者存储到NSUserdefault中。 必须实现的两个方法 encodeWithCoder,initWithCoder。对象序列化的本质就是 对象NSData。
-
SQLite: 适合大量,重复,有规律的数据存储。而且频繁的读取,删除,过滤数据,这种适合使用数据库 (iOS 使用第三方FMDB)
-
CoreData: Sqlite叫做关系型数据库,CoreData 是一中OR-Mapping的思想 ,O代表对象Object,R代表relationship,Mapping代表映射,直译过来就是对象关系映射,其实就是把对象的属性和表中的字段自动映射,简化程序员的负担,以面向对象的方式操作数据库。ORMapping是一种思想,CoreData实现了这种思想,在Java中,hibernate 也是对ORMapping的一种实现,只是利用java实现的。
-
CoreData 本质还是数据库,只不过使用起来更加面向对象,不关注二维的表结构,而是只需要关注对象,纯面向对象的数据操作方式。我们直接使用数据库的时候,如果向数据库中插入数据,一般是把一个对象的属性和数据库中某个表的字段一一对应,然后把对象的属性存储到具体的表字段中.取一条数据的时候,把表中的一行数据取出,同样需要再封装到对象的属性中,这样的方式有点繁琐,不面向对象。CoreData解决的问题就是不需要这个中间的转换过程,看起来是直接把对象存储进去,并且取出来,不关心表的存在,实际内部帮你做好了映射关系。
-
4.1简单描述客户端缓存机制?
1.缓存可以分为:内存数据缓存、数据库缓存、沙盒文件缓存
2.当获取数据的时候:
(1)先检测内存中有没有缓存
(2)在检测本地数据库、沙盒文件有没有缓存
(3)都没有的话,就发送网络请求
(4)将服务器返回的网络数据进行缓存,缓存到内存、数据库、沙盒文件中,以便下次读取
4.2SQLite数据存储怎么用?
1.添加SQLite动态库 libsqlite3.dylib
2.导入头文件:#import<sqlite3.h>
3.利用C语言函数创建、打开数据库,编写SQL语句
4.第三方框架 FMDB对SQLite3封装
4.3CoreData:对SQLite数据库的封装
1.NSManagedObject:
实体对象(1个类对应一张表,1个对象对应表中的一条记录)
2.NSPersistentStoreCoordinator:
相当于存储器,决定了你的数据存储在什么地方(SQLite、XML、其他文件)
3.NSManagedObjectContext:
操作数据库。在多线程中安全,是一个单例。
要想在多线程中访问CoreData,最好的办法是一个线程一个NSManagedObjectContext
NSManagedObjectContext会在使用NSPersistentStoreCoordinator前上锁,
每个NSManagedObjectContext对象实例都可以使用同一个NSPersistentStoreCoordinator实例。
5.对沙盒的理解
- 每个iOS应用都被限制在沙盒中,沙盒相当于一个加了仅主人可见权限的文件夹,就是在应用程序安装过程中,系统为每个单独的应用程序生成它的主目录和一些关键字的子目录。
苹果对沙盒的以下限制:
- 1.应用程序在自己的沙盒中运作,但是不能访问任何其他应用程序的沙盒
- 2.应用程序之间不能共享数据,沙盒里的文件不能被复制到其他应用程序的文件夹中,也不能把其他应用文件夹复制到沙盒中
- 3.苹果禁止任何读写沙盒以外的文件,禁止应用程序将内存写到沙盒以下的文件夹中
- 4.沙盒目录里有:
-
.app
:包含资源文件和可执行文件 -
Documents
:存储应用程序的数据文件,用户数据其他定期备份信息,【例如:游戏排名数据,可以被iTunes同步】 -
Documents.Inbox
:保存外部请求应用程序打开的文件。采用复制的方式,将其他应用程序拷贝到这个目录。 -
Library
:用来存放默认设置设置或其他状态信息。Library
下面还有2个文件夹 -
Library.Caches
:存放缓存文件,可再生文件不会被同步 -
Library.Perferences
:包含应用程序的偏好设置文件,使用NSUserDefaults
写的设置数据都会被保存到该目录下的一个plist
文件中 -
temp:
存放临时文件,程序再次启动不需要的文件
-
- 6.获取沙盒根目录的方法:用
NSHomeDirectory
获取。 - 7.获取Document路径:
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)
6.设计模式
6.1通知NSNotification
和KVO``观察键值监听
的区别和用法是什么?
NSNotification通知模式:
对于跨模块的类交互,可以给多个对象传递数据消息。一个通知能被多个对象接收,一个对象能接收多个通知。比如:每个应用程序都有一个通知中心,键盘
6.2KVO观察者模式:
键值监听,监听某个类的属性值的变化,当发生变化时,而做出相应的改变。比如:监听模型里面某个属性值的变化,发生变化时,更新UI显示
KVO性能不好,底层会动态产生新的类。【在运行期会动态地创建这个类的派生类,在这个派生类中重写基类的setter方法,去观察属性的变化。】
特点:一个对象的属性能被多个对象监听,1个对象能监听多个对象的其他属性
6.3通知模式
在iOS中广播通知(broadcast notification)、本地通知(local notification)、推送通知(push notification)。
区别:
-
广播通知
是Cocoa Touch框架中实现观察者模式的一种机制,它可以在一个应用内部的多个对象之间发送消息 -
本地通知
和推送通知
中的通知
是给用户一种提示,它的提示方式有警告对话框、发出声音、振动和在应用图标上显示数字等。在计划时间到达时,本地通知
由本地iOS发出,推送通知
由第三方程序发送给苹果的远程服务器,再由远程服务器发送给iOS特定的应用。
广播通知的实现原理:
在通知机制中对某个通知
感兴趣的所有对象都可以成为接收者
。
- 首先,接收对象需要向通知中心(NSNotificationCenter)发出
addObserve:selector:name:object:
消息进行注册, - 在投送对象投送通知给通知中心时,通知中心就会把通知广播发出给注册过的接收者。
- 所有的接收者都不知道是谁投送的,更不用关心它的细节。
- 投送对象与接收者是一对多的关系,接收者如果对通知不再关注,会给通知中心发出
removeObserve:name:object:
消息解除注册,以后不再接收通知。
6.4Delegate代理模式
实现原理:
- 【发起】--【委托方】 委托人 老板
保持了``指向委托对象
员工 的"弱引用"【代理必须用弱引用:为了防止内存引用计数器增加而导致委托对象无法释放的问题】 - 【通知】--【协议】 委托人要做的事情是
委托协议
,委托协议规定了要实现的方法 - 【实现】--【受委托方 真正做事的】
委托对象
实现了协议规定实现的方法
示例:UITextField为例,
-
UITextField
是委托方
, -
UITextFieldDelegate
是委托协议
, -
UITextFieldDelegate的实现方法
就是受委托方
,比如文本框开始编辑前textFieldShouldBeginEditing:
,文本框开始编辑后textFieldDidBeginEditing:
6.5单例模式
什么是单例模式?
- 单例模式是一种常用的设计模式,单例模式可以保证某个类创建出来的对象永远只有一个,通过全局的一个入口点对这个实例对象进行访问
单例模式的作用?
-
节省内存开销
:如果一些数据整个程序都用得上,那么只需要使用一份资源。一般把工具类设计为单例模式
如何实现一个单例模式:
有三种方式:
1.简单 单例模式:可以延迟加载,按需分配内存来节省开销。但这并不是一个线程安全的写法,比如2个或多个线程并发的调用sharedInstance
方法,可能会得到多个实例。
Cocoa 库本身在一些地方也使用了单例模式,例如[NSNotificationCenter defaultCenter]
,[UIColor redColor]
Singleton.h
#import "Foundation/Foundation.h"
@interface Singleton:NSObject
+ (Singleton *)sharedInstance;
@end
Singleton.m
#import "Singleton.h"
static Singleton *instance = nil;
@implementation Singleton
+(Singleton *)sharedInstance{
if(!instance){
instance = [[super allocWithZone:NULL] init];
}
return instance;
}
2.加锁 @synchronized
可以使用@synchronized进行加锁。这种写法是懒加载,保证了线程安全,但是锁的存在当多线程访问时,性能会降低。
+ (Singleton *)sharedInstance{
@synchronized (self){
if(!instance){
instance = [[super alloc] init];
}
}
}
3.GCD,推荐使用的。满足了线程安全问题,GCD可以确保以更快的方式完成这些检测,它可以保证block中的代码在任何线程通过dispatch_once 调用之前被执行,但它不会强制每次调用这个函数都让代码进行同步控制
函数原型如下:
void dispatch_once(
dispatch_once_t *predicate,
dispatch_block_t block);
实现思路:
ARC下实现1.2.3
- 1.声明一个单例对象的静态实例,并初始化为nil-->
static id _instace = nil;
- 2.创建一个类方法,分配内存空间,当且仅当这个类的实例为nil时,生成该类的实例。-->
*当一个变量初始化为alloc时,内部会自动调用allocWithZone方法*
+ (id)allocWithZone:(struct_NSZone *)zone{
if(_instace == nil){
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
_instace = [super allocWithZone:zone];
});
}
return _instace;
}
*重写init初始化方法*
- (id)init{
static dispatch_once_t once_Token;
dispatch_once(&onceToken,^{
_instace = [super init];
});
return _instace;
}
- 3.实现
NSCopying协议
,覆盖allocWithZone:
方法,确保用户在直接分配和初始化对象时,不会产生另一个对象
当关键字 使用copy的时候,会自动调用这个方法,把对象分配到zone里
+ (id)copyWithZone:(struct _NSZone *)zone
{
return _instace;
}
+ (id)mutableCopyWithZone:(struct _NSZone *)zone
{
return _instace;
}
MRC还要实现以下3个方法
- 4.覆盖
release、autorelease、retain、retainCount方法
,来确保单例的状态
重写retain,保证这个对象一直到在,不会创建一次就被释放
- (id)retain{
return self;
}
保证对象的计数器一直都为1
- (NSUInteger)retainCount{
return 1;
}
释放对象
- (void)release
{
}
- 5.在多线程环境下,使用
@synchronized关键字或者GCD的dispatch_once,以确保静态实例被正确的创建和实例化
- ARC下
#if __has_feature(objc_arc) endif
- 非ARC下
#if !__has_feature(objc_arc) end if
6.6分类模式category
iOS中的category
是在运行时决定的,在已经存在的类中添加方法,在不修改原来模型的基础上扩充方法。
区分:extension类扩展:是在编译时决定的,属于类的 一部分。
在编译时和头文件里的@interface
以及实现文件里的@implement
一起形成一个完整的类,它随着类产生和消亡。extension
一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension
。【所以无法为系统的类比如NSString
添加extension
】
-
1.分类只扩充方法,不扩充变量,而
类扩充方法和变量,产生新的类
。分类无法添加实例变量(因为在运行时,对象的内存布局已经确定,如果添加会破坏类的内部布局,这对编译型语言来说是灾难) -
2.分类有名称,类扩展没有名称
-
3.类扩展写在.m文件中
区分OC中类的扩展--Protocol,Category和Extension
Protocol:Delegate(委托)是Cocoa中常见的一种设计模式,是依赖于protocol
这个语言特性的。
OC是单继承的,OC中的类可以实现多个protocol
来实现类似C++中多重继承的效果。
Protocol
译为协议。定义了一个方法列表,这个方法列表中的方法可以使用@required
,@optional
标注,以表示该方法是否是客户类必须要实现的方法。一个protocol
可以继承其他的 protocol
.
@protocol test<NSObject>//NSObject也就是一个protocol,这里即继承NSObject里的犯法
- (void)Print;
@end
@interface B:NSObject<test>
- (void)Print;//默认方法是@required的,必须实现的
@end
@protocol和category中如何让使用@property
- 在protocol中使用property只会生成setter和getter方法声明,我们使用属性的目的是希望遵守协议的对象能实现该属性
- category使用@property也是只会生成setter和getter方法的声明,如果我们真的需要给category增加属性的实现,需要借助运行时的2个函数:
objc_setAssociatedObject
objc_getAssociatedObject
反射
- 简单反射:获取类class、根据字符串获取类
- 检查是否有Selector
-
isKindOfClass
和isMemberOfClass
- 高级反射:关联对象等
6.7工厂模式(Factory)
让一个类的实例化延迟到子类中进行
比如:生成控件的API,封装成一套全是扩展的方法
12.多线程
12.1基本介绍
多线程的原理:
同一时间,CPU只能处理一条线程,只有一条线程在工作。多线程并发【同时】执行,其实就是CPU快速地在多条线程之间调度切换,如果CPU调度线程的时间够快,就造成了多线程并发执行的假象。
如果线程非常多,CPU会在N多条线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低。
多线程底层的实现:
(1)搞清楚什么是线程、多线程?
(2)Mach是第一个以多线程方式处理任务的系统,【Mach是一个系统,比Linux、unix都要早】,因此多线程的底层实现机制是基于Mach的线程
(3)开发中很少用到Mach级的线程,因为Mach级的线程没有提供多线程的基本特征,线程实现是独立的。
(4)开发中的多线程方案:
- C语言的POSIX接口:
#import<pthread.h>
- OC的NSThread
- C语言的GCD接口(性能最好,代码更精简)
- OC的NSOperation和NSOperationQueue(基于GCD)
多线程的应用:
一个程序运行后,默认会开启一条线程,称为"主线程或父线程"
主线程的主要作用:处理事件【点击事件、滚动事件、拖拽事件】
主线程的使用注意:别把比较耗时的操作放到主线程中,耗时操作会卡住主线程,严重影响流畅度,给用户一种“卡”的坏体验。
种类
1.NSThread:轻量级的线程。一个NSThread对象表示一条线程.缺点
:需要自己管理线程的生命周期,线程同步,线程同步对数据的加锁会有一定的系统开销。
2.NSOperation:优点不需要关心线程管理,数据同步的事情。 NSOperation和NSOperationQueue也能实现多线程编程
因为NSOperation是一个抽象类,不能封装操作,必须使用它的子类
,NSInvocationOperation和NSBlockOperation
,来创建操作对象,开启执行操作。
下面看NSInvocationOperation封装操作
默认情况下。操作对象在主线程中执行,是同步执行,只有将NSInvocationOperation
添加到NSOperationQueue队列中
才会异步执行
操作
实现具体步骤:
(1)封装对象:先将需要执行的操作封装到一个NSOperation对象中
(2)添加对象到队列:将NSOperation对象添加到NSOperationQueue中
(3)系统自动取出对象:系统会自动将NSOperationQueue中的NSOperation对象取出来
(4)将取出的NSOperation封装的操作放到一条新的线程中执行
NSOperationQueue操作队列。
作用:NSOperation可以调用start方法来执行任务,但默认是同步执行的,
如果将NSOperation添加到NSOperationQueue操作队列中,
系统会自动异步执行NSOperation中的操作,添加操作到NSOperationQueue中,自动执行操作,自动开启线程。
3.GCD(Grand Central Dispatch)伟大的中枢调度器--异步执行任务的技术之一
前言
GCD
是纯C语言的APT,我们在编写GCD相关代码的时候,面对的是函数而不是方法,GCD
函数大部分都是以dispatch
开头,因为GCD
在libdispatch
这个库中,这个调度库包含了GCD所有的东西,在程序运行的过程中,程序会自动加载这个库,不需要手动导入。
3.1GCD内部怎么实现的?
- (1)iOS和OS X的核心是XNU内核(虽然系统是unix但是unix基于XNU),所以GCD是基于XNU内核实现的。
- (2)GCD的API全部在libdispatch库中
- (3)GCD的底层实现主要有
Dispatch Queue
(管理block:存放任务))和Dispatch Source
(处理事件,任务:执行什么操作)
3.2GCD的优势:
- GCD是苹果公司为多核的并行运算提出的解决方案
- GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行声明任务,不需要编写任何线程管理代码
3.3GCD的核心:任务和队列
基本常识:任务:执行什么操作
、队列:用来存放任务
GCD的使用主要就2个步骤:
(1)定制任务
、
(2)确定想做的事情
将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行。
GCD任务block的取出只支持FIFO队列【Dispatch Queue通过结构体和链表,实现FIFO
NSOperationQueue可以方便调整执行顺序,设置最大并发数量
3.3.1GCD的核心--任务
1.GCD:有2个用来执行任务的函数:
把右边的(block)任务提交给左边的(queue)队列执行。
基本常识:同步异步--决定要不要开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
(1)同步方式执行任务:在当前线程中执行``dispatch_sync(dispatch_queue_t queue, ^(void)block);
(2)异步方式执行任务:在另一条线程中执行`` dispatch_async(<#dispatch_queue_t queue#>, ^(void)block);
总结:
dispatch_sync
中block的执行线程和dispatch_sync
上下文线程是同一个线程;
dispatch_async
中的block的执行线程和dispatch_async
上下文不是同一个线程
主队列中的异步任务还是在主队列中执行
串行队列中的任务,无论是同步执行还是异步执行,执行顺序都是FIFO
3.3.2GCD的核心--队列
GCD队列分为串行队列【任务一个接一个执行】
和并行队列:让多个线程并发同时执行,自动开启多个线程同时执行任务 只在异步(dispatch_async)函数下才有效
基本常识:串行并发--决定了任务的执行方式
串行:一个任务执行完后再执行下一个
并发:多个任务并发同时执行
串行队列:
(1)利用dispatch_queue_create创建串行队列
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t);
或者
dispatch_queue_t queue = dispatch_queue_create("alisa", NULL);
(2)使用主队列(跟主线程相关的队列)
主队列是GCD自带的一种特殊的串行队列,放在队列中的任务,都会放到主线程中执行, 如果把任务放到主队列中执行,不论处理函数是异步还是同步都不会开启新的线程
dispatch_queue_t queue = dispatch_get_main_queue();
并发队列:
GCD默认提供了全局的并发队列,供整个应用使用,不需要手动创建
(1)获得全局的并发队列,priority是优先级。 参数 flags暂时传0
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flages);
(2)获得全局并发队列 优先级默认是一个全局的并发队列。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//全局并发数列的优先级:都是宏
DISPATCH_QUEUE_PRIORITY_HIGH 高
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认
DISPATCH_QUEUE_PRIORITY_LOW 低
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台
代码示例
文顶顶的GCD代码示例
//用异步函数往并发队列中添加任务:同时开启2个子线程
- (void)async{
//1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.添加任务到队列中:用异步函数:具有开启新线程的能力
dispatch_async(queue, ^{
NSLog(@"下载图片1------%@",[NSThread currentThread]) ;
});
dispatch_async(queue, ^{
NSLog(@"下载图片2------%@",[NSThread currentThread]) ;
});
//打印主线程
NSLog(@"主线程------%@",[NSThread mainThread]);
}
3.3.3与NSOperationQueue的区别
(1)GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本的封装
(2)GCD只支持FIFO队列,NSOperationQueue可以方便地调整执行顺序,设置最大并发数量
(3)NSOperationQueue可以在Operation间设置依赖关系,而GCD需要写很多的代码才能实现
(4)NSOperationQueue支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
(5)GCD的执行速度比NSOperationQueue快
任务之间不太互相依赖:GCD。
任务之间有依赖或者要监听任务的执行情况用:NSOperationQUeue。
12.4应用:用NSOperation 异步下载图片
1.用NSOperation 异步下载图片,创建一个全局的queue来管理下载图片的操作。
//存放所有下载操作的队列
@property (nonatomic, strong) NSOperationQueue *queue;
2.创建2个字典来存放所有的下载操作和下载完成的图片
//存放所有的下载操作(url是key,operation的对象是value)
@property (nonatomic, strong) NSMutableDictionary *operations;
//存放所有下载完成的图片,用于内存缓存,同样用url作为key
@property (nonatomic, strong) NSMutableDictionary *images;
3.具体思路:
- 先判断内存中(images字典里)有没有图片
- 如果有,取出url对应的图片显示
- 没有,去沙盒缓存中查看,存到沙盒缓存中的都是NSData对象
- 如果沙盒缓存中有,取出对应的数据给cell显示
- 如果没有,先显示占位图片,再创建operation去执行下载操作
- 创建operation之前,先判断这个operation操作是否存在,
- 取出当前URL对应的下载操作 -->
NSBlockOperation *operation = self.operations[app.icon];
- 如果没有下载操作,才需要真正的创建operation执行下载
- 创建好后把这个操作存放到全局队列中去异步执行-->
[self.queue addOperation:operation];
- 同时把操作放入搭配operations字典中记录下来-->
self.operation[app.icon] = operation;
- 下载完成后,把下载好的图片放到内存中,同时存到沙盒缓存中
- 回到主线程刷新表格,从operations字典中移除下载操作(防止operations越来越大,同时保证下载失败后,能重新下载)
刷新当前行的图片数据
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
//存放到沙盒中的代码
if(image){//防止下载失败为空赋值导致崩溃
vc.images[app.icon] = image;
//下载完成的图片存入沙盒中
//UIImage-->NSData-->File(文件)
NSData *ImageData = UIImagePNGRepresentation(image);
NSString *CachesPath = [NSSearchPathForDirectoriesInDomains(NSCacheDirectory,NSUserDomainMask,YES) lastObject];
NSString *filePath = [CachesPath stringByAppendingPathComponent:[app.icon lastPathComponent]];
[ImageData writeToFile:filePath atomically:YES];
}
3.4使用GCD来替代performSelector的原因
3.4.1 performSelector
会导致内存泄露问题
用
performSelector:
调用一个方法,编译器并不知道将要调用的selector
是什么,就不了解其方法签名及返回值,甚至是否有返回值也不知道。
由于编译器不知道方法名,所以没法用ARC的内存规则来判定返回值是不是该释放。
因此,ARC采用了比较谨慎的做法,就是不加释放操作,那么这就可能导致内存泄露了,因为方法在返回值对象时已经将其保留了。
3.4.2performSelector返回值只能是void或者对象类型(id类型)
如果想返回整数或浮点数等scalar类型(标量)值,那么就需要执行一些复杂的转换操作,而这种转换操作很容易出错。
由于id类型表示指向任何OC对象的指针,所以,只要返回的大小和指针所占大小相同就行。
也就是说,在32位架构的计算机上,可以返回任意32位大小的类型,而64位架构的计算机上,则可以返回任意64位大小的类型。
可以返回NSNumber进行转换,若返回的类型为C语言结构体,则不可使用performSelector方法
3.4.3performSelector提供的方法局限性大
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
具备延迟功能的方法无法处理带有2个参数的选择子。而能够指定执行线程的哪些方法,则与之类似,所以不是特别通用。如果要用这些方法,就得把很多参数打包到字典中,然后在被调用的方法中将这些参数提取出来,这样会增加开销,同时也提高了产生bug的可能性。
3.5 GCD中block的使用问题
3.5.1 GCD会对block进行复制
Dispatch Queue对添加的Block会进行复制,在完成执行后自动释放。不需要手动添加block到queue时 显式复制
3.5.2 GCD中的autorelease pool自动缓存池
GCD dispatch queue 有自己的autorelease pool来管理内存对象,但是不保证在什么时候会进行回收,如果在block中创建了大量的对象,可以添加自己的autorelease pool来进行管理。
3.5.3 GCD中国在开新的线程执行任务一定比较快吗?
如果对于工作量小的block,切换线程的开销
大于直接在原来线程上执行block的开销
这样会导致新的线程没有原来的线程执行的快
3.5.4 GCD中的block会造成循环引用吗?
会。如果控制器持有block,会造成循环引用
如果只持有队列queue是不会造成循环引用的。
3.6GCD常见用法
3.6.1延迟执行
- (void)delayOperation
{
//延迟执行:5秒钟之后,执行block中的代码段
//1.主队列中,在主队列中执行
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( 5* NSEC_PER_SEC)) , queue, ^{
//5秒钟后要执行的操作;
});
//2.在并发队列中
dispatch_queue_t Bqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//设置时间
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
//开启一条新的线程执行
dispatch_after(when, Bqueue, ^{
NSLog(@"并发延迟执行的操作");
});
}
3.6.2只执行一次的代码
- (void)onlyOnetime
{
//不用dispatch_once的写法
if (_log == NO) {
NSLog(@"该行代码执行一次");
_log = YES;
}
//使用dispatch_once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"该行代码执行一次");
});
}
3.6.3变更优先级dispatch_set_target_queue
- (void)setTargetqueue{
//默认优先级为Global Dispatch Queue
dispatch_queue_t mySerial = dispatch_queue_create("串行", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
//变更优先级
dispatch_set_target_queue(mySerial, globalDispatchQueueBackground);
}
3.6.4Dispatch Group:追加到Dispatch Queue中的多个处理全部结束后,执行结束处理。
比如:【如何用GCD同步若干个异步调用?(如根据若干个rul异步加载多张图片,然后在都下载完成后合成一张整图)】
使用Dispatch Group,追加3个block到Dispatch Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中处理结束的block
- (void)dispatchGroup
{
//创建一个全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//穿件一个组队列
dispatch_group_t group = dispatch_group_create();
//将block任务添加到队列中
dispatch_group_async(group, queue, ^{
NSLog(@"block1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"block2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"block3");
});
//dispatch_group_notify 追加结束处理的函数
//dispatch_group_notify函数会将执行的block追加到Dispatch Queue中
dispatch_group_notify(group, queue, ^{
NSLog(@"done");
});
//等待group的处理,DISPATCH_TIME_FOREVER表示只要处理尚未执行结束,就会一直等待,中途不能取消。DISPATCH_TIME_NOW,则不用任何等待即可判断属于Dispatch Group的处理是否执行结束。
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//主线程的Runloop的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间,虽然这样也可以,但一般这种情况下,还是用dispatch_group_notify函数,【追加结束处理到Main Dispatch Queue主队列】中。
//释放
dispatch_release(group);
}
3.6.5Dispatch_barrier_async:
的作用:解决数据竞争问题
- (void)dispatch_barrier_async{
//当访问数据库户文件的时候,写入处理由串行的主队列处理,读取处理追加到并发队列中,写入处理在任一个读取处理没有执行的状态下,追加到串行队列中即可。【在写入处理结束之前,读取处理是不能执行的】
//使用barrier来等待之前任务完成,避免数据竞争问题。
//dispatc_barrier_async函数同dispatch_queue_create函数生成的并发队列一起使用
dispatch_queue_t queue = dispatch_queue_create("dispatch_barrier_async", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue,^{
NSLog(@"block1");
});
dispatch_async(queue,^{
NSLog(@"block2");
});
dispatch_barrier_sync(queue, ^{
NSLog(@"处理数据竞争问题");
});
dispatch_async(queue, ^{
NSLog(@"block3");
});
}
GCD的暂停和继续
3.6.6Dispatch_suspend:不执行已追加的处理,即挂起已追加的队列,当可以执行时再回复
dispatch_suspend函数挂起指定的 Dispatch Queue
dispatch_suspend(queue);
dispatch_resume函数恢复指定的 Dispatch Queue
dispatch_resume(queue);
这些处理对已经执行的处理没有影响,挂起后,追加到Dispatch Queue中但尚未执行的处理在这之后停止执行。dispatch_rusume函数则使得这些处理能够执行
3.6.7实际运用:有些图片加载的比较慢怎么处理?如何优化程序的性能?
(1)图片下载放在异步线程
(2)图片下载过程中使用占位图片
(3)如果图片较大,可以考虑多线程断点下载
【开多条线程同时下载,设置http的请求头信息,比如 字节数:bytes = 0-1024个字节 1024-5120个字节】
3.7 Dispatch Source分派源--基础数据类型,协调特定底层系统事件的处理
是基于BSD系内核惯有功能kqueue的包装
GCD 支持以下 dispatch source :
- Timer dispatch source:定期产生通知
- Signal dispatch source:UNIX 信号到达时通知
- Descriptor dispatch source:各种文件和socket操作的通知
- 数据可读
- 数据可写
- 文件在文件系统中被删除、移动、重命名
- 文件元素改变
- Process dispatch source:进程相关的事件通知
- 进程退出时
- 当进程发起fork或exec等调用
- 信号被递送到进程
- Mach port dispatch source:Mach 相关事件的通知
- Custom dispatch source:自己定义并自己触发
3.7.1Dispatch Source的实现
Dispatch source
替代了异步回调函数,来处理系统相关的事件,当你配置一个dispatch source
时,你指定要监测的事件。
dispatch queue
以及处理事件的代码 (block或函数)当事件发生时,dispatch source
会提交 block或函数到指定的queue去执行。
·dispatch source提交的block或函数和手工提交到queue的任务不同,dispatch source为应用提供连续的事件源。除非你显式地取消,dispatch source会一直 保留与dispatch queue的关联。只要相关的事件发生,就会提交关联的代码到dispatch queue去执行。
为了防止事件积压到 dispatch queue,dispatch source实现了事件合并机制
如果新事件在上一个事件处理器出列并执行之前到达,dispatch source会将新旧事件的数据合并。根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。
3.7.2Dispatch Source和Dispatch Queue两者在线程执行上的关系
两者线程上没有关系,独立运行
Dispatch Queue 相当于任务生产者
Dispatch Source 相当于处理任务的消费者。可以一边异步生产,一边异步消费
可以在任意线程上调用dispatch_source_merge_data
以触发dispatch_source_set_event_handler
.而句柄的执行线程,取决于你创建句柄时所指定的线程
自定义源也需要一个队列,用来处理所有的响应句柄block。那么就有2个队列了,一个队列用来执行自定义源,另一个队列用来执行句柄。
13.AFNetWorking的认识
13.1从NSRLConnection到NSURLSession
block
- 多处使用单例,为了保证线程安全,使用
dispatch_once
,保证执行一次 -
NSURLConnection
没有办法处理多个网络请求时线程的管理问题,还需要借助NSOperation
,对于AFN2.0
,会启动一个线程,借助runloop
一直检测一个port端口
空转,来达到常驻线程的效果,使NSOperation
可以接受回调。 -
NSURLSession
就不必要那么麻烦了,但基于NSURLSession
的AFNetworking
只需要创建一个网络请求任务就可以了,原因在于:NSURLSession
内部已经维护了两个操作队列,一个是处理session
的相关回调,一个是处理响应相关的回调。 -
NSURLSession
是借鉴了AFNetworking
的继承于NSOperation
和用单线程发起并等待响应的思想 -
NSURLConnection
不是基于HTTP2.0
协议,NSURLSession
基于HTTP2.0
协议的多路复用技术【允许同时通过单一的HTTP/2连接发起多重请求-响应消息,因此HTTP/2很容易实现多流并行而不用依赖建立多个TCP连接】,就避免了多个请求,经过三次握手这样延迟的问题,可以在共用一个NSURLSession
14 应用程序类
1414.1在iOS7之前,后台执行内容有几种形式都是什么?
一般的应用在进入后台的时候 可以 获取一定时间来运行相关任务,也就是说可以在后台运行一小段时间(10s左右)
1.后台音乐播放
2.后台GPS跟踪
3.后台voip支持 电话
iOS7之后在plist文件中可以修改
14.2AFN与ASI的区别
ASIHTTPRequest
外号"HTTP终结者",功能强大,可已停止更新。
AFNetworking
简单易用,提供了基本够用的常用功能。
原理分析:
ASI的性能优于AFN,ASI是基于CFNetwork框架开发的
,而AFN是基于NSURL
.
- 所有的网络通信的基础是Socket,一个Socket与另一个连接并传送数据。
BSD Socket
是一类最常见的Socket
抽象接口。 - Core Foundation框架中的
CFSocket
是基于BSD Socket
开发的。几乎涵盖了BSD Socket
的全部功能,更重要是把Socket整合到事件的处理循环中。 -
CFStream
是基于CFSocket
开发的读写流支持。 -
CFNetwork
是基于CFStream
的一个底层的高性能网络框架,由3部分组成:【提供基础服务的CFSocketStream
】,支持HTTP协议的CFHTTP
【用于身份认证的CFHTTPAuthentication
】和【支持FTP
协议的CFFTP
组成】。
ASI
是基于CFHTTP
开发的一个组件,而AFN
的基础是NSURL
。ASI更加底层。
在使用NSOperation和NSOperationQueue上的实现
- ASI直接操作对象
ASIHTTPRequest
,对象ASIHTTPRequest
是NSOperation
的子类,实现了Copying
协议。在initialize
和initWithURL
方法中初始化相关属性并配置一系列请求相关参数默认值。 - ASI在异步请求的处理上,ASIHTTPRequest对象初始化结束后,在startAsynchronous方法中把对象加入共享操作队列。此后,包括创建CFHTTPMessageRef,也就是处理网络请求的主要对象(事实上是一个指向__CFHTTPMessage结构的指针),在内的所有操作都在ASIHTTPRequest对象所属的子线程中完成。
- AFN的直接操作对象AFHTTPClient不同于ASI,是一个实现了NSCoding和NSCopying协议的NSObject子类。
AFHTTPClient是一个封装了一系列操作方法的“工具类”,处理请求的操作类是一系列单独的,基于NSOperation封装的,AFURLConnectionOperation的子类。
AFN的示例代码中通过一个静态方法,使用dispatch_once()的方式创建AFHTTPClient的共享实例,这也是官方建议的使用方法。在创建AFHTTPClient的初始化方法中,创建了OperationQueue并设置一系列参数默认值。在getPath:parameters:success:failure方法中创建NSURLRequest,以NSURLRequest对象实例作为参数,创建一个NSOperation,并加入在初始化发方中创建的NSOperationQueue。
以上操作都是在主线程中完成的。在NSOperation的start方法中,以此前创建的NSURLRequest对象为参数创建NSURLConnection并开启连结。
- 在异步回调的处理上二者也有区别,ASI采取的是CFHTTP请求完成,直接回调ASIHTTPRequest的实例方法,通过储存的实例对象记录的信息完成Delegate模式或Block模式的回调。而AFN则直接使用了NSOperation的completionBlock属性。
14.3程序自己关掉和程序进入后台,远程推送的区别
1.程序自己关掉:关掉后不执行任何代码,不能处理事件
2.程序进入后台:程序进入后台状态不久后转入
挂起状态。
在这种状态下,应用程序不执行任何代码,并有可能在任意时候从内存中删除。
只有当用户再次运行此应用,应用才会从挂起状态唤醒,代码才能继续执行
或者进入后台时开启多线程状态,保留在内存中,这样就可以执行系统允许的动作
3.远程推送是由远程服务器上的程序发送到APNS[苹果的一个与远程推送相关的机制,主要是获取到用户的设备id [device Token],根据device Token来推送消息]
--> 再由APNS把消息推送至设备上的程序,当应用程序收到推送的消息会自动调用特定的方法执行实现写好的代码
14.4本地通知和远程推送通知的基本概念和用法?
概念:本地推送和远程推送通知都可以向不在前台运行的应用发送消息,这种消息可能是即将发送的事件
,也可能是服务器的新数据
。不管是本地通知还是远程通知,它们在程序界面的显示效果相同,都可能显示为一段警告信息或者应用程序图标上的徽章。
目的:都是让应用程序能够通知用户某些事情,而且不需要应用程序在前台运行。
区别:本地通知由本应用负责调用,只能从当前设备上的iOS发出。
而远程通知由远程服务器上的程序发送到APNS,再由APNS把消息推送至用户设备上的程序。【国内有极光推送jpush】
14.5客户端安全性处理方式?
传输数据和保存数据方面
- 传输数据:
- 网络数据传输(敏感数据【账号、密码、消费数据、银行卡账号】,不能明文发送。用加密算法加密)
- 协议的问题:(自定义协议,游戏的代练【模仿协议发送数据给服务器】)
- 保存数据:
- 本地文件存储(游戏的存档,加密。当软件删除时文件也就没有了)
- 源代码方面。iOS是一个.exec的可执行文件,存放的是编译之后的源代码。 【有反编译工具,将编译之后的二进制文件.o--->反编译为runtime运行时的C语言文件。】
14.6sip是什么?
SIP(Session Initiation Protocol),会话发起协议
SIP是建立VOIP连接的IETE标准,IETE是全球互联网最具权威的技术标准化组织
VOIP,就是网络电话,直接用互联网打电话,不用耗费手机话费
14.7如何实现一个框架或者库给别人使用?如果设想和设计框架的public的API,并指出大概需要如何做,需要注意一些什么方面,来使别人容易地使用你的框架。
(1)提供给外界的接口功能是否实用,够用
(2)别人使用我的框架时,能不能根据类名、方法名就能猜出接口的具体作用
(3)别人调用接口时,提供的参数是否够用,调用起来是否简单
(4)别人使用我的框架时,要不要导入依赖其他的框架
14.8手机架构与性能调试?
手机架构:项目一般分层,分为UI层-->``业务层(处理业务逻辑)-->``工具层(处理业务的手段,网络,存储)
(1)刚接手公司的旧项目时,模块比较多,而且代码几乎写在控制器里面,比如UI控件代码,网络请求代码,数据存储代码
(2)接下来才去MVC模式进行封装、重构。这也会造成Controller很臃肿
比如封装,重构:自定义UI控件封装内部的业务逻辑
封装网络请求工具类
封装数据存储工具类
14.9怎么实现原子锁
iOS中一般用nonatomic,在Mac中用atomic。自己加一把锁,在调用getter,setter方法时加锁。
@property (atomic, assign) int age;
- (void)setAge:(int)age{
@synchronized(self) {
_age = age;
}
}
14.10即时通讯中的大数据处理
即时通讯需要长时间连接(长连接,一般用socket实现),发送put,post,https请求上传大数据到文件服务器
,然后利用TCP\IP发带url的自定义格式给对方,对方接收到之后下载。
Socket 建立网络连接的步骤
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket
,另一个运行于服务器端,称为ServerSocket
。
套接字之间的连接过程分为三个步骤:
-
服务器监听:
- 服务器套接字并不定位具体的客户端套接字,而是处于等待连接的状态,
- 实时监控网络状态,等待客户端的连接请求
-
客户端请求:
- 客户端的套接字提出连接请求,要连接的目标是服务端的套接字。
- 客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务端套接字的地址和端口号,
- 向服务端套接字提出连接请求。
-
连接确认:
-
当服务端套接字监听到货接收到客户端套接字的连接请求时
-
响应客户端套接字的请求,
-
建立一个新的线程,把服务端套接字的描述发给客户端,
-
客户端确认了此描述,双方就正式建立连接。
-
服务端套接字继续处于监听状态,继续接受其他客户端套接字的连接请求。
-
AsyncSocket
相关代码 谷歌的开工工程,为了在苹果系统上异步网络I/O
-
#import "AsyncSocket/AsyncSocket.h"
@interface ViewController ()
@property (nonatomic) AsyncSocket *socket;
@property (nonatomic, copy) NSString *socketHost;
@property (nonatomic, assign) uint16_t socketPort;
@property (nonatomic) NSTimer *connectTimer;
@end
//socketConnect
- (void)socketConnectHost {
self.socket = [[AsyncSocket alloc] initWithDelegate:self];
NSError *error = nil;
[self.socket connectToHost:self.socketHost onPort:self.socketPort withTimeout:-1 error:&error];
}
- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
NSLog(@"连接成功");
//每个3s 向服务器发送心跳包
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
[self.connectTimer fire];
}
- (void)longConnectToSocket {
NSLog(@"在longConnectToSocket方法中进行长连接需要向服务器发送的讯息");
// socket发送数据是以栈的形式存放,所有数据放在一个栈中,存取时会出现粘包的现象,所以很多时候服务器在收发数据时是以先发送内容字节长度,再发送内容的形式,得到数据时也是先得到一个长度,再根据这个长度在栈中读取这个长度的字节流,如果是这种情况,发送数据时只需在发送内容前发送一个长度,发送方法与发送内容一样
NSData *dataStream ; //[@8 dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:dataStream withTimeout:1 tag:1];
// 接收数据
}
- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
//对得到的data值进行解析与转换即可
[self.socket readDataWithTimeout:30 tag:0];
}
网络安全
计算机网络面临的安全性威胁
- 被动攻击:截获。攻击者观察分析PDU(协议数据单元)---流量分析
- 主动攻击:中断、篡改、伪造。攻击者对某个连接中通过对PDU进行更改、删除、记录、复制、延迟、组合或伪造的PDU送到另一个连接中。
- 更改报文流:
通过第连接的PDU的真实性、完整性和有序性的攻击
- 拒绝服务 :
攻击者向因特网上的服务器不停地发送大量分组,使因特网或服务器无法提供正常服务。
- 伪造连接初始化:
攻击者重放以前已被记录的合法连接初始化序列,或伪造序列身份企图建立连接。
方案:
- 更改报文流:
- 被动攻击:采用各种数据加密技术
- 主动攻击:将价目技术与鉴别技术相结合
- 特殊的主动攻击:恶意程序攻击,主要有以下几种
- 计算机病毒:会传染其他程序的程序
- 计算机蠕虫:通过网络通信功能将自身从一个结点发送到另一个结点并自动运行的程序。
- 特洛伊木马:执行某种恶意的功能。偷偷拷贝用户源程序。
- 逻辑炸弹:当运行环境满足某种特定条件时执行其他特殊功能的程序。
网络安全协议
运输层(TCP)安全协议:安全套接层SSL(Secure Socket Layer)。
SSL对客户端端与服务器之间传送数据进行加密和鉴别。在双方联络阶段(握手阶段)对将要使用的加密算法(RSA
)和双方共享的会话密钥进行协商,完成客户端与服务端之间的鉴别。
不同角度来看
- 发送方 :(从上到下)
SSL
接收应用层
的数据(如HTTP
或IMAP
报文)对数据进行加密,然后把加密后的数据发往TCP
套接字 - 接收方:(从下到上)
SSL
从TCP
套接字读取数据,解密后把数据交给应用层
。
SSL三大功能
- SSL服务器鉴别:允许用户证书服务器的身份
- 加密的SSL会话:客户和服务器交互的所有数据都在发送方加密,在接收方解密。还提供了检测信息是否被篡改的机制
- SSL客户鉴别:允许服务器证书客户的身份。
SSL工作原理 以浏览器和服务器通信为例
- 1 浏览器想服务器发送-->浏览器要使用SSL版本号和密码编码的参数选择。浏览器和服务端要协商使用哪1种对称加密算法
- 2 服务器想浏览器发送服务器的版本号、密码编码的参数选择和服务器的证书。[证书包括服务器的RSA公钥]
- 3 浏览器有一个可信的CA表,表中有每一个CA公钥。当浏览器接收到服务器发来的证书,就会检查此证书发行者是否在可信的CA表中。若不在偶棉的加密和鉴别连接就不能进行下去了。若在,使用CA响应的公钥对证书解密,得到服务器的公钥。
- 4 浏览器随机生成一个对称会话密钥,并用服务器的公钥加密,然后将加密后的会话密钥发送给服务器。
- 5 浏览器向服务器发送一个报文,表示以后浏览器使用此会话密钥进行加密。
- 6 浏览器向服务器发送一个单独的加密报文,指出浏览器端的握手过程完成。
- 7 服务器也向浏览器发送一个报文,表示以后服务器使用此会话密钥进行加密,再向浏览器发送一个单独加密报文,表示服务端的握手过程已经完成
网络通讯中加密方式有哪些,各自原理?
- md5(哈希算法):把任意长度的字符串加密成一个128bit的大整数,并且是不可逆的。每次固定字符加密出来的密文都相同。如果只是一个"123"字符串,使用MD5加密
- RSA(非对称算法加密):产生一对非对称的公钥和私钥,公钥加密,私钥解密。私钥加密,公钥解密。
- AES(对称加密):加密和解密的密钥都是同一个
- base64(现代密码学基础):(编码方案Base 64)原本8bit一组的数据改成6bit一组,不足的地方补0,每两个0用一个=表示
用户需要上传和下载一个重要的资料文件,应该如何判断操作成功?
- 用
MD5
验证文件的完整性。仅仅通过代码来判断当前的请求发送结束或者接收的数据结束是不可取的。 - 具体做法:
- 上传文件:
- 当客户端上传一个文件的时候,在请求body里面添加该文件的MD5值来告诉服务器
- 服务器接收文件完毕以后通过校验收到的文件的MD5值与请求body里面的MD5值来最终确定本次上传是否成功
- 下载文件:
- 在响应头里面收到了服务器附带的该文件的MD5值,文件下载结束以后,通过获取下载后的文件的MD5值,与本次请求服务器返回的响应头中的MD5值做一个比较,来最终判断本次下载是否成功
- 上传文件:
- MD5,将数据字符串转换成
固定长度的值
的单向操作。- 用途:机密资料的校验,下载文件的检验,明文密码的加密等。
ReactiveCocoa(RAC)如何防止UIButton短时间内多次重复点击,大概思路?
- 建立一个flag
- 时间完成后flag重置,否则一直skip
你一般是如何调试Bug的?
- 在运行过程中,如果出现EXC_BAD_ACCESS 异常,往往提示的信息很少或者没有提示,启用NSZombieEnabled后在控制台能打印出更多的提示信息,便于debug,请注意,僵尸模式下的调试工作只能在模拟器中实现,我们无法在物理设备上完成这一诊断流程.
- 异常断点,一般程序crash时Xcode一般会定位到main函数中,得不到详细的crash信息,打上异常断点后就极大可能定位到程序的crash处,利于debug。
- 一般来说,在创建工程的时候,应该在Build Settings启用Analyze During 'Build',这样每次编译时都会自动静态分析。这样的话,写完一小段代码之后,就马上知道是否存在内存泄露或其他bug问题,并且可以修bugs。
- 如果你想在运行的时候查看APP是否存在内存泄露,你可以使用Xcode上instruments工具上的Leaks模块进行内存分析。但是有些内存泄露是很难检查出来,有时只有通过手动覆盖dealloc方法,看它最终有没有调用。
界面多个网络请求,如何处理刷新
**界面同时有多个网络请求,这时还没得到数据,已经提前刷新导致界面空白 **
- 使用调度组
dispatch_group_t
解决此问题。-
dispatch_semaphore:
信号量
为基于计数器
的一种多线程同步机制
。用于解决在多个线程访问共有资源的时候,会因为多线程的特性而导致数据出错的问题。让线程线程同步问题
-
dispatch_semaphore_create
创建一个信号量信号计数``dispatch_semaphore_create(10)
-
dispatch_semaphore_signal(semaphore)
发送一个信号信号计数会+1
-
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
设置等待时间,此处为一直等待。信号计数小于0会一直等待
,
-
-
semaphore
计数大于等于1、计数-1、返回,程序继续运行。如果计数为0,则等待。
-
1. 调度组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"netWorking_Frist");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"netWorking_Second");
});
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"netWorking_Three");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"complete");
});
2. 信号量
- (void)netWorkingimplementation{ // 基于自身项目的网络请求
// 1. 发起请求
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 2. 成功/失败回调标记
dispatch_semaphore_signal(semaphore);
// 3. 计数为0 ,则一直等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
如何是init方法私有化
- 1.使用instancetype类型,内部不响应
- (instancetype)init {
// 抛出不识别,没有说明真的原因
[super doesNotRecognizeSelector:_cmd];
return nil;
}
- 2.通过断言
- (instancetype)init1{
NSAssert(false,@"unavailable, use sharedInstance instead");
return nil;
}
- 3.通过异常
- (instancetype)init2{
[NSException raise:NSGenericException format:@"Disabled. Use +[%@ %@] instead",
NSStringFromClass([self class]),
NSStringFromSelector(@selector(sharedInstance))];
return nil;
}
如果tableView界面网络请求有缓存数据逻辑
-
首次进入一个界面TableView的数据应该先读取缓存。
- 防止没有网络或网络不好用户长时间等待数据或没有数据,用户体验不好
- 主要用于TableView请求的数据量很大,或者数据短时间变化不是很快的模块
-
用户下拉刷新--重新请求数据,应该强制更新数据 不论本地是否有缓存,将这部分数据同步到数据库,保证退出后下次进入界面能够读取最后最新的缓存
-
上拉加载更多--不读缓存。
-
不用缓存的 地方主要是数据变化快。发起的网络请求是一次性操作。eg:收藏某个题目。
代理(Delegate)--软件设计模式
- iOS中以
@protocol
形式体现 - 传递方式是
一对一
- 与通知最大的不同是:通知是一对多的。
- 委托方---->(要求代理方需要实现的接口方法)协议。
- 协议----> (协议中可以定义方法和属性)--->
- 代理方---->(按照协议去实现方法,可能会有返回值。)
- 代理方---->委托方(代理方返回一个处理结果给委托方。)
- 委托方---->代理方(委托方调用代理方遵从的协议方法 。)
注意
- 代理一般声明为
weak
为规避循环引用。 - 持有情况。
- 代理方 strong--->强持有引用委托方
- 委托方要有代理方有一个声明,weak来声明。