第一章:重新认识Objective-C

2017-05-04 20:14

在当前iOS开发中,主流的开发语言依然是Objective-C(后面简称OC)语言,当然现在的Swift语言喧嚣尘上,但在大部分较大型的iOS软件系统构建中,OC依然占据着不可替代的地位。学习并深刻的认识OC这种语言依然是当前iOS开发中不可或缺的一项任务,一种编程语言的诞生不是偶然,那么,OC是如何诞生的呢?我们首先对它做个总览。

OC的起源

1980年代初布莱德·考克斯(Brad Cox)在其公司Stepstone发明了Objective-C语言,并于1986年将这种语言构建成书。Brad Cox一直专注于软件工程,软件重用性和组建化是OC里面的核心思想。Brad当时想打造一门流行的、可移植的C语言与优雅的Smalltalk的结合体,Smalltalk是历史上第二个面向对象的程序设计语言和第一个真正的集成开发环境 (IDE),由于Smalltalk编程语言对近代面向对象编程语言的影响,因此被称为“面向对象编程之母”。Cox在1983年修改了C编译器用于面向对象编程,因此编译面向对象的C也被称为OOP C。Cox将Smalltalk的object和message passing分层构造在C语言之上,这点让程序设计师可以持续使用熟悉的C语言开发,又可以使用面向对象特性,OC语言就在这个基础上诞生了。

此时,在1976年创建Apple的史提夫·乔布斯因为内部斗争被赶出了苹果公司,这一年是1985年。乔布斯离开后创立了NeXT电脑公司,致力于开发强大且经济的工作站。NeXT获得了Stepstone公司的Objective-C语言授权并且可以发布自己的 Objective-C Compiler和libraries。NeXT使用Objective-C开发了一套NeXTSTEP操作系统,并创建了NeXTSTEP Toolkit软件包,这个工具包用于开发用户界面,功能十分强大。NeXTSTEP系统是以Mach为kernel,加上BSD所打造出来的类unix的操作系统,以Objective-C为本地语言与运行环境,包含有很多面向对象的软件开发套件和各种开发工具(Project Builder, Interface Builder),并且具有很先进的GUI接口。NeXT拥有当时最先进的技术,但是却不能成为最流行的电脑,NeXT Workstations仅仅销售了5000套。

1993年,NeXT终止了硬件业务,转为专注于NeXTSTEP(或称为OpenStep)的软件市场,并推出了一套网络程序架构WebObjects用于进行动态页面的生成。OpenStep实际上是NeXT和SUN公司合作开发的一套系统,可以运行在Soloris和Windows NT上。1994年NeXT与Sun共同制定了OpenStep API标准,其中两个重要的部分是Foundation跟Application kit,此时开始使用命名前缀NS,之后在Objective-C语言的程序中便会看到NX与NS字样,因为Mac OS X、iPhone SDK、Xcode都可追溯到NeXT、NeXTStep系统。

历史的转机来到了1996年,这一年Apple买下了NeXT,乔布斯也于1997年重回Apple。这次并购的主要用意就是要以NeXTStep系统取代老旧的Mac OS,NeXTSTEP被重命名为Cocoa,WebObjects则集成到Mac OS Server和Xcode中。Objective-C自然而然成为Mac平台的首选开发语言,并受到Macintosh编程人员的广泛认可。Cocoa成为苹果免费提供的开发工具,提供Mac平台应用开发的环境。

自此,我们迎来了一个时代,一个乔布斯带给我们的时代。

OC的特性

Objective-C是面向对象的语言,遵从ANSI C标准C语法,是在C语言的基础上,增加了一层最小的面向对象语言。它是一种静态输入语言,在构建中必须先声明数据中每个变量(或者容器)的数据类型。因为使用了Smalltalk的方法在运行时可以灵活处理,因而也是一个动态语言,代码中的某一部分可以在app运行的时候被扩展和修改。

运行时非常灵活,包含有Dynamic Binding(动态绑定)、Dynamic Typing(动态检查)和Dynamic Linking(动态链接)。Dynamic Language几乎所有的工作都可以在运行时处理,使用运行时特性可以最大灵活性的减少RAM和CPU使用。OC完全兼容C语言,在代码中可以混用c,甚至是c++代码。Objective-C可以在任何gcc支持的平台上进行编译,因为gcc原生支持Objective-C。

