了解Objective-C语言的起源
Objective-C是C语言的超集,是面向对象的语言。Objetive-C是动态绑定的消息结构,在运行时才会检查对象类型。接受消息之后,执行的代码由运行期环境决定。
Objective-C是一种面向对象的语言,与C++和Java之类的语言类似。不同的是,Objective-C是Smalltalk演化而来的,使用的是消息结构(messaging structure)而不是函数调用(function calling)。
- 消息结构(Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
- 函数调用(C++)
Object *obj = new Object;
obj->perform(parameter1,parameter2);
使用函数调用的语言
以C++为例,运行时执行的代码由编译器决定。如果类中函数是多态的,编译器就会为该类生成一个虚函数表,然后自动将虚表指针添加为成员变量,根据虚表来执行函数。
使用消息结构的语言
以Objective-C为例,运行时所执行的代码不是编译器决定的,而是由runtime决定的,不管消息是否为多态,总是在运行时才会去查找执行的方法,编译器甚至不关心消息对象是何种类型。
Objective-C的重要工作都由"runtime component"来完成,所有的面向对象的特征和所需的全部数据结构和数据都在里面。runtime component本质上是一种与开发者所编写的代码相链接的动态库,将代码和程序粘合起来。这样的话,只要更新运行期组件就可以提升程序的性能。而在"编译器"完成的语言,要想获得类似的性能改变,就要重新编译代码。
声明一个对象时,保存的是一个存在栈(stack)的一个指向堆(heap)中对象的指针。
NSString *someString = @"The string";
NSString *anotherString = someString;
可以看出,只创建了一个NString对象和两个指向该对象的指针,说明当前栈帧(stack frame)里分配了两块内存,每块能容纳下一个指针(32位机上是4个字节,64位上是8个字节),每块中存储的都是NSString的真实地址。
分配在栈中用于保存对象的内存会在栈帧被弹出时自动清理,而分配在堆中的内存必须直接管理。
Objective-C将堆的内存管理抽象出来了,不需要malloc和free来分配或者释放对象所占的内,而是使用引用计数来管理。
在Objective-C中,有时会喷到定义不含*的变量,这些是直接使用"栈空间"来保存的变量,不是对象,比如CoreGraphies框架中的CGRect
CGRect frame;
frame.size.width = 100.0f;
CGRect是结构体,只保存基本类型的变量,那么如果使用Objective-C对象来做的话,每次还需要分配和释放堆内存,会造成额外的开销,因此如果只保存int,float等"非对象类型",使用结构体就
struct CGRect{
CGPoint origin;
CGSize size;
};
在类的头文件中尽量不要引入其他头文件
除非确有必要,不要引入头文件。尽量使用向前声明来提及别的类,然后在实现文件中引入那些类的头文件。这样做可以减低类之间的耦合。
协议单独成类,并尽量将协议写在类扩展中,而不是公共头文件中。
在一个类A中,如果要使用类B,在类A的头文件中应该尽量使用向前声明来告知编译器需要引用这类。因为编译这个文件的时候,并不需要知道引入的文件的所有细节。在类A的实现文件中,在来引入类B。
也就是说,如果要在类A中使用类B,那么在头文件A.h中只需要
@class B;
在类A的实现文件A.m中,若要使用B,必须知道B的所有借口细节,所以
#import B.h
将引入头文件的时机尽量延后,只有在确有需要的时候引入,这样可以
- 减少类使用者引入的头文件的数量,减少编译的时间。
- 解决两个类互相引用的问题
假设有个Employer类和Employee类,它们互为各自的成员变量,那么要编译Employer则必须知道Employee,反之亦然。如果在各自头文件中引入对方的文件,则会导致"循环引用"。当解析其中一个头文件的时候,会引入另一个头文件,而另一个头文件又要解析之前的头文件。使用#import而不是#include虽然不会导致死循环,但是却会导致其中一个类无法被正常编译。
但是有时候也必须在头文件中引入其他头文件
- 如果你的类继承自某个超类,则必须引入超类头文件
- 如果要声明你写的类遵从某个协议(protocol),则该协议也必须引入
继承自Shape并实现Drawable协议的Rectangle类的头文件
//Ractangle.h
#import "Shape.h"
#import "Drawable.h"
@interface Rectangle:Shape
...
@end
需要注意的是,协议最好单独成类,不要和其他类混在一起,例如在类C的文件中,声明了协议a,当类D要使用协议a时,就要引入类C,则又编译时间增加,且有可能造成相互引用,所以协议最好单独成类。
还有一种协议,也就是"委托协议(delegate protocol)",就可以不用单独写一个头文件,因为委托协议只有与被委托的类在一起才有意义,可以将两者写在一起。委托协议可以写在类扩展(class-continuation)中,不需要在公共头文件中导入。
具体来说,每次引入其他文件时,可以如下操作
- 如果可以用向前声明,就使用向前声明
- 协议单独成类
- 协议最好写在类扩展中
多用字面量语法,少用与之等价的方法
应该使用字面量语法来创建字符串,数值,数组,字典。与创建此类的常规方法相比,这么做更加简明扼要
应该通过取下标操作来访问数组下标或者是字典中的键所对应的元素
用字面量语法来创建数组或者字典的时候,若值中有nil,或抛出异常,因此要确保值中不含nil
字面数值
有时候需要将整数,浮点数,布尔值等基本类型封装成类的时候,可以使用NSNumber。
- 常规方式
NSNumber *someNumber = [NSNumber numberWithInt:1];
- 字面量语法
NSNumber *someNumber = @1;
不仅语法更加精简,而且能以NSNumber实例表示的所有数据类型都可以使用该语法
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.141592;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
字面量数组
- 常规:数组结尾处要加上nil
NSArray *animals = [NSArray arrayWithObject:@"cat",@"dog",@"mouse",@"badger",nil];
- 字面量:数组结尾处不需要加上nil
NSArray *animal = @[@"cat",@"dog",@"mouse",@"badger"];
不仅更加简单,而且还可以使用下标来操作
字面量语法,实际上是一种语法糖
字典字面量
- 常规
NSDictionary *personData = [NSDictionaryWithObjectsAndKeys:
@"Matt",@"fistName",
@"Galloway",@"lastName",
[NSNumber numberWithInt:28],@"age",
nil];
- 字面量写法
NSDictionary *personData =
@{@"fisrtName":@"Matt",
@"lastName":@"Galloway"
@"age":@28};
这样写更简洁,而且键出现在对象之前,更易读。
可变数组与字典
通过取下标操作可以直接访问某个下标或者字典的对象,如果容器是可变的,还可以直接修改
- 常规做法
[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"Galloway" forKey:@"lastName"];
- 字面量
mutableArray[1] = @"dog";
mutableDictionary[@"lastName"]= @"Galloway";
多用类型常量,少用#define预处理命令
不要用预处理指令定义常量
在实现文件中使用"static const"来定义只在编译单元内可见的常量
在头文件中使用extern来声明全局常量
假如你现在需要设置某个动画时间,也许会这样做
#define ANIMATION_DURATION 0.3
这样定义出的来的常量
- 没有类型信息,看不出具体是指什么
- 在所有引入了这个头文件的类中,ANIMATION_DURATION都将被替换成0.3
解决方法:可以是使用类型常量
static const NSTimeInterval kAnimationDuration = 0.3;
该方式定义包括了常量类型,能清晰描述常量的意义
这里kAnimationDuration是一个类变量,这个定义可以写在类的实现文件的#import和实现函数之间
//AnimatedView.m
#import "Animated.h"
static const NSTimeInterval kAnimationDuration = 0.3;
@implement AnimatedView
...
@end
- 使用const,表示常量,如果在实现文件中企图修改,会不成功
- 使用static意味着该变量仅在定义此变量编译单元可见。如果不加static,编译器会为它创建一个"外部符号",如果其他编译单元中出现了同名变量,则抛出错误信息。
事实上,同时声明为static和const之后,效果相当于在该编译单元进行了宏替换。
有时候,需要公开某个常量,比方说,你可能使用NSNotificationCenter来通知别人。这个变量可以声明为外界可见的常值变量。
此类常量放需放在"全局符号表"中,以便定义该常量编译单元之外的单元使用,定义方式如下
//在头文件中
extern NSString *const StringConstant;
//在实现文件中
NSString *const StringConstant = @"value";
这个常量需要在头文件中声明,在实现文件中定义。定义在头文件中是为了告诉编译器,这边有一个全局变量,请注册到全局变量表中,而编译器并不关心变量的定义。
此变量必须定义,并且只能定义一次
用枚举来表示状态、选项、状态码
应该用枚举来表示状态机的状态
如果状态是可以多选的,可以使用2的幂来表示枚举,以便使用逻辑操作
使用NS_ENUM和NS_OPTIONS来代替enum
枚举是一种常量命名的方式,以套接字连接状态为例
enum ConnectionState
{
DisConnected,
Connecting,
Connected,};
typedof enum ConnetionState ConnectionState; //这样下次用枚举的时候就不用加enum
枚举默认是使用NSInteger从0开始标识,也可以自行改变标识方式
enum ConnectionState
{
DisConnected=1, //从1开始
Connecting, //2
Connected, //3};
typedof enum ConnetionState ConnectionState; //这样下次用枚举的时候就不用加enum
当定义选项的时候,如果可以彼此组合,只要枚举得当,就可以通过"按位或操作符"来组合
如果iOS UI框架中有如下枚举
enum UIViewAutoresizing{
UIVIewAutosizingNone = 0,
UIViewAutosizingFlexibleLeftMargin = 1 <<0,
UIViewAutosizingFlexibleWidth = 1 <<1,
UIViewAutosizingFlexibleRightMargin = 1 <<2,
UIViewAutosizingFlexibleTopMargin = 1 <<3,
UIViewAutosizingFlexibleHeight = 1 <<4,
UIViewAutosizingFlexibleBottomMargin = 1<<5,
}
每个选项都可以启用或禁用
Foundation框架定义了一些辅助宏,日常中要避免使用C风格的enum尽量使用NS_ENUM和NS_OPTION
用法如下
typedef NS_ENUM(NSUInteger,ConnectionState)
{
Disconnected,
Connecting,
Connected,};
typedef NS_OPTION(NSUInteger,PermiitedDirection)
{
Up =1<<0,
Down =1<<1,
Left =1<<2,
Right =1<<3,};