本文是阅读Jesse Feiler的《Sams Teach Yourself Objective-C in 24 Hours》的笔记。
iOS层次划分
- 应用程序
- Cocoa Touch
- 媒体
- 核心服务
- OS核心
重点两个框架
Mac OS X
- 应用程序框架中的AppKit
- 核心服务中的Core Foundation
iOS
- Cocoa Touch中的UIKit
- 核心服务中的Foundation
Objective-C继承
- 调用——基类声明init,当子类上调用init时,实际调用的是基类的init方法
- 覆写——覆盖改写,在子类中也同样声明了init方法,那么当在子类中调用init方法的时候实际上是调用的子类的init方法
覆写并调用——在子类中覆写基类的方法时总是可以调用[super init],这种方法其实在子类的的覆写当中经常见到
协议——这是一组可以被该类采纳的方法。采纳协议的类需要实线协议中的方法,一旦一个类采纳该协议定义的方法后就可以调用任何协议中的方法了(协议可以定义为Optional,这样调用时就可以确保只调用那些必须被实现的方法或者被实线的方法)。协议结构在某种程度上实线了多继承的部分特性,与java中的接口类似。协议中的方法可以出现在几个毫不相干的类中,从而扩展了通过继承链来共享方法的思想。
- 分类——即Category,协议是可以被任何类所采纳,而分类则由一组被添加到某一类的方法构成。使用分类可以向一个无源码的既有类中添加方法,其实就是拓展一个既有类的最有效方法,区别于继承,只能添加方法,不能加入成员变量。
- 扩展——扩展有时候被成为匿名分类,它是在类的实线文件中进行声明和实线。扩展可以向一个类添加方法和属性,还可以重定义属性(比如常见的将readonly变为readwrite)。
- 为什么在Objective-C中分类、扩展和协议是重要的?
它们允许以不创建子类的方式复用代码。这意味着与其他只能通过子类化来共享代码的语言相比,OC中,类层次结构可能更加扁平。- 为什么封装是重要的?
封装和数据抽象意味着对象是自包含的。类对外部世界暴露的属性和方法是类的所有实例共同拥有的属性和方法,这使得维护变得更加容易。
声明方法
对象(实例)方法
在方法前面加-表示这个方法声明的类的实例都可以调用这个方法。
类方法
在方法前面加+表示这个方法调用只能是类本身。类方法最常见的用途是作为工厂方法使用。可能最常见的类方法就是alloc了。通过1
+(id)alloc;
来返回类的一个实例。
强弱类型
其实这里所说的强弱类型就是对象的层次关系,弱类型就是父类,强类型就是子类,那NSArray就是NSMutableArray的弱类型,而弱类型是可以指向强类型的,这就是C语言中的父类指针指向子类对象,所以最弱的类型就是id
良好的编码习惯是将某一类型的方法放在一起,比如将所有的初始化器放在一起,然后加上#pragma mark - 标记
#include和#import之间有什么区别?
对于#include在将指定文件包含进来的时候不会判断该文件是否在之前已经被包含过了。这样当一个文件被包含两次时会造成重复声明。#import只引用文件一次,并且会忽略后面对同一文件的#import指令。
创建方法
Apple在命名规则上给出的指南是类名应该以两个或者三个大写字母作为前缀,这个前缀通常用于说明其所属的框架。如果类不属于任何框架,那么可以用这个前缀说明类所属的项目。比如多瑞手写记事本的前缀为DRD DuoRuiDiary的简称。
前向引用
尽管引入文件是一种修正错误的方式,但这并不是解决前向引用未声明的类这个问题的最优雅的方式。相反,可以在类声明中使用@class指令在解决前向引用的问题。此外,还可能经常需要引用未声明的协议,OC提供了一个类似的语法解决了这个问题:1
@protocol MyProtocol;
可写性
- readwrite(默认值)——@synthesize会同时生成一个getter和一个setter。
- readonly——@synthesize只会生成一个getter。所有使用点语法设置值的代码都不会通过编译。
Setter语义
- assign(默认值)——就是传统意义的赋值操作。它仅仅将一个值赋给属性,并不会修改引用计数。
- retain——这会在属性的新值上调用retain,接着在属性的上一个值上调用release,从而会给引用计数减1。
- copy——这会复制被设置的对象,然后在前一个值上调用release。这意味着属性值对象在后面被修改或被释放了也不会影响属性值,因为已经将属性值设置成了该对象的一个副本。
原子性
这个特性是用来管理线程行为的,值为nonatomic,如果没有特别指定该特性,编译器会采用atomic这个值,atomic特性能够在多线程环境下安全的使用属性,不过会影响效率。实现属性
- @synthesize——最常用最强大的变体之一是将属性值设置为所属类中的一个实例值。即便现在Xcode可能在看到@synthesize的时候自动采用同名实例变更声明,但是使用与属性不同名的支撑变量(如以下划线打头)是一项较好的做法。
1
window = _window;
- @dynamic——这个指令通常与NSManagedObject一起使用,它表明除@synthesize指令提供的信息之外的其他信息以及getter和setter是跟数据库数据动态生成的。
- 使用声明属性的优势是什么?
创建访问器之后就可以用点语法来使用访问器,这样就能提升对实例变量的封装性。此外除了可以指定属性的类型之外,还可以通过属性的特性来指定属性的更多信息,如可以控制属性的可写性以及内存管理方面的信息。 - 在声明属性中使用点语法的优势是什么?
输入的内容变少了,同时也增强了语言的可读性。
- 使用声明属性的优势是什么?
接口方法
不要隐藏方法任务,除非这些任务已经很明显地属于主任务的一部分了,最好是将执行多个任务的方法分解成几个相互协作的方法,即可以将前一个方法的返回值传入下一个方法。
一个过程最简单以及最佳的实线方式,是那些其工作原理能够为维护这些过程的新开发人员所理解的实现方式。
- 在方法中声明双重间接参数的用途是什么?
对一个典型的参数来讲,通常会传入一个指向实例的指针,接着就可以在方法代码中修改这个实例了(通常init方法会这么做)。而有了像NSError**这样的双重间接之后,通常就会传入一个nil值,然后返回一个新对象(有时会传入一个对象,而返回另一个对象)。- 为什么在方法参数的末尾已经很少使用省略号了?
当所有参数都有名字时代码会变得更加清晰,可读性更好。如果需要灵活的指定参数的数量,那么可以考虑使用一个集合类,如NSArray、NSSet或NSDictionary。
范式和设计模式
范式是一个单词,在很多领域有着很长的使用历史。设计模式是在20世纪70年代才被提出来并且在持续优化的一个概念,从一个更高层次上来讲,范式和设计模式都是规则, 它们并不会给语言或者框架添加任何约束,而只是限制框架和语言的使用方式。
- 动作和方法之间的差别是什么?
动作的语法是常规的方法语法的一个子集,这使得Xcode很容易就能够实现创建动作自动化工具,此外还使得在运行时分发各种动作变得更加容易。- 为何iOS动作能够同时拥有一个sender和一个event?
在Mac OS X中,控件上的用户事件主要是鼠标点击。而在Cocoa Touch框架中则支持各种点击和手势动作。通过event参数能够获知更多有关动作触发方式方面的信息。
选择器路由消息
1 | myVariable = [myObject init]; |
上面写了一行代码来解释OC中’消息’这一概念。
- 接收者(Receiver)是接收消息的对象(在本例中是myObject)。
- 选择器(Selector)是发送给接收者的命令(在本例中是init)。
在运行时选择器用来识别对象中将被执行的特定方法。选择器和接收者的方法之间的差别在于选择器在运行时选择需执行的方法,这为OC提供了很多灵活性和动态性。这种特性是通过OC运行时实现的。静态类型语言中编写调用函数的代码是编译器能够检查并确保执行该函数的对象确实拥有该函数,但对于动态消息环境来讲,消息(具体来说是选择器)被发送给了接收者,因此很有可能会发生接收者无法执行该方法的情况。
在很多情况下(特别是当接收者的类型被声明成id时,即任意对象时)是无法在编译时执行这种检查的。在最简单的情况下,要么编译时对参数进行一次错误检查,要么在运行时每次发送消息时对参数进行错误检查。硬件速度的提升以及在优化根据选择器分发方法方面数十年来积累下来的经验使得OC的消息模型至少在Mac OS X和iOS环境上已经胜出了。
使用SEL和@selector()
面向对象程序关注内存管理的同时还需要关注的一点是性能。影响性能最关键的因素之一在于方法的选择和分发。OC语言及编译器的开发人员已经在这一方面投入了巨大的精力。程序的方法名是存储在一张表中,程序在执行的过程中可以查询这张表。每个方法在表中都有唯一的标识符与之对应,而在分发时则需要用到这个唯一的标识符。这种做法避免了在查找方法(方法名可能具备很强的描述性)是需要处理长字符串。
在编写代码的时候可以通过间接的方式房粉这些标识符,并且能借此优化自己的代码,同时给代码增加额外的动态性。为了达到这一目标,OC创建了一种特殊类型,即SEL。可以像int或float来定义标识符类型那样使用。
编译选择器与计算选择器
像编写1
myVariable = [myObject init];
需要指定接收者和选择器。与其他代码一样,编译器会生成可执行的代码。这里的选择器是指编译选择器。
另一方面,可以使用一个在运行是根据一个程序自动生成的字符串计算得出的选择器。这种选择器称为计算选择器。
使用@selector来创建选择器
1 | myVariable = [myObject myAction]; |
可以使用下面的方法来生成一个编译选择器。1
2SEL mySelector;
mySelector = @selector(myAction);
如果所调用的方法包含参数,那么需要提供它们的关键词(不是值)。1
mySelector = @selector(myAction: forPurpose:);
如果有两个参数,那么就需要同时提供两个关键词。1
mySelector = @selector(myAction: forPurpose: withCondition:);
根据字符串来创建选择器
有时候可能并不想硬编码选择器,相反,需要能够在运行时根据一个字符串变量来设置选择器。(这就是计算选择器)通过一个实用函数来完成的。1
2
3NSString * myString;
SEL mySelector:
mySelector = NSSelectorFormString(myString)
如果需要存储一个选择器字符串以备后面的复用之需,那么可以将选择器转换为字符串。1
myString = NSStringFromSelector(myACtion: forPurpose);
使用选择器
1 | - (id)performSelector:(SEL)aSelector; |
消息中的选择器需要使用performSelector,如下所示:
1 | [receiver performSelector:mySelector]; |
- 如何在使用performSelector和使用NSInvocation之间做出选择?
performSelector用于零个、一个或两个参数,并且参数的类型必须是id。- 什么是运行时?
它是实现OC语言的运行时动态特征的数据结构和函数。它不是可选的。- 如何确保发送的一条消息是有效的?
将消息打包进一个选择器并使用respondsToSelector在接收者上对该消息进行检测。- NSInvocation会将一条OC消息转换成什么?
一个对象。
Foundation类
Foundation框架中包含很多类,应用程序可以使用这些类并且创建这些类的子类。在框架中有两个根类和其他各种各样的类,这些类被分成了几个组。
根类
之前NSObject类被认为是所有其他类的根类。实际上,它仅仅是两个根类中的一个,另外一个叫NSProxy,它通常与分布式对象一起使用。这个类表示一个远程对象(该对象可能还没有被创建)。
NSObject协议中包含的方法以前是NSObject类本身的一部分。通过将其中一些方法抽象到NSObject协议中之后所有的跟对象(目前就这两个)就能够共享这个协议的功能了。
Foundation范式与策略
Foundation框架的基础包括了对其他框架和读者自己编写的代码所实现的策略和范式的实现。这些范式和策略包括4个方面。
- 可变性
- 类簇
- 通知
- 垃圾收集
下面分别讲解这四个方面。可变性
很多Foundation类都拥有可变和不可变的版本,如NSArray、NSSet、NSString和NSData。一般来讲,可变的版本是不可变版本的子类,通过其名称来识别。例如NSMutableArray是NSArray的一个可变版本。这个名称上的可识别性使得编译器和运行时对可变版本进行性能优化变为可能。
如果将能够修改可变类的方法限制在一到两个方法之内的话(通常这是一种优秀的设计)就能够利用相应的不可变类在性能方面的优势了。例如,一般情况下可以使用一个不可变的数组或其他类,当需要修改数组的时候可以使用mutableCopy来创建一个可变的副本,写着修改该副本,最后使用copy将可变的副本转换为不可变的对象(这是copy方法的默认行为)。接着就可以继续使用这个不可变的副本了。
这种在可变和不可变的对象之间来回切换的做法是否值得依赖于所需修改的数据量和复制对象所需花费的时间。类簇
Foundation利用了类簇在组织隐藏在抽象超类中的子类,开发人员通常会与这些子类打交道。见得最多的类簇应该就是NSNumber了。隐藏在抽象类NSNumber中的子类用于表示整数、长整数、字符等。这是一种在实现多个子类的同时不暴露出这些子类相互之间的独立性的极佳方式。通知
通知提供了一种广播形式的通信。观察者进行注册以监控特定的事件,应用中的对象会发布通知。各个进程中的通知中心会处理这一过程。
通知是一种非常高效的底层通信形式,它用于在应用中传递信息。通知是NSNotification的子类,它有3个基础值,读者在需要的时候可以添加其他值。基础的NSNotification结构既简单有非常高效。每个通知都包括3个值。 - Name。在应用发出的通知中这个名称应该是唯一的。观察者需要知道这个名称。这样才能够注册接收这种类型的通知。名称是一个NSString。
- Object。一般来讲,这是发出通知的对象,但不是必需的。它甚至可以是nil,其实很多时候都是nil,类型是id。
- userInfo。这是一个字典,包含与该通知相关的数据。开发人员需要负责定义该字典并加入传递给观察者的数据。它的类型是NSDictionary。
1.发布通知1
2+ (id)notificationWithName:(NSString *)aName object:(id)anObject;
+ (id)notificationWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)userInfo;
通常开发者在使用NSNotificationCenter中的方法通过一个步骤就可以创建和发布通知。1
2
3- (void)postNotification:(NSNotification *)notification;
- (void)postNotification:(NSString *)notificationName object:(id)notificationSender;
- (void)postNotification:(NSString *)notificationName object:(id)notificationSender userInfo:(NSDictionary *)useInfo;
2.注册接收通知1
- (void)addObserver:(id)notificationObserver selector:(SEL)notificationSelector name:(NSString *)notificationName object:(id)notificationSender;
有时候可以不用选择器,直接指定一个队列并在队列中添加一个块。块接收一个参数,即通知本身。1
- (id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *)block;
3.删除观察者1
2- (void)removeObserver:(id)notificationObserver;
- (void)removeObserver:(id)notificationObserver name:(NSString *)notificationName object:(id)notificationSender;
- 通知所实现的目标是什么?
使用通知可以通过分发中心将消息传递给观察者,即向分发中心发布消息,然后分发中心会将消息传递给观察者。使用通知之后无须在消息发布者和观察者之间建立直接的通信连接。- 类簇的目的是什么?
类簇是一组共享同一抽象超类的相关类。开发人员使用抽象超类的方法,但实际上操作的是隐藏在抽象超类中的子类。抽象类的initWith…方法通常会负责创建具体的子类。- 一个应用有多少个通知中心?
一个- 引入可变和不可变类的目的是什么?
可以为不会发生变更的对象生成优化过的高效代码。
在OC当中,最好是使用OC类,而不是使用原生的C类型。比如下面这样。1
2不要这样 - (float) convertCurrency:(float)units withRate:(float)exchangeRage;
要这样 - (NSNumber *) convertCurrency:(NSNumber *)units withRate:(NSNumber *)exchangeRage;
为什么思考和反思方法头中的关键词和方法名是重要的?
因为这两者能够很清晰的表明方法的功能以及容易让人理解。其实重点是让代码更容易读懂,方法开发者之间的交流,就像我们要练习如何把话说明白一样。
管理内存和运行时对象
ARC不会给应用的运行时增加负担,因为它是在编辑器和优化器这一层实现的,依赖与Cocoa的命名规则来生成代码,其实就是代替了之前需要开发人员手动管理内存的工作。
- 在ARC环境中Cocoa编码规则为何是重要的?
方法名中的第一个动词(alloc、init、new或copy)会触发ARC保持一个对象。- 垃圾收集和引用计数之间的差别是什么?
垃圾收集是运行时运行的一个过程。引用计数是在编写代码和编译代码的时候实现的,但其作用需要在运行时才能看得出来。
协议
协议中无任何前置指令的类也是必须存在的类,之所以这样做的原因因为在协议中加入可选方法@optional之前,协议中的所有方法都是必须存在的@required。为了避免破坏既有代码,编译器会默认按照这种方式来处理,但最好是在鞋一种指定哪些方法是可选的,哪些方法是必须的。
- 如何使用协议向类添加属性和变量?
无法直接完成这个任务,但是可以在协议中指定访问器。这样采用该协议的类就会实现这些访问器,而这些访问器则可以访问采用该协议的变量和属性。- 什么是委托,它是如何跟协议相关联的?
很多Cocoa类都包含委托。委托一般由一个类来实线并会将其赋给这个类的delegate属性。实现委托的类必须要能够响应发送给委托的消息,那些消息通常是在委托的协议中指定的。- 如果一个协议方法没有被标记成必需的或可选的,如何解释?
默认是必须的。- 一个类通常拥有几个委托?
不超过一个。- 一个类能够采用几个协议?
没有限制。
快速枚举
简单的说快速枚举提供了一个枚举循环,这样就无须编写控制结构了,代码会变得更加清晰,避免一些潜在的错误,如果在快速枚举过程中底层结合发生变更的话会抛出一个异常,这样开发人员就可以处理异常了。
使用块
块利用了20世纪最基本的一个计算机原理:计算机存储和操作数字数据。这个关键的原理是说数字数据标识数字或文本的难易程度与它标识计算机指令的难易程度是一样的。所有这些都是数字,管理这些数字的方法基本上是一致的。
块与回调函数在某些方面是类似的,在C和OC以及其他程序设计语言中。回调函数已经使用了很长时间了。回调函数通常用于实时处理,因此可以在Core Audio以及类似的Cocoa框架中找到使用回调函数的地方。一个回调函数通常会作为参数传递到一段代码中用来管理可能发生(以及经常重复发生)的事件。
回调函数要比周期性检查过程是否结束并在结束时执行结束例程的做法更加优雅和高效。
块在OC中的实现与毁掉函数相比存在3个重要的区别。
- OC中块是运行时对象。
- 块能够访问其被定义时所属的作用域中的数据。这意味着当一个块被调用时,它能够访问其被定义时所属的作用域中的可用数据,即使这些数据已经不能再用了,当然对这些数据的访问是只读的。
- 块声明的语法使用了一个特殊字符(^),这种特殊字符使得块语法的可读性更强。
声明一个块1
float(^myBlock)(int, float);
如果需要多次使用同一个块的话最好使用typedef,这样将块声明放在一个地方,并且可以让几个块使用同样的定义。
1 | typedef float(^myBlock)(int, float); |
要创建一个变量的话可以像平时使用typedef那样声明并提供块的代码。
1 | MyBlockType myBlock = ^(int myInt, float myDivisor) { |
块的用法和函数的用法是一样的。1
float myFloat = myBlock(17, 2.0);
- 最好使用哪种方式来声明块?
使用typedef,虽然用内联的方式声明块会更加简洁一点。- 一个块能够包含多少代码?
没有必须要遵循的规则,但是大部分块都是非常短小的。- 如果在一个块中使用了_block变量,那么当控制离开了变量所声明的作用域时会发生什么情况?
在在块中仍然是有效的直至控制离开块为止。