Objective-C是非常“实际”的语言,它使用一个用C写成的很小的运行库,只会令应用程序的大小增加很小,和大部分OO(面向对象)系统使用极大的VM(如JVM等)执行取代整个系统的运作相反,OC写成的程序通常不会比其原始代码大很多。 Objective-C最初版本并不支持垃圾回收,即是鉴于当时的面向对象语言回收时有漫长的“死亡时间”,会使整个系统失去功用,Objective-C为避免此问题才不拥有这个功能。与之相对,Objective-C的内存管理采用引用计数的方式,后期引入ARC(自动引用计数)。

OC不包括命名空间机制(namespace mechanism),取而代之的是必须在其类别名称前加上前缀,因而会有引致冲突的可能。所有Mac OS X的类别和函式均有“NS”作为前缀,使用“NS”是由于这些类别的名称在NeXTSTEP开发时定下的。虽然C是Objective-C的母集,但它并不视C的基本型别为第一级的对象。和C++不同,Objective-C不支持运算子多载(不支持ad-hoc多型),Objective-C只容许对象继承一个类别(不许多重继承),不过可以使用Categories和protocols实现多重继承。

由于OC使用动态运行时类型,而且所有的方法都是函数调用(有时甚至连系统调用syscalls也如此),很多常见的编译时性能优化方法都不能应用于OC(例如:内联函数、常数传播、交互式优化、纯量取代与聚集等)。这使得OC性能劣于类似的对象抽象语言(如C++等)。OC运行时消耗较大,致使OC不适于当前使用C++的常见的底层抽象,这也是静态处理和动态处理的重要区别。但是,OC本来就不是设计来做这些的,强求于此也大可未必。

对于OC有了一个初步的认识之后,我们将从OC的起源,面向对象原则谈起。

面向对象三原则(封装,继承,多态)

C语言是面向过程的语言(关注的是函数),OC、C++、JAVA、C#、PHP、Swift是面向对象的,面向过程关注的是解决问题涉及的步骤,而面向对象关注的是设计能够实现解决问题所需功能的类,抽象是面向对象的思想基础。想要深刻的了解OC这种语言,面向对象三原则是一个无法绕过的坎。

面向对象具有四个基本特征:抽象,封装,继承和多态。

抽象包括两个方面,一是过程抽象,二是数据抽象。过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值只能通过使用这些操作修改和观察。抽象是一种思想,封装继承和多态是这种思想的实现。

封装

封装是把过程和数据包围起来(即函数和数据结构,函数是行为,数据结构是描述),有限制的对数据进行访问。面向对象是基于这个基本概念开始的(因为面向对象更注重的是类),即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。但是封装会导致并行效率问题,因为执行部分和数据部分被绑定在一起,制约了并行程度。面向对象思想将函数和数据绑在一起,扩大了代码重用时的粒度,而且封装下的拆箱装箱过程中也会导致内存的浪费。

继承

继承是一种层次模型,允许和鼓励类的重用,并提供了一种明确表述共性的方法。新类继承了原始类的特性,新类称为原始类的派生类(子类和父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。继承性很好的解决了软件的可重用性问题,但是,不恰当地使用继承导致的最大的一个缺陷特征就是高耦合(是设计类时层次没分清导致的)。解决方案是用组合替代继承,将模块拆开,然后通过定义好的接口进行交互,一般来说可以选择代理模式。

使用继承其实是如何给一类对象划分层次的问题,在正确的继承方式中,父类应当扮演的是底层的角色,子类是上层的业务。父类只是给子类提供服务,并不涉及子类的业务逻辑,层级关系明显,功能划分清晰,父类的所有变化,都需要在子类中体现,此时耦合已经成为需求。

多态

多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性,很好的解决了应用程序函数同名问题。多态一般都要跟继承结合起来说,其本质是子类通过覆盖或重载父类的方法,来使得对同一类对象同一方法的调用产生不同的结果。

覆盖是对接口方法的实现,继承中也可能会在子类覆盖父类中的方法。重载,是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后在调用时,VM就会根据不同的参数样式,来选择合适的方法执行。在使用重载时只能通过不同的参数样式,例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样)。于此相对,继承会在多态使用混乱的境况产生耦合。更好的方法是使用接口,通过IOP将子类与可能被子类引入的不相关逻辑剥离开来,即提高了子类的可重用性,又降低了迁移时可能的耦合。

接口规范了子类哪些必须实现,哪些可选实现。那些不在接口定义的方法列表里的父类方法,事实上就是不建议覆重的方法。如果引入多态之后导致对象角色不够单纯,那就不应当引入多态,如果引入多态之后依旧是单纯角色,那就可以引入多态;如果要覆重的方法是角色业务的其中一个组成部分,那么就最好不要用多态的方案,转而使用用IOP实现,因为在外界调用的时候其实并不需要通过多态来满足定制化的需求。

