一、了解Object-C语言的起源-总结
1、oc使用“消息结构”(messaging structure)而非“函数调用”(function calling);
示例代码:
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2]; //给对象发送消息performWith:and:
Object *obj = new Object;
obj->perform(parameter1, parameter2); //函数调用,perform函数包含两个参数
2、消息和函数调用的区别:消息结构执行的代码由运行环境决定,函数调用由编译器决定;
3、oc运行时才会查找对应的执行方法,过程叫做“动态绑定”,对比函数调用,如果函数是多态的,则也会通过查找对应的“方法表”来找出具体的实现,而oc则始终在运行时才会根据方法名查找具体的映射实现;
4、oc工作核心是“运行期组件”而非编译器,主要体现在oc所需的全部数据结构和函数都在运行期组件里面,比如包含了全部的内存管理方法,运行期组件本质是一个“动态库”与开发者编写的代码相链接,可以把开发者编写的所有程序粘合起来;
5、性能提升方面:只需要更新运行期组件,即可提升应用程序性能,编译期语言则需要重新编译代码;
6、oc对象内存总是分配在“堆空间”中,不能在栈中分配oc对象;
示例代码:
NSString *someString = @"The string"; //声明了一个NSString *类型的oc变量,指向NSString的指针
NSString stackString; //报错error:interface type cannot be statically allocated
7、指针存放在“栈帧”分配的内存里,指向分配在堆中的对象实例,分配在堆中的内存必须直接管理,而分配在栈上的变量内存在栈帧弹出时自动清理;
示例代码:
NSString *someString = @“The string”;//变量someString指向堆中的某块内存,当前栈帧分配了someString变量的内存地址
NSString *anotherString = someString; //anotherString指向了堆中同一个某块内存,当前栈帧分配了anothString变量的内存地址
8、oc将堆内存管理抽象出来,主要体现为在运行期环境把这部分工作抽象为一套内存管理架构,名叫“引用计数”;
9、oc定义不含*的变量,使用栈空间(stack space),保存的不是oc对象,比如oc中如果只需保存“int、char、float、double”等基本数据类型,使用结构体CGRect即可,如果使用oc对象呢?性能会受影响,因为创建对象还需要额外的开销,分配和释放内存等。
二、在类的头文件中尽量少引入其他头文件-总结
1、“@class”为向前声明,不需要知道声明类的实现,只需要让编译器知道这个类;
2、将引入头文件的时机尽量延后,只在确有需要时才引入,这样可以减少类的使用者所需引入的头文件数量;
3、向前声明,避免引入过多根本用不到的内容,会增加编译时间,比如,#import操作写在.m文件中,而.h文件仅仅使用@class进行声明,不禁纳闷起来,为什么不直接把#import放到.h文件中呢?这是因为.h文件在修改后,所有#import该.h文件的所有文件必须重新编译,因此,如果把#import写在.h文件中,#import该.h文件的文件也就会产生不必要的编译,增加编译时间,特别是项目是在项目文件多的情况下。想象一下,如果只是修改一个.h文件而导致上百个文件不必要的编译,将会浪费很多的时间;
4、向前声明,也解决了两个类“循环引用”的问题,使用#“import”避免了死循环,但是两个类有一个无法被正确编译;
5、继承自超类时,头文件必须要引入,如果声明某个类需要遵从某个协议,则也必须要引入头文件,因为此时编译器需要知道协议中定义的方法;
6、协议最好放在单独的头文件中,避免引入协议时引入过多其他用不到的内容,增加编译时间;但是委托最好和被委托类放在一起才会有意义,此时只需要在实现文件里引入并实现委托方法即可,最好把实现方法放在分类中实现;
7、每次在头文件引入其他头文件时,都要确保此举是否真的有必要,如果可以使用向前声明,就不要引入,若因为要实现属性、实例变量或者要遵从协议而必须要引入头文件,尽量移至分类中,这样做不仅可以缩短编译时间,而且还能降低彼此的依赖程度;
8、如果只想把代码的某个部分开放为公共api的话,太复杂的依赖关系也会出问题。
三、多用字面量语法,少用与之等价的方法-总结
1、使用字面量语法可以缩短源代码长度,使其更为易懂,比如数据通过取下标、字典通过key获取value相比objectAtIndex和objectForKey方法更加简洁易懂;
2、使用字面量语法更加安全,例如数组有空值nil,字面量语法会抛出异常,而常规数组则不会抛出异常出现的隐性问题不易发现;
示例代码:
//使用数组
NSArray*commonArray = [NSArrayarrayWithObjects:@"xiaoming",nil,@"xiaohong",nil];
NSLog(@"commonArray data = %@", commonArray);//数组长度为1,内容只包含“xiaoming”,数据有误
NSArray *ZMLArray = @[@"xiaoming", nil, @"xiaohong"];//编译器会直接报错
//使用字典
NSDictionary*commonDict =[NSDictionarydictionaryWithObjectsAndKeys:@"name",@"xiaoming",@"weight",nil,@"age", @18,nil];//程序crash,数据问题导致
NSLog(@"commonDict data = %@", commonDict);
NSDictionary*ZMLDict =@{
@"name":@"xiaoming",
@"weight":nil,//同理数组,编译器也会直接报错
@"age":@18
};
3、局限性:使用字面量语法必须属于foundation框架才行,如果是子类,则无法使用字面量语法,使用字面量创建出来的都是不可变对象,如果需要可变对象需要复制一份,需要多调用一个方法,再创建一个对象。
示例代码:
NSMutableArray*mutable = [@[@1, @2, @3, @4, @5]mutableCopy];
四、多用常量类型,少用#define预处理指令-总结
1、使用static const定义常量包含了类型信息,比如常量NSSrtring等有助于编写开发文档,加强代码可阅读性,命名规范的一个细节:常量名称如果局限于某“编译单元”则在前面加字母k,如果对外界可见,则以类名为前缀;
示例代码:
static const NSTimeInterval kAnimationDuration = 0.3;//仅限当前类单元可见
2、定义常量的位置,需注意冲突的问题,如果是多数类可能都会使用到的常量类型,可能会出现命名冲突,则应以类名为前缀加以区分,如animationDuration动画时间,可能多个类的定义时间是不一样的,这样区分可以有效避免冲突的问题;
3、static只限在当前单元可读取,对外不可见,如果不加static会生成一个“外部符号”,会与另外的单元的同名变量发生冲突,const为只读属性,static const表示只在当前单元是可读属性,无法被外界单元读取;
4、还有一些常量需要对外公开,使用者无需关心常量值是什么,比如通知,此类变量会放在“全局符号表”,便于定义该常量的编译单元之外使用,主要通过extern关键字告诉编译器在“全局符号表”加入此变量,此类变量命名需要谨慎,为避免冲突,使用与当前类名做前缀,ios系统中也采用了这种命名策略,如uikit中的uiapplicationdidenterbackgroundnotification等类似的常量名;
示例代码:
extern NSString* const loginSuccessNotification;//登录成功的通知常量,在.h声明,.m文件赋值,全局可调用此常量
5、使用预处理指令定义常量可能会无意中遭人篡改,从而导致应用程序各部分使用的值不同,应该尽量避免使用预处理指令定义常量,应该借助static或者extern来定义可查询类型的对外不公开或者对外公开的常量。
五、用枚举表示状态、选项、状态码-总结
1、c++11标准修改了枚举的特性,其中一项改动是可以指明用何种“底层数据类型”来保存枚举变量的值,语法为enum 枚举变量x:类型y,还可以手工指定某个枚举成员所对应的值;
2、枚举另一个使用场景如果需要选项可以组合,通过“按位或操作符”来组合,则可以通过枚举实现,系统库也频繁使用这个方法,例如uikit框架中的uiinterfaceorientationmask枚举,开发者需要实现对应方法通过“按位或”组合枚举值告诉系统视图所支持的显示方向;
3、foundation框架中用一些宏定义枚举类型,实现向后兼容能力,这些宏使用#define预处理实现的,枚举类型主要有状态或者选项组合,可以直接指定枚举值的底层数据类型;
4、凡是需要以按位或组合的枚举都应该使用ns_options定义,如果枚举不需要组合,则使用ns_enum来定义;
5、switch语句中,若是使用枚举来定义状态,则不应该使用default分支,因为如果使用default会处理枚举新加的一些状态,导致编译器不发警告,令开发者忽略需要在switch应该追加处理的情况;
六、理解“属性”这一概念-总结
1、“属性”是Objective-C的一项特性,用于封装对象中的数据;
2、使用synthesize指定实例变量的名字,默认带下划线开头,好处是代码易懂,使用dynamic关键字,自己来实现存取方法;
3、属性特质,属性的各种特质设定也会影响编译器所生成的存取方法;
示例代码:
@property (nonatomic, readwrite, copy) NSString *firstName;
原子性,是否使用同步锁,使用同步锁开销太大,会带来性能问题,所以一般属性都声明为nonatomic,一般情况下不要求属性必须是“原子的”,并不能保证“线程安全”,若要实现“线程安全”,还需采用更为深层的锁机制才行,比如,一个线程在连续读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值,读写权限,是否拥有“获取方法(getter)”和“设置方法(setter)”,可以把属性对外公开为只读属性,然后在分类中将其重定义为读写属性,内存管理语义,assgin、strong、weak、copy等;
七、在对象内部尽量直接访问实例变量-总结
1、在写入实例变量时,通过“设置方法”来做,而在读取实例变量时,直接访问,既可以提高读取操作的速度,又能控制对属性的写入操作,在初始化方法或dealloc方法中,应该直接访问实例变量,不应该使用存取方法,防止被子类覆写;
2、使用懒加载配置数据,需要通过属性获取方法来访问属性,否则对象永远不会被创建。
八、理解“对象等同性”这一概念-总结
1、若想检测对象的等同性,需要提供“isequal:”与hash方法;
2、相同的对象具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
3、如果已知两个受测对象都属于同一个类,可以使用“等同性判定方法”,如NSString的isEqualToString,使用“等同性判定方法”相比使用“isEqual”方法快,前者还好执行额外的步骤,因为它不知道受测对象的类型;
4、“isEqual”方法先判断两个指针是否相等,如果相等,则其均指向同一对象,受测对象也必定相等,接下来,比较两个对象所属的类是否相等,最后检测每个属性是否相等,只要有不相等的属性,就判定两对象不登,否则两对象相等;
5、hash方法的实现,可以保持较高效率,又能使生成的哈希吗至少位于一定范围之内,而不会过于频繁的重复;
6、如果经常需要判断等同性,可以自定义等同性判定方法,并覆写“isEqual:”方法,如果受测参数与接收该消息的对象都属于同一个类,就调用自定义的判定方法,否则交给超类方法“isEqual:”方法来判断;
7、等同性判定的执行深度,对象等同性判定的原则可能不一样,如NSArray的检测方式为先看两个数组的个数是否相等,如果相等,则在每个对应位置的两个对象身上调用其“isEqual:”方法,如果对应位置的对象均相等,那么这两个数组相等,这叫做“深度等同性判定”,有些对象则无需这种深度判定,如对象对应的一个唯一标识符来标记此对象,那么只需比较这个唯一标识符是否相等,就可以判断两个对象是否相等;
8、对象放入collection之后不应该再改变其内容,否则可能会造成数据错误的结果;
九、以“类族”模式隐藏实现细节-总结
1、类族是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节,比如iOS用户界面框架UIKit中的UIButton,创建者只需传入一个类型,即可获得一个实例,无需关心类内部具体的创建过程,如果按钮类型很多,则可以把按钮的实现方法放在相关子类中去,“类族”模式可以灵活应对多个类,将它们的实现细节隐藏在抽象积累后面,保证接口简洁,用户无需创建子类实例,只需调用基类方法来创建即可;
2、如果对象所属的类位于某个类族中,你可能觉得创建了对应类的实例,实际上创建的是其子类的实例,[object isMemberOfClass:[Object class]]似乎会返回YES,但实际上返回的是NO;
3、系统框架中有许多类族,大部分collection类都是类族,初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型,比如NSArrray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需要具备的一些接口,所以cocoa中的集合类新增子类,需要定义自己的数据存储方式,另外应继承自类族中的抽象基类,还应该覆写超类文档中指明需要覆写的方法,比如NSArray的子类,就需要实现count及“objectAtIndex:”方法,不过像lastObject则无需实现,基类可以根据前两个方法实现出这个方法;
4、类族模式可以把实现细节隐藏在一套简单的公共接口后面;
5、从类族的公共抽象类中继承子类时需要当心,优先阅读开发文档。
十、在既有类中使用关联对象存放自定义数据-总结
1、可以通过“关联对象”机制来把两个对象连起来,比如小明有一条狗,那么小明是被关联对象,狗是关联对象,小明还可以拥有更多的关联对象,比如一只猫,一只老鼠;
2、定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”,被关联的对象释放时,根绝内存管理语义决定是否释放关联对象,copy/retain会释放,而assign则不会释放,如果我们需要释放关联对象,则可以使用setAssociatedObject为nil来实现;
3、iOS中比较常见的是分类中添加属性,借助运行时的特性,为属性添加getter和setter方法,在setter方法中为分类关联一个值,通过一个key,这样在getter方法中,我们再通过这个key获取到这个关联值并返回,从而实现了为分类添加属性的目的;还有一些使用场景,比如uialertview,可以通过key关联一个具体的block块实现,来处理不同的alertview的具体逻辑。
十一、理解objc_msgsend的作用-总结
1、消息由接收者、选择子(方法名)及参数构成,给某对象“发送消息”也就相当于在该对象上“调用”方法;
2、发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码;
3、在Objective-C中,如果向对象传递消息,会使用动态绑定机制来决定需要调用的方法,在底层,所有的方法都是普通的C语言函数,对象究竟该调用哪个方法取决于运行期;
4、核心调用函数objc_msgSend,会依据接收者和选择子来调用适当的方法,需要在接收者所属的类中需其“方法列表”查找,如果能找到名称相符的方法,就执行实现代码,否则沿着继承体系继续向上查找,如果最后还是查找不到相符的方法,执行“消息转发”操作。
十二、理解消息转发机制-总结
1、若对象无法响应某个选择子(方法),进入消息转发流程比如当在控制台看到下面这种提示信息,那就说明你曾经向某个对象发送过一条其无法解读的消息,从而启动了消息转发机制,并将此消息转发给了NSObject的默认实现;
2、借助运行时,可以在需要用到某个方法时再将其加入到类中;
3、对象可以把无法解读的某些方法转交给其他对象来处理;
4、消息转发分两大阶段,第一阶段,先征询接收者,所属的类,是否能动态的添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”,第二阶段涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了,运行期系统会请求接收者以其他手段来处理当前消息,首先,接收者会看看是否还有其他对象能处理这条消息,若没有,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。
十三、用“方法调配技术”调试“黑盒方法”-总结
1、在运行期,可以向类中新增或替换方法所对应的方法实现;
2、使用另一份实现来替换原有的方法实现,叫“方法调配”,借助此技术向原有实现中添加新功能;
3、调试程序的时候可以在运行期修改方法实现,不能滥用。
十四、理解“类对象”的用意-总结
1、每个对象都有一个指向类对象的isa指针,用以表明其类型,而这些类对象则构成了类的继承关系;
2、如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知,比如“isKindOfClass:”可查询是否为某个类或者其派生类的实例,如NSMutableArray的实例是NSArray类派生类的实例,这是与“isMemberOfClass”的区别;
3、尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发机制,比方说,某个对象可能会把其受到的所有选择子都转发给另外一个对象,这样的对象叫做“代理”(proxy),此种对象均以NSProxy为根类,如果调用class方法,返回的实则是代理对象本身,若是改用“isKindOfClass:”这样的类型信息查询方法,返回的才是接收代理的对象的类的类型。
十五、用前缀避免命名空间冲突-总结
1、命名前缀至少是三个字母及以上的,因为apple保留使用所有“两字母前缀”的权利,使用两个字母可能会与系统类产生冲突;
2、应该给c函数加上前缀,因为是“顶级符号”,可能会产生命名冲突;
3、使用第三方库,应该为第三方库加上前缀,防止出现重复符号的错误。
十六、提供“全能初始化方法”-总结
1、当创建实例有多个初始化方法时,会有一个全能初始化方法令其他初始化方法都来调用它,只有在全能初始化方法中才会存储内部数据,当底层数据存储机制改变时,只需修改此方法的代码即可,无需改动其他初始化方法;
十七、实现description方法-总结
1、覆写description方法,将描述此对象的字符串返回;
2、在新实现的description方法中,也应该像默认的实现那样,打印出类的名字和指针地址;
3、可以借助nsdictionary类的description方法,在description方法中输出更多互不相同的信息;
4、使用nsdictionary来实现功能可以令代码更易维护:如果以后还要向类中新增属性,并且要在description方法中打印,那么只需修改字典内容即可;
5、debugdescription方法,与description相似,是开发者在调试器中以控制台命令打印对象时才调用的;
6、若想在调试时打印出更详细的对象描述信息,则应事先debugdescription方法。
十八、尽量使用不可变对象-总结
1、在设计类的时候,充分利用属性来封装数据,使用属性的时候,可将其声明为“只读”,尽量少使用不可变对象;
2、尽量把对外公布的属性设为只读,在确有必要时才对外公开,如果确定属性数据是不会改变的,可以设置为只读,并且设置好内存管理语义,方便以后快速更改读写属性;
3、如果修改类的内部数据,但是并不对外公开,通常做法是将readonly修改为readwrite,如果属性是nonatamic,则外界通过kvc仍然可以获取属性的修改权限,如果对象内部正在修改数据,外界也在同时修改读写此对象时,可能会导致数据不统一,可以通过“派发队列”来解决此问题,将数据存取操作设置为同步操作;
4、将属性在对象内部重新声明为readwrite这一操作可在分类中完成,在公共接口中声明的属性可在分类中重新声明,属性的其他特质必须保持不变,而readonly可扩展为readwrite,此时开发者应避免通过kvc来间接修改此属性的值;