1、了解OC
OC使用消息结构(messaging structure)而不是函数调用(function calling)。OC由Smalltalk演化而来。
使用消息结构的语言,其运行时所执行的代码由运行环境来决定;而使用函数调用的语言,由编译器决定。
采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,其过程叫做“动态绑定(dynamic binding)”
OC的重要工作由运行期组件(runtime component)而非编译器来完成。使用OC的面向对象特性所需的全部数据结构及函数都在运行期组件里面。
运行期组件,本质上就是一种与开发者所编写代码相连接的动态库(dynamic library),其代码能把所有程序粘合起来。只要更新运行期组件,即可提升应用程序性能。
同时掌握C与OC两门语言的核心概念,才能写出高效的OC代码来。尤为重要的是C语言的内存模型(memory model),有助于理解OC的内存模型及其“引用计数”机制的工作原理。
OC语言中的指针是用来指示对象的。对象所在内存总是分配在堆空间(heap space)中,不会分配在栈(stack)上。
OC是C的超集(superset)。使用动态绑定的消息结构,在运行时才会检查对象类型。接收消息之后,究竟应执行何种代码,由运行环境而非编译器来决定。
2、在类的头文件中尽量少引入其他头文件
头文件 header file
实现文件 implementation file
向前声明 forward declaring
在头文件中,通过@class 类名
的方式,告诉编译器,有一个类名。
但是在实现文件中,如果需要知道所有的接口细节,则需要#import "xxx.h"
。
优点:
将头文件引入的时机尽量延后,只在确有需要时才引入,减少类的使用者所需引入的头文件数量。减少编译时间。
解决两个类互相引用的问题。在各自的头文件里引入对方头文件,会导致循环引用(chicken-and-egg situation)。
不能使用向前声明,只能引入头文件的情况
- 1、如果写的类继承自某个超类,则必须引入定义那个超类的头文件。
- 2、声明你写的类遵从某个协议(protocol),那么该协议必须有完整定义,不能使用向前声明。向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。
第二种情况,解决方式:
- 1、把协议单独放在啊一个头文件中,这样,会产生互相依赖问题,增加编译时间。
- 2、委托协议(delegate protocol)不用单独写一个头文件,只有与接收协议委托的类放在一起才有意义。此时最好能在实现文件中声明此类实现了该委托协议,并把实现代码放在class-continuation 分类里
每次在头文件中引入其他头文件之前,都要先问问自己这样做是否确有必要。如果可以使用向前声明取代引入,那么就不要引入。如果因为要实现属性、实例变量或者要遵循协议而必须引入头文件,则尽量将其移至“class-continuation分类”中。不仅可以缩减编译时间,还能降低彼此依赖程度。
3、多用字面量语法(语法糖),少用与之等价的方法
使用字面量语法(literal syntax)可以缩减源代码长度,使其更为易读。
字面数值
将整型、浮点数、布尔值封入OC对象。
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.4f;
NSNumber *doubleNumber = @3.242592;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
int x = 2;
int y = 2.2;
NSNumber *number = @(x * y);
字面量数组
字面量语法床架数组时要注意,数组元素对象有nil,会抛出异常。
NSArray *array = @[@"Dog",@"Tiger",@"Cat",@"Panda"];
NSString *item = array[0];//取下标
-
arrayWithObjects:
与字面量数组的区别
arrayWithObjects:
方法会在遇到第一个为nil的对象时,结束添加。
但是字面量创建数组不同,遇到nil 就会报异常。
字面量字典
NSDictionary *dict = @{@"1":@"one",
@"2":@"two",
@"3":@"three",
};
NSString *one = dict[@"1"];
字典中的键和对象必须是OC对象。字面量字典中有nil就会抛出异常。
可变数组与字典
通过下标可以访问数组中的某个下标或者字典中某个键对应的元素。如果数组和字典是可变的,也可以通过下标修改其中的值。
mutableArray[0] = @100;
mutableDict[@"2"] = @"222";
array[0] = @100;
局限性
字面量语法,除了字符串意外,所创建出来的对象必须属于Foundation框架才行。如果自定义这些类的子类,无法用字面量语法创建其对象。
使用字面量语法创建出来的字符串、数组、字典对象都是不可变的。
4、多用类型常量,少用#define预处理指令
static const NSTimeInterval kAnimationDuration = 0.4;
这种方式定义的常量,包含类型信息,清楚地描述了常量的含义。
常量的名称:若常量局限于某编译单元(translation unit,也就是实现文件)之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。
定义常量的位置很重要,同时也要注意常量的命名规则。
私有常量
只在编译单元内可见的常量(translation-unit-specific constant)。
若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。变量一定要同时使用static和const来声明。如果试图修改const修饰符所声明的变量,那么编译器就会报错。static修饰意味着该变量仅在定义此变量的编译单元中可见。
编译器每收到一个编译单元,就会输出一份目标文件(object file)。如果声明常量不加static,编译器会为它创建一个外部符号(external symbol),此时若另外一个编译单元也生命了同名变量,就会出现错误信息:
duplicate symbol _kAnimationDuration in:
xxxx/x86_64/ViewController.o
xxxx/x86_64/ZYDErrors.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
如果一个变量既声明为static
,又声明为const
,编译器根本不会创建符号,而是像#define
预处理指令一样,把所有的变量都替换为常值。不过:用这种方式定义的常量带有类型信息。
对外可见的常值变量(constant variable)
此类常量需放在全局符号表(global symbol table)中,以便可以在定义该常量的编译单元之外使用。
需要在头文件中声明:
extern NSString *const DogObjectNotificationString;
extern
关键字,告诉编译器,在全局符号表中会有一个指定的符号。
常量定义从右至左解读,所以“DogObjectNotificationString
是一个常量,这个常量是一个指针,指向NSString
对象”
在实现文件中定义:
NSString *const DogObjectNotificationString = @"DogObjectNotificationString";
此类定义必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件中。
由实现文件生成目标文件时,编译器会在数据段为字符串分配存储空间。链接器会把此目标文件与其它目标文件相链接,以生成最终的二进制文件。凡是用到该全局符号的地方,链接器都能将其解析。
因为符号要放在全局符号表里,所以命名一定要注意。最好是根据命名规范加前缀同时加类名前缀。
5、用枚举表示状态、选项、状态码
枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)
每个状态都用一个便于理解的值来表示,所以这样写出来的代码更容易读懂。
简单枚举 一
编译器会为枚举分配一个独有的编号,从0还是,每个枚举递增1。
enum DogObjectTest {
DogObjectTestFirst,
DogObjectTestSecond,
};
此类型枚举,声明一个枚举变量:
enum DogObjectTest one = DogObjectTestFirst;
声明变量不够简洁。
简单枚举 二
用typedef关键字重新定义枚举类型。
enum DogObjectTest {
DogObjectTestFirst,
DogObjectTestSecond,
};
typedef enum DogObjectTest DogObjectTest;
声明枚举标量,可以不用enum直接使用枚举类型名。
DogObjectTest two = DogObjectTestSecond;
指定底层数据类型枚举
可以向前声明枚举变量。
若不指底层数据类型,无法向前声明枚举类型,因为编译器不知道底层数据类型的大小,所以在用到次枚举类型的时候,也就不知道究竟该给变量分配多少空间。
enum DogObjectStatus:NSUInteger {
DogObjectStatusOne,
DogObjectStatusTwo,
};
指定底层数据类型是NSInteger。
- 向前声明式指定底层数据类型
enum DogObjectStatus: NSUInteger;
指定类型并且简化声明
typedef enum : NSUInteger {
DogObjectPeriodLow,
DogObjectPeriodMiddle,
DogObjectPeriodHeight,
} DogObjectPeriod;
声明变量
DogObjectPeriod period = DogObjectPeriodLow;
定义选项
若选项可以彼此组合,更应如此。只要枚举定义的对,各选项之间可以通过按位或操作符(bitwise OR operator)来组合。如iOS UI框架有如下枚举类型,用来表示某个视图应该在水平或垂直方向上调整大小。
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
每个选项均可启用或禁用。每个枚举值所对应的二进制表中,只有一个二进制位是1。用按位或操作符可组合多个选项如UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth
。
可以用按位与操作符(bitwise AND operator)即可判断出是否已启用某个选项。
状态码
如果用switch作状态码判断,不应使用default分支,那么如果后面添加新的状态值,就会提示Enumeration value 'DogObjectPeriodOther' not handled in switch
不会直接跳到default
分支,导致分支情况处理不完整的情况。