OC的动态性

Objective-C是面相运行时的语言,它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。使用Runtime可以按需要把消息重定向给合适的对象,交换方法的实现等等。

Runtime简称运行时,其中最主要的是消息机制,它是一个主要使用C和汇编写成的库。OC的函数调用称为消息发送,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数(在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错,而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。

例如:

[obj makeTest];

将转化为

objc_msgSend(obj,@selector(makeTest));

objc_msgSend方法包含两个必要参数:receiver、方法名(即:selector),如:[receiver message];将被转换为objc_msgSend(receiver, selector);此外objc_msgSend方法也能使用message的参数,如:

objc_msgSend(receiver, selector, arg1, arg2, …);

回到上面的示例,objc_msgSend方法按照一定的顺序进行操作,以完成动态绑定。objc_msgSend函数会依据接收者与selector的类型来调用适当的方法。编译器执行上述转换时,在objc_msgSend函数中首先通过obj的isa指针找到obj对应的class。每个对象内部都默认有一个isa指针指向这个对象所使用的类,isa是对象中的隐藏指针,指向创建这个对象的类,具体的流向可以参照下图。

1170656-3a332c88b6baeb75.png

在Class中先去cache中通过SEL查找对应函数(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中未找到,再去methodList中查找,若methodlist中未找到,则去superClass中查找,若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。

调用实现时,将一系列参数传递过去,最后将该实现的返回值作为自己的返回值返回。objc_msgSend等函数一旦找到应该调用的方法实现之后,就会跳转过去。之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数。由上面的分析可知,每个类里都有一张表格,其中的指针都会指向这种函数,而selector的名称则是查表时所用的“键”,objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的,并且利用了“尾调用优化”(tail-call optimization)技术。

“尾调用优化”技术用在某函数的最后一项操作是调用另外一个函数的情况,编译器会生成调转至另一函数所需的指令码,并且不会向调用堆栈中推入新的“栈帧”(frame stack),只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,并且还会有过早地发生“栈溢出”(stack overflow)的现象。

因此,消息传递的关键是编译器构建每个类和对象时所采用的数据结构。每个类都包含以下两个必要元素:一个指向父类的指针;一个调度表(dispatch table),该调度表将类的selector与方法的实际内存地址关联起来。调用一个方法需要很多步骤,但objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与selector相同的消息,那么执行起来就很快了。当然啦,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,不过只要把selector缓存起来也就不会太慢了。

动态类型、动态绑定和动态加载

OC的动态特性表现为了三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时(runtime)才会做一些事情。

动态类型,就是id类型。动态类型是跟静态类型相对的,内置的基本类型都属于静态类型(int、NSString等)。静态类型在编译的时候就能被识别出来(即前面说的静态输入),因此,若程序发生了类型不对应,编译器就会发出警告,但动态类型在编译器编译的时候是不能被识别的,要等到运行时(runtime),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。

我们可以把任何想要的消息发送给代码中类型为“id”的变量,然后Objective-C的动态消息处理就会在运行时让这一调用正确地工作。但在实际情况中,即使方法的查找是发生在运行时期,这也只够确保正确的方法被调用,但却不足以确保参数是有效的。编译器肯定是需要推断出一些与所涉及的方法签名有关的信息的,即使编译器不需要知道id的类型,但它确实需要知道所有参数的字节长度,以及任何返回值的确切类型。这是因为参数的列集(压入栈以及从栈中弹出它们)是在编译时配置的。通常情况下,我们不需要采取任何步骤来使之发生,参数的信息是通过查看你试图调用的方法的名称来获取的,搜索整个被包含进来的头文件查找与被调用方法的名称吻合的方法,然后从其找到的第一个匹配方法中获取参数的长度。即使你真正指向的确切方法不能被明确分辨出来,匹配方法之间的参数也有可能会是相同的,因为Objective-C中的方法名称通常就暗示了数据的类型。

假设有一个类MyClass,该类有一个名为currentPoint的实例方法,该方法返回一个int类型的值。如何希望调用保存在数组中的对象的currentPoint,就会使用下面的代码:

int result = [[someArray objectAtIndex:0] currentPoint];

运行时调用的方法是正确的,问题是编译器为这一调用列集的参数是不正确的,这导致了数据的返回类型被破坏。[someArray objectAtIndex:0]不能明确的指出得到的一定是类MyClass,解决办法就是强制转换。

int result = [(MyClass *)[someArray objectAtIndex:0] currentPoint];

在消息发送之前,编译器需要正确地把参数压入到栈中,然后执行消息发送,使用正确的objc_msgSend来取回返回值,而这就是出现故障的地方。

编译器使用方法签名(通过查看接收者的类型和该接收者的所有有效方法名称来获取的)来准备参数,并试图找出你可能想要调用的方法是哪一个。由于接收者的类型(比如说objectAtIndex:的调用结果)仅为id,这样的话我们就没有显式的类型信息,因此编译器就会查看所有已知方法的一个列表。如果编译器找到的不是我们的MyClass的方法,而决定匹配NSBezierPath的名为currentPoint的方法,并准备了与该方法的签名相匹配的参数。NSBezierPath的方法返回一个struct类型的NSPoint,而这就导致我们的返回类型被破坏了。

对于实例方法来说,我们通过强制转换成所需的特定对象类型来修正问题,但在两个对象都是类方法的情况下,就不可能强制转换所涉及的特定Class了,在Objective-C中,你不能强制转换类方法。如果你不能够改变方法的名称的话,那么唯一的权变之法看起来就像是这个样子:

 int result = objc_msgSend([someArray objectAtIndex:0], @selector(currentPoint));

是的,需要使用objc_msgSend方法,这是一个极端的案例。

动态语言和静态语言的另一个区别是静态语言提前编译好文件,即所有的逻辑已在编译时确定,运行时直接加载编译后的文件;而动态语言是在运行时才确定实现。典型的静态语言是C++,动态语言包括OC,JAVA,C#等;因为静态语言提前编译好了执行文件,也就是通常所说的静态语言效率较高的原因。

下面我们来看看动态绑定,动态绑定(dynamic binding)需要用到@selector/SEL。关于“函数”,对于其他一些静态语言,比如c++,一般在编译的时候就已经将要调用的函数的函数签名都告诉编译器了,是静态的,不能改变。而在OC中,其实是没有函数的概念的,我们叫“消息机制”,所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去,这就是动态绑定。要实现它就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。这里要注意一点,SEL并不是C里面的函数指针,虽然很像。SEL变量只是一个整数,它是使用方法的ID,以前的函数调用,是根据函数名,也就是字符串去查找函数体。但现在,我们是根据一个ID整数来查找方法,整数的查找自然要比字符串的查找快得多!所以,动态绑定的特性不仅方便,而且效率更高。

动态加载就是根据需求动态地加载资源,在运行时加载新类。在运行时创建一个新类,只需要3步:

1、为 class pair分配存储空间 ,使用 objc_allocateClassPair 函数

2、增加需要的方法使用 class_addMethod 函数,增加实例变量用class_addIvar

3、用objc_registerClassPair函数注册这个类,以便它能被别人使用。

其实就是这么简单。

结合上面的理解,我们详细的看一下示例。

- (void)ex_registerClassPair {
Class TestClass= objc_allocateClassPair([NSObject class], "TestClass", 0);
//为类添加变量
class_addIvar(TestClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
//为类添加方法 IMP 是函数指针  typedef id (*IMP)(id, SEL, ...);
IMP i = imp_implementationWithBlock(^(id this,id other){
    NSLog(@"%@",other);
    return @123;
});
//注册方法名为 testMethod: 的方法
SEL s = sel_registerName("testMethod:");
class_addMethod(TestClass, s, i, "i@:");
//结束类的定义
objc_registerClassPair(TestClass);
1
//创建对象
id t = [[TestClass alloc] init];
//KVC 动态改变 对象t 中的实例变量
[t setValue:@"测试" forKey:@"name"];
NSLog(@"%@",[t valueForKey:@"name"]);
//调用 t 对象中的 s 方法选择器对于的方法
id result = [t performSelector:s withObject:@"测试内容"];
NSLog(@"%@",result);
}

打印结果:

 测试
 测试内容
 123

完全正确。

基于以上的认识,我们来看一道网上的面试题吧(注:此处采用自网络):

@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
    NSLog(@"%@", NSStringFromClass([self class]));
    NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

答案:都输出”Son”

因为oc的方法寻找是在编译期执行的,self表示本类Son,super表示父类Father,NSObject是Son和Father的父类。class方法在NSObject中定义,并且没有被Father和Son覆盖。[self class]的方法寻找方式是:Son,Father,NSObject,[super class]的方法寻找方式是:Father,NSObject,最终执行的都是NSObject里面定义的class方法。要执行的方法找完之后,程序进入运行期,即执行刚才找到的方法。而方法的实际调用者是对象。[super class]和[self class]的实际调用者都是Son对象。因此最后的打印结果应该都是Son。

Method Swizzling

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP类似函数指针,指向具体的Method实现。

用 method_exchangeImplementations 来交换2个方法中的IMP,

用 class_replaceMethod 来修改类,

用 method_setImplementation 来直接设置某个方法的IMP,归根结底,都是偷换了selector的IMP。

示例如下:

- (void)viewDidLoad {
[super viewDidLoad];

Method ori_Method =  class_getInstanceMethod([self class], @selector(testOne));
Method my_Method = class_getInstanceMethod([self class], @selector(testTwo));
method_exchangeImplementations(ori_Method, my_Method);

[self testOne];
}

- (void)testOne {
NSLog(@"原来的");
}

- (void)testTwo {
NSLog(@"改变了");
}

结果:

改变了

其实,就是这么简单。

RunLoop

RunLoop是一个让线程能随时处理事件但不退出的机制。RunLoop实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行Event Loop的逻辑。线程执行了这个函数后,就会一直处于这个函数内部的"接受消息->等待->处理"的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回,让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。总结来说,一个runloop就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。

RunLoop有四个作用:使程序一直运行接受用户输入;决定程序在何时应该处理哪些Event;调用解耦;节省CPU时间。线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时,并且只能在一个线程的内部获取其RunLoop(主线程除外)。主线程的runloop默认是启动的。

OS X/iOS 系统中,提供了两个这样的对象:NSRunLoop和CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

NSRunLoop是一种更加高明的消息处理模式,在对消息处理过程进行了更好的抽象和封装,不用处理一些很琐碎很低层次的具体消息,在NSRunLoop中每一个消息打包在input source或者是timer source中,使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,可以大大节省系统资源。

对其它线程来说,runloop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个Cocoa程序的线程中,都可以通过:

NSRunLoop *runloop = [NSRunLoop currentRunLoop];

获取到当前线程的runloop。

Cocoa中的NSRunLoop类并不是线程安全的,我们不能在一个线程中去操作另外一个线程的runloop对象,那很可能会造成意想不到的后果。但是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的runloop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:

 - (CFRunLoopRef)getCFRunLoop;

获取对应的CFRunLoopRef类,来达到线程安全的目的。

Runloop的管理并不完全是自动的。我们仍必须设计线程代码以在适当的时候启动runloop并正确响应输入事件,当然前提是线程中需要用到runloop。而且,我们还需要使用while/for语句来驱动runloop能够循环运行,下面的代码就成功驱动了一个run loop:

BOOL isRunning = NO;
do {
 isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]];
} while (isRunning);

Runloop同时也负责autorelease pool的创建和释放,在使用手动的内存管理方式的项目中,会经常用到很多自动释放的对象,如果这些对象不能够被即时释放掉,会造成内存占用量急剧增大。Runloop就为我们做了这样的工作,每当一个运行循环结束的时候,它都会释放一次autorelease pool,同时pool中的所有自动释放类型变量都会被释放掉。

系统默认注册的5个Mode

kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用

GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。

kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

轮播图中的NSTimer问题

创建定时器的第一种方法:

 NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(changeImage) userInfo:nil repeats:YES];

此方法创建的定时器,必须加到NSRunLoop中。

NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 
[runLoop addTimer:timer forMode: NSRunLoopCommonModes];

forMode的参数有两种类型可供选择:NSDefaultRunLoopMode , NSRunLoopCommonModes,第一个参数为默认参数,当下面有textView,textfield等控件时,拖拽控件,此时轮播器会停止轮播,是因为NSRunLoop的原因,NSRunLoop是一个死循环,实时监测有无事件响应,如果当前线程是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。NSRunLoopCommonModes 能够在多线程中起作用,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合,这也是将modes换为NSRunLoopCommonModes便可解决的原因。

创建定时器的第二种方法:

self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(changeImage) userInfo:nil repeats:YES];

此种创建定时器的方式,默认加到了runloop,使用的是NSDefaultRunLoopMode。

main函数的运行

在main.m中:

 int main(int argc, char *argv[])
 {
    @autoreleasepool {
      return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));
   }
 }

UIApplicationMain() 函数会为main thread 设置一个NSRunLoop 对象,这就解释了app应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。仅仅在为你的程序创建辅助线程的时候,你才需要显式运行一个runloop。Runloop是程序主线程基础设施的关键部分,所以,Cocoa和Carbon程序( Carbon是苹果电脑操作系统的应用程序编程接口API之一)提供了代码运行主程序的循环并自动启动runloop。iOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。如果你使用xcode提供的模板创建你的程序,那你永远不需要自己去显式的调用这些例程。

对于辅助线程,你需要判断一个runloop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的runloop,比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动runloop。runloop在你要和线程有更多的交互时才需要,比如以下情况:

1.使用端口或自定义输入源来和其他线程通信。

2.使用线程的定时器。

3.使线程周期性工作。

其实,在日常开发中,很少会单独处理这一块的运行。

事件响应链

对于iOS设备用户来说,操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。对应的事件类型有以下三种:

1、触屏事件(Touch Event)

2、运动事件(Motion Event)

3、远端控制事件(Remote-Control Event)

事件的传递和响应分两个链:

传递链:由系统向离用户最近的view传递。

UIKit –> active app’s event queue –> window –> root view –>……–>lowest view

响应链:由离用户最近的view向系统传递。

initial view –> super view –> …..–> view controller –> window –> Application

响应者链(Responder Chain)是由多个响应者对象连接起来的链条,作用是能很清楚的看见每个响应者之间的联系,并且可以让一个事件为多个对象处理。响应者对象(Responder Object)指的是有响应和处理事件能力的对象,响应者链就是由一系列的响应者对象构成的一个层次结构。

UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。

响应者链有以下特点:

  1. 响应者链通常是由视图(UIView)构成的。

  2. 一个视图的下一个响应者是它视图控制器(UIViewController)(如果有的话)。

  3. 视图控制器的下一个响应者为其管理的视图的父视图(如果有的话)。

  4. 单例的窗口(如UIWindow)的内容视图将指向窗口本身作为它的下一个响应者,Cocoa Touch应用不像Cocoa应用,它只有一个UIWindow对象,因此整个响应者链要简单一点。

  5. 单例的应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图。hitTest:withEvent:方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。

引用计数器(ARC 和 MRC)

ARC是自动引用计数器(Automatic Reference Counting),MRC是手动引用计算器(现在几乎不用了,但对于内存管理的理解是有帮助的)。Objective-c中提供了两种内存管理机制MRC(MannulReference Counting)和ARC(Automatic Reference Counting),分别提供对内存的手动和自动管理来满足不同的需求,Xcode 4.1及其以前版本没有ARC。

在MRC的内存管理模式下,与变量的管理相关的方法有:retain,release和autorelease。retain和release方法操作的是引用记数,当引用记数为零时,便自动释放内存。并且可以用NSAutoreleasePool对象对加入自动释放池的变量进行管理,当内存紧张时回收内存。

具体分析如下:

  1. retain,该方法的作用是将内存数据的所有权附给另一指针变量,引用数加1,即retainCount+= 1;

  2. release,该方法是释放指针变量对内存数据的所有权,引用数减1,即retainCount-= 1;

  3. autorelease,该方法是将该对象内存的管理放到autoreleasepool中。

在ARC中与内存管理有关的标识符,可以分为变量标识符和属性标识符,对于变量默认为__strong,而对于属性默认为unsafe_unretained,但也存在autoreleasepool。其中assign、retain、copy与MRC下property的标识符意义相同,strong类似与retain,assign类似于unsafe_unretained,strong、weak、unsafe_unretained与ARC下变量标识符意义相同,只是一个用于属性的标识,一个用于变量的标识(一般带两个下划短线__)。

为了加深理解,我们就来从早期的经验中来详细的分析下OC的内存管理吧。OC内存管理四句箴言:自己生成的自己持有;非自己生成的对象自己也可持有;不再需要自己持有时释放;非自己持有的对象无法释放。

Objective-C中的内存管理机制跟C语言中指针的内容是同样重要的,要开发一个程序并不难,但是优秀的程序则更测重于内存管理,它们往往占用内存更少,运行更加流畅。在Xcode4.2及之后的版本中由于引入了ARC(Automatic Reference Counting)机制,程序编译时Xcode可以自动给你的代码添加内存释放代码。内存管理是开发中不可忽略的一块,虽然ARC帮我们节省了很多精力,不过作为一名合格的开发人员,最基本的知识还是要理解的。

在开发中,如果使用了alloc、new、copy、mutableCopy或以其开头的方法名,意味着自己生成的对象只有自己持有,如:allocMyJob,newItName,copyAfter,mutableCopyYourName。但是以allocate,newer,copying,mutableCopyed开头的是不自己持有的,很明显它们是假冒的。

以下为例:

 id obj = [NSMutableArray array]; // 取得非自己生成并持有的对象

取得的对象存在,但自己并不持有对象。NSMutableArray类对象被赋给变量obj,但变量obj自己并不持有该对象。持有时需要用retain方法,如:

id obj = [NSMutableArray array]; 
[obj retain];

通过retain,非自己用alloc等生成的对象也可以自己持有了。

不需要时,使用release释放,对象一经释放就不可访问了。用alloc/new/copy/mutableCopy持有和retain持有的对象,不需要时一定要用release释放。同理使用如allocObject也可以取得自己持有对象。

id obj1 = [obj0 allocObject];

当使用autorelease时,即使取得了对象存在,但是自己不持有对象。如:

- (id)object
{
id obj = [[NSObject alloc] init]; //自己持有对象
[obj autorelease];
return obj;// 取得的对象存在,但自己不持有
}

[NSMutableArray array]就是因为用了autorelease使得谁都不持有。当然再次使用retain就持有了。程序里所有autorelease pool都是以栈(stack)的形式组织的。新创建的pool位于栈的最顶端。当发送autorelease消息给一个对象时,这个对象被加到栈顶的那个pool中。发送drain给一个pool时,这个pool里所有对象都会受到release消息,而且如果这个pool不是位于栈顶,那么位于这个pool“上端”的所有pool也会受到drain消息。

[pool drain] 和 [pool release] 的区别是:

  1. release,在引用计数环境下,由于NSAutoReleasePool是一个不可以被retain的类型,所以release会直接dealloc pool对象。当pool被dealloc的时候,pool向所有在pool中的对象发出一个release的消息,如果一个对象在这个pool中autorelease了多次,pool对这个对象的每一次autorelease都会release。在GC(garbage-collected environment)环境下release是一个no-op操作(代表没有操作,是一个占据进行很少的空间但是指出没有操作的计算机指令)。

  2. drain,在引用计数环境下,它的行为和release是一样的。在GC的环境下,这个方法调用objc_collect_if_needed 触发GC。重点是:在GC环境下,release是一个no-op,所以除非你不希望在GC环境下触发GC,你都应该使用drain而不是使用release来释放pool。

对于iOS来说drain和release的作用其实是一样的。

一个对象被加到一个pool很多次,只要多次发送autorelease消息给这个对象就可以。同时,当这个pool被回收时,这个对象也会收到同样多次release消息。简单地可以认为接收autorelease消息等同于接收一个retain消息,同时加入到一个pool里,这个pool用来存放这些暂缓回收的对象,一旦这个pool被回收(drain),那么pool里面的对象会收到同样次数的release消息。UIKit框架已经帮你自动创建一个autorelease pool。大部分时候,你可以直接使用这个pool,不必自己创建;所以你给一个对象发送autorelease消息,那么这个对象会加到这个UIKit自动创建的pool里。

某些时候,可能需要创建一个pool:

1.没有使用UIKit框架或者其它内含autorelease pool的框架,那么要使用pool,就要自己创建。

2.如果一个循环体要创建大量的临时变量,那么创建自己的pool可以减少程序占用的内存峰值。(如果使用UIKit的pool,那么这些临时变量可能一直在这个pool里,只要这个pool受到drain消息;完全不使用autorelease pool应该也是可以的,可能只是要发一些release消息给这些临时变量,所以使用autorelease pool还是方便一些)

3.创建线程时必须创建这个线程自己的autorelease pool。使用alloc和init消息来创建pool,发送drain消息则表示这个pool不再使用。pool的创建和drain要在同一上下文中,比如循环体内。

ObjC中没有垃圾回收机制,在ObjC中内存的管理是依赖对象引用计数器来进行的。在ObjC中每个对象内部都有一个与之对应的整数(retainCount),叫“引用计数器”,当一个对象在创建之后它的引用计数器为1,当调用这个对象的alloc、retain、new、copy方法之后引用计数器自动在原来的基础上加1(OC中调用一个对象的方法就是给这个对象发送一个消息),当调用这个对象的release方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。手动管理内存有时候并不容易,因为对象的引用有时候是错综复杂的,对象之间可能互相交叉引用,此时需要遵循一个法则:谁创建,谁释放。

深浅复制和属性为copy,strong时值的变化

浅复制只复制指向对象的指针,而不复制引用对象本身。对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只不个是一个指针,对象本身资源还是只有一份,那如果我们对A_copy执行了修改操作,那么发现A引用的对象同样被修改了。深复制就好理解了,内存中存在了两份独立对象本身。

在Objective-C中并不是所有的对象都支持Copy、MutableCopy,遵守NSCopying协议的类才可以发送Copy消息,遵守NSMutableCopying协议的类才可以发送MutableCopy消息。

[immutableObject copy] // 浅拷贝
[immutableObject mutableCopy] //深拷贝
[mutableObject copy] //深拷贝
[mutableObject mutableCopy] //深拷贝

属性设为copy,指定此属性的值不可更改,防止可变字符串更改自身的值的时候不会影响到对象属性(如NSString、NSArray、NSDictionary)的值。strong属性的指会随着变化而变化,即copy是内容拷贝,strong是指针拷贝。

生命周期

app应用程序有5种状态:

Not running未运行:程序没启动。

Inactive未激活:程序在前台运行,不过没有接收到事件。在没有事件处理情况下程序通常停留在这个状态。

Active激活:程序在前台运行而且接收到了事件。这也是前台的一个正常的模式。

Backgroud后台:程序在后台而且能执行代码,大多数程序进入这个状态后会在这个状态上停留一会,时间到了之后会进入挂起状态(Suspended)。有的程序经过特殊的请求后可以长期处于Backgroud状态。

Suspended挂起:程序在后台不能执行代码,系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。

iOS的入口在main.m文件的main函数,根据UIApplicationMain函数,程序将进入AppDelegate.m,这个文件是xcode新建工程时自动生成的。AppDelegate.m文件,关乎着应用程序的生命周期。它有如下几个方法:

  1. application didFinishLaunchingWithOptions:当应用程序启动时执行,应用程序启动入口,只在应用程序启动时执行一次。若用户直接启动,lauchOptions内无数据,若通过其他方式启动应用,lauchOptions中包含对应方式的内容。

  2. applicationWillResignActive:在应用程序将要由活动状态切换到非活动状态时候要执行的委托调用,如按下 home 按钮,返回主屏幕,或全屏之间切换应用程序等。

  3. applicationDidEnterBackground:在应用程序已进入后台程序时,要执行的委托调用。

  4. applicationWillEnterForeground:在应用程序将要进入前台时(被激活)要执行的委托调用,刚好与applicationWillResignActive 方法相对应。

  5. applicationDidBecomeActive:在应用程序已被激活后要执行的委托调用,刚好与applicationDidEnterBackground 方法相对应。

  6. applicationWillTerminate:在应用程序要完全推出的时候要执行的委托调用,这个需要设置UIApplicationExitsOnSuspend的键值。

初次启动时执行一下步骤:

didFinishLaunchingWithOptions

applicationDidBecomeActive

按下home键:

applicationWillResignActive

applicationDidEnterBackground

点击程序图标进入:

applicationWillEnterForeground

applicationDidBecomeActive

当应用程序进入后台时,应该保存用户数据或状态信息,所有没写到磁盘的文件或信息,在进入后台时最后都要写到磁盘去,因为程序可能在后台被杀死。释放尽可能释放的内存。

- (void)applicationDidEnterBackground:(UIApplication *)application

该方法有大概5秒的时间让你完成这些任务,如果超过时间还有未完成的任务,你的程序就会被终止而且从内存中清除。如果还需要长时间的运行任务,可以在以下方法中调用。

[application beginBackgroundTaskWithExpirationHandler:^{ 
NSLog(@"begin Background Task With Expiration Handler"); 
}];

程序终止。

程序只要符合以下情况之一,只要进入后台或挂起状态就会终止:iOS4.0以前的系统;app是基于iOS4.0之前系统开发的;设备不支持多任务;在Info.plist文件中,程序包含了 UIApplicationExitsOnSuspend 键。

系统常常是为其他app启动时由于内存不足而回收内存最后需要终止应用程序,但有时也会是由于app很长时间响应而终止。如果app当时运行在后台并且没有暂停,系统会在应用程序终止之前调用app的代理的方法 - (void)applicationWillTerminate:(UIApplication *)application,这样可以让你可以做一些清理工作或保存一些数据或app的状态。

与其他动态语言比较

OC中方法的实现只能写在@implementation··@end中,对象方法的声明只能写在@interface···@end中间。对象方法都以-号开头,类方法都以+号开头。函数属于整个文件,可以写在文件中的任何位置,包括@interface··@end中,但写在@interface···@end会无法识别。

对象方法只能由对象来调用,类方法只能由类来调用,不能当做函数一样调用。对象方法归类对象所有,类方法调用不依赖于对象,类方法内部不能直接通过成员变量名访问对象的成员变量。OC只支持单继承,没有接口,但可以用delegate代替。

Objective-C与其他语言最大的区别是其运行时的动态性,它能让你在运行时为类添加方法或者去除方法以及使用反射,极大的方便了程序的扩展。

参考文献

  • 《松本行宏的程序世界》

  • 苹果开发文档