声明:这个笔记的系列是我每天早上打开电脑第一件做的事情,当然使用的时间也不是很多因为还有其他的事情去做,虽然吧自己买了纸质的书但是做笔记和看的时候基本都是看的电子版本,一共52个Tip每一个Tip的要点我是完全誊写下来的,害怕自己说的不明白所以就誊写也算是加强记忆,我会持续修改把自己未来遇到的所有相关的点都加进去,最后希望读者尊重原著,购买正版书籍。PS:不要打赏要喜欢~
Demo:GitHub代码网址,大大们给个鼓励Star啊。
整个系列笔记目录
《Effective Objective-C 2.0》第一份读书笔记
《Effective Objective-C 2.0》第二份读书笔记
《Effective Objective-C 2.0》第三份读书笔记
第一章 熟悉Objective-C
1.了解Objective-C语言的起源
Objective-C 起源自Smalltalk的,也就是消息性语言。而不是函数调用
[obj perform:parameter1 and:parameter2]; 消息语言
obj->perform(parameter1,paramter2); 函数调用方式
关键区别在于:消息语言最终执行的代码由运行环境决定,而函数调用则是由编译器决定。这也叫做“动态绑定(dynamic binding)”
我们创建一个OC对象的时候可以这样:
NSString * someString = @“new string ”;
中间的星号代表的就是指针,所以我们创建的是一个someString指针指向一个NSString的对象,我们把someString指针对象放在栈(stack)中,而真正的指针对象"new string"我们放在堆(heap space)中。如果我们新建一个新的指针anyString的数值仍然是"new string",那么就不会重新在堆中新建内存而是直接吧新的anyString指向已经创建的"new string"对象。
分配在堆中的内存需要程序员直接管理,而分配在栈上的会在栈弹出时自动清理。
Objective-C将堆内存管理抽象出来了,不需要用malloc及free来分配或释放对象所占内存。Objective-C运行期环境把这部分工作抽象为一套内存管理框架,名为“引用计数”,
在OC代码中也会有使用栈(stack)空间的变量,比如不带星号的Sturck对象,比如CGRect,如果只是简单的高度,宽度等的数据的时候,就不用建立OC对象了会耗费更多额外的开销。
要点:
- Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接受一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型和指针。
2.在类的头文件中尽量少引入其他头文件
这里面讲了为什么使用@class “EOCEmployer.h”这样的写法。
这叫做“向前声明(forward declaring )” 这是因为不需要知道EOCEmployer的内部接口细节。
要点:
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来体积别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
- 有时无法使用向前声明,比如要声明某个类遵守一项协议。这种情况下,尽量把“该类遵守某协议”的这条声明移至"class-continuation分类"中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
3.多用字面量语法,少用与之等价的方法
我们可以通过“字符串自变量(string literal)”语法来创建对象:
NSString * someString = @“Effective Objective-C 2.0”;
使用字面量语法可以缩减源代码长度,使其更为易读。
有的时候需要把整数、浮点数、布尔值封入Objective-C对象中。这种情况下可以用NSNumber类,该类可处理多种类型的数值。若是不用字面量,那么就需要按下述方式创建实例:
NSNumber * someNumber = [NSNumber numberWithInt:1];
NSNumber * someNumber = @1;
NSNumber * boolNumber = @YES;
NSNumber * charNumber = @‘a’;
NSNumber * expressionNumber = @(x * y);
关于NSArray的一个字面量问题:
id object1 = /********/;
id object2 = /********/;
id object3 = /********/;
NSArray * arrayA =[NSArray arrayWithObjects:object1,object2,object3,nil];
NSArray * arrayB =@[object1,object2,object3];
如果object1和object3 都是指向正常的对象,而object2 是nil。那么按照字面量语法创建数组arrayB时会抛出异常,而arrayA虽然能创建出来,但是只有一个object1一个对象。”arrayWithObjects”方法会依次处理各个参数,知道发现nil为止,由于object2是nil,所以该方法会提前结束。所以arrayB更加安全,直接弹出要比你完美运行后来少一个数据来的更安全。
小缺点:
使用字面量语法创建的字符串,数组,字典对象都是不可变的(immutable)。若想要可变版本的,就需要复制一份:
NSMutableArray * mutable = [[@1,@2,@3,@4,@5] mutableCopy];
这么做会多调用一个办法,但是好处是要多余上面的那个缺少数据的缺点的。
要点:
- 应该使用字面量语法来创建字符串,数值,数组,字典。与创建此类对象的常规方法相比,这么做更加简单扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组和字典时,若值中又nil,则会抛出异常。因此,务必确保值里不含 nil。
4.多用类型常量,少用#define预处理指令
#define ANIMATION_DURATION 0.3
这样的预处理容易导致整个工程里的ANIMATION_DURATION都是0.3,很有可能会改变系统定义的一些宏。
我们选择这种方式:
static const NSTimerInterval kAnimationDuration = 0.3;
我们的习惯是如果常量只是出现在一些类的“编译单元(translation unit)”之内,则前面加"k",如果是类之外也可见就用类名做前缀。
定义常量的位置很重要,我们总喜欢在头文件里声明预处理指令,这样做真的很糟糕,当常量名称有可能互相冲突时更是如此。
如果想要定义一个定义域在某个文件中的定义的话,我们可以这样:
static const NSTimerInterval kAnimationDuration = 0.3;
变量一定要用static和const一同修饰,用const修饰的变量如果遭到更改就会报错,而static修饰符则意味着变量仅在定义此变量的编译单元中可见,也就是这个.m中。假如声明此变量时不加static,则编译器就会为它创建一个“外部符号(external symbol)”。此时如果是另一个编译单元里面也声明了一样的变量就会抛出错误。
而想要一个全局变量的话:
//.h:
extern NSString * const EOCStringConstant;
//.m:
NSString * const EOCStringConstant = @“VALUE”;
const修饰符放在EOCStringConstant指针外面就符合语义为防止指针的方向。
编译器看到头文件中的extern关键字,就会明白如何在引入磁头文件的代码中处理该常量了。在全局符号表将会有一个名教EOCStringConstant的符号。也就说,编译器无需查看器定义,即允许代码使用此常量。因为它知道,当连接成二进制文件之后,肯定能好到这个常量。
此类常量必须要定义。而且只能定义一次。编译器会在"数据段(data section)"为字符串分配存储空间
。链接器会把目标文件和其他目标文件相链接,生成最终的二进制文件。链接器可以随时解析这个常量。
要点:
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译之前根据此执行查找与替代操作。即使有人重新定义了常量值,编译器也不会产生警告信息。这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const来定义“只在编译单元内可见的常量”。由于次常量不在全局符号表中,所以无需在其前面加前缀。
- 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加上区隔,通常用与之相关的类名做前缀.
5.用枚举表示状态,选项,状态码
枚举可以用来列举某个对象的一些形态
enum EOCConnectionState{
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
如果想要简单编写的话
typedef enum EOCConnectionState = EOCConnectionState;
EOCConnectionState state = /*****/; //这样
还有一种情况适用枚举类型,定义选项的时候,如果其中可以彼此组合那就更加适合了,各个选项之间可通过"按位或操作符(bitwise)"。
enum UIViewAutoresizing{
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1<<0,
UIViewAutoresizingFlexibleWidth = 1<<1,
UIViewAutoresizingFlexibleRightMargin = 1<<2,
}
这样除了none,其他的都可以多选。
下面是”<<“符号带来的内存存储方式:
要点:
- 应该用枚举来表示状态机的状态,传递给方法的选项以及状态码等值,给这些值起一个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又同时实现,那么就将个选项值定义为2的幂,以便通过按位或操作将其组合起来。
- 用NS_ENUM和NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
- 在处理枚举类型的switch语句中不要定义default分支,这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
第二章 对象,消息,运行期
6.理解“属性”这一概念
在OC中,对象(object)就是基本构造单位(building block),开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)。
当应用程序运行起来以后,为其提供相关支持的代码叫做”Objective-C运行期环境(Objective-C runtime)”,它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。
关于NSString的Copy和Strong修饰符
这边重申一下为什么NSString使用copy而不用strong:
因为如果NSString对象是取一个NSMutableString对象的数值的话,当NSMutableString的对象数值发生改变的时候,NSString会相应的发生改变,而不是保持远数值.
为什么NSMutableString使用strong而不用copy:
因为如果是copy修饰的话,NSMutableArray的数值就不能发生改变。
先随便写一个声明
@interface EOCPerson:NSObject{
@public
NSString * _firstName;
NSString * _lastName;
@private
NSString * _ someInternalData
}
如果我们在_firstName上面在加一个_iSuName而firstName和lastName都向下移动,这样EOCPerson就会拥有三个属性,其实一搭眼好像这样完全没有问题,其实对象布局在编译器布局的时候已经固定了,所以当你在上面加入一个iSuName的时候其实,想要获取firstName的类还是会根据编译器时候的偏移量来获取firstName,但是实际上获取的却是iSuName的值,原来的元素全部都后移的时候,那么以偏移量为参考量的做法会导致请求的元素不兼容(incompatibility)- - ,OC解决这种问题的方法就是把实例变量当做一种存储偏移量所用的“特殊变量”,然后交由“类对象”保存(14条详解)。这个时候偏移量在运行的时候就会动态改变,找到正常的数值。这就是稳固的ABI(Application Binary Interface)。
get 和 set方法:
EOCPerson * aPerson =[Person new];
aPerson.firstName = @“Bob”; // same as [aPerson setFirstName: Bob];
NSString * lastName = aPerson.lastName; // same as NSString * lastName = [aPerson lastName];
然而属性还有更多的优势。如果使用了属性的话,编译器就会自动编写访问这些属性所需的方法。
如果想要改变名字的长相的话:
@implementation EOCPerson
@synthesize firstName = _myFirstName ;
@synthesize lastName = _mySecondName;
这样你在声明里面定义的firstName,lastName就变成了_myFirstName,_mySecondName。
当然你也可以选择手动生成get,set方法(@dynamic)
@implementation EOCPerson
@dynamic firstName,lastName
编译器不会为上面这两个属性自动合成存储方法和实例变量。如果用代码访问其中的属性,编译器也不会发出警告消息。
属性特质:
- 原子性:严格意义上说原子性(automicity)因为使用同步锁的原因是要比非原子性(nonatomic)要更加安全,但是在属性上添加同步锁要消耗过多的资源。所以我们基本上会使用nonatomic做修饰符。
- 读写权限:也就是readwrite(读写),readonly(只读)。
- 内存管理语义:
assign : 只会执行对“纯量类型”,例如(CGFloat或者是NSInteger等)的简单赋值操作。
strong : 定义了一种“拥有关系”,为这种属性设置新值时,设置方法先保留新值,并释放旧值,然后将新值替换上去。
weak: 定义了一种”非拥有关系”,为这种属性设置新值的时候,设置方法即不保留新值也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁的时候,属性值也会被清空。
unsafe_unretained: 和assign相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”,但是当目标对象遭到摧毁的时候,属性值不会自动清空,这一点和weak有区别。
copy: 此特质所表示的所属关系和strong类似。然而设置方法并不保留新数值,而是将其“拷贝”。当属性类型为NSString *时候,经常用此特质来保护其封装性。
需要注意:如果自己来实现存取方法,那么应该保证其具备相关属性所声明的特质,比方说,如果将某个属性声明为copy,那么就应该在”设置方法”中拷贝相关对象。否则会误导该属性的使用者。
说道这个在初始化的时候为什么不是用set方法来保证每次都调用NSString都能带上Copy的内存语义。而是使用属性赋值给copy的:_firstName = [firstName copy];(第七条详解)
//.h
@interface EOCPerson : NSManagerObject
@property (copy) NSString * firstName ;
@property (copy) NSString * lastName ;
- (id) initWithFirstName: (NSString *)firstName lastName:(NSString *)secondName;
@end
//.m
- (id) initWithFirstName: (NSString *)firstName lastName:(NSString *)secondName{
if( self = [super init]){
_firstName = [firstName copy];
_lastName = [lastName copy];
}
}
要点:
- 可以用@property语法来定义对象中所封存的数据。
- 通过“特质”来制定存储数据所需的正确定义。
- 在设置属性所对象的实力变量时,一定要遵从属性所声明的寓意。
- 开发iOS程序时候应该使用nonatomic属性,因为atomic属性会严重影响性能。
7.在对象内部尽量直接访问实例变量
首先确认一下 _name:直接访问对象实例变量 self.name 间接访问实例变量
self.name 和_name 的区别是:
- 由于不经过Objective-C的“方法派发”步骤,所以直接访问实例变量的速度当然比较快。在这种情况下所生成的代码会直接访问保存对象变量的那块内存。
- 直接访问实例变量时,不会调用其“设置方法”,这就绕过了相关属性定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
- 如果直接访问实例变量,那么不会触发“键值观察(KVO)”,通知。这样做是否会产生问题,还取决于具体的对象行为。
回想一下上一个Tip上的问题为什么在初始化的时候不使用self.firstName = firstName,而选择_firstName = [firstName copy];
嗨呀,好气啊,我思想跑的太远了,我以为是在子类重写父类属性会影响父类的运行呢,并不是啊只是子类运行的时候如果发现父类的init方法中使用点语法而子类中正好重写了这个方法,那么就会走子类的方法,子类如果有限制条件语句的时候会导致条件在不清楚的情况下意外不能通过。而如果直接是属性的话就不会走set方法直接就跳过子类的判断环节了。例子请看EOCPerson.demo
还有一种情况下使用获取方法
那就是惰性初始化(lazy initialization)
- (EOCBrain *)brain{
if(!_brain){
_brain = [Brain new];
}
return _brain;
}
这种情况下你不self.brain的话代码就没法走了。
要点:
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则通过属性来写。
- 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读取数据。
- 有时会使用惰性初始化计数配置某分数据,在这种情况下,需要通过属性来读取数据。
8.理解“对象等同性”这一概念
按照 “==” 操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是期所指的对象。
NSString * foo = @“Badger 123”;
NSString * bar = [NSStringWithFormat:@“Badger %i”,123];
BOOL equalA = ( foo == bar);
BOOL equalB = [foo isEqual:bar] ;
// set out -> equalA == NO
- 第一种判定的方法就是:
isEqual
所谓的所有属性判断:
a.判断两个指针是否相等,当切仅当指针值相等的时候才会想等。
b.比较两个对象所属的类。
c.逐条属性判断。 - 第二种判定的方法就是:
hash
若两个对象相等,则其哈希吗也相等。但是两个哈希吗相等的对象未必相等。
假如某个collection是用set实现的,那么set可能会根据哈希吗把对象分装到不同的数组中。向set中添加新对象时,要根据其哈希吗找到与之相关的那个数组。依次检查其中各个元素。看是哦否有相同的,如果有相同的,那就说明要添加的对象已经在set里面了。由此可知,如果令每个对象返回相同的哈希吗,那么在set中已经有10000000个对象的情况下,如要继续向里面添加对象,就需要全部便利一边。
那么有没有可能让set 中含有两个相同的对象呢。
下面代码:
NSMutableSet * set = [NSMutableSet new];
NSMutableArray * arrayA =[@[@1,@2] mutableCopy];
[set addObject:arrayA];
NSLog(@"%@",set);
NSMutableArray * arrayB =[@[@1,@2] mutableCopy];
[set addObject:arrayB];
NSLog(@"%@",set);
NSMutableArray * arrayC =[@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"%@",set);
[arrayC addObject:@2];
NSLog(@“%@",set);
最后在NSSet * setB =[set Copy];
就成了含有两个 (1,2)….
要点:
- 若想检测对象的等同性,请提供”isEqual”和hash方法。
- 相同的对象必须具备相同的哈希吗,但是两个哈希码相同的对象确未必相同
- 不要盲目的逐个检测每条对象,而是应该依照具体需求制定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算吗
9.以“类族模式”隐藏实现细节
“类族”是一种很有用的模式(pattern),可以隐藏“抽象基类”背后的实现细节。Objective-C系统框架中普遍使用此模式。比如:
+ (UIButton *)buttonWithType:(UIButtonType)type;
该方法的返回对象,其类型取决于按钮的类型。然而,不管返回是什么类型的对象,它们都集成来自同一个基类:UIButton。这么做的意义在于:UIButton类的使用者无需关心创建出来的按钮属于哪一个子类,也不用考虑绘制细节。
在测试代码里面有创建类族的代码
在这些代码里面,基类实现了一个“类方法”,该方法根据待创建的雇员类别分配好对应的雇员类实力。这种“工厂模式(Factory pattern)”是创建类族的办法之一。
如果对象所属的类位于某个类族中,那么在查询其类型消息(introspection)时就要小心了,你可能觉得创建了某个类的实例,然而实际上创建的却是其子类的实例。在这个Employee这个例子中,[employee isMemberOfClass:[EOCEmployee class]]
似乎会返回YES,但实际上返回的却是NO,因为employee并非Empoyee类的实例,而是其某个子类的实例。
Cocoa里的类族
系统框架中又许多类族。大部分collection类都是类族,例如NSArray与其可变的版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以合起来算作一个类族。
像NSArray这样的类的背后其实是个类族(对于大部分collection类而言都是这样),明白这一点很重要,否则可能会写出下面这种代码
id maybeArray = /****/
if ([maybeAnArray class] == [NSArray class]) {
// Will never be hit 永远不会进入
}
你要是知道NSArray是个类族,那就会明白上述代码错在哪里:[maybeAnArray class]所返回的类绝不可能是NSArray类本身,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型。我们可以通过
判断对象是否在类族iskindof
来判断(14条详细讲解):
id maybeAnArray = /*****/
if ([maybeAnArray isKindOfClass:[NSArray class]]){
// Will be hit // 会走这边
}
要点:
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档请先阅读。
10.在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息。我们可以使用关联对象Associated Object
。
可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明”存储策略(storage policy)“,用以维护响应的“内存管理语义”。
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_COPY copy
下列方法可以管理关联对象:
void objc_setAssociatedObject (id object ,void * key ,id value,objc_AssociationPolicy policy)
此方法以给定的键和策略为某对象设置关联对象值。
void objc_getAssociatedObject(id object, void object)
此方法根据给定的键从某对象中获取相应的关联对象值。
void objc_removeAssocatedObjects(id object)
此方法移除指定对象的全部关联对象。
做的方法:
- (void)askUserAQuestion{
UIAlertView * alert =[[UIAlertView alloc]initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
void(^block)(NSInteger) = ^(NSInteger buttonIndex){
if (buttonIndex == 0){
[self doCancle];
}else{
[self doContinue];
}
};
objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
[alert show];
}
//UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
void (^block)(NSInteger)= objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
block(buttonIndex);
}
要点:
- 可以通过“关联对象”机制来把两个对象连起来
- 定义关联对象时可指定内存管理语义,用以访问定义属性时采用的“拥有关系”和“非拥有关系”
- 只有在其他做法不可行的时候才会应用关联对象,因为这种做法通常会引入难于查找的bug。
11.理解 objc_msgSend的作用
objective-C的术语来说,这叫“传递消息”
id returnValue = [someObject messageName: parameter];
在本例中,someObject叫做接受者(receiver),messageName叫做“选择子(selector)”。
void objc_msgSend(id self,SEL cmd,…)
id returnValue =objc_msgSend (someObject,@selector(messageName:),parameter);
为了完成调用的做法,该方法需要在接受者所属的类中搜索器“方法列表”(list of methods)
,如果能找到与选择子名称相符的方法,就跳至其实现代码,如果找不到就沿着继承体系向上查找,等找到合适的方法之后在跳转。如果最终还是找不到相符的方法,那就执行“消息转发(message forwording)
操作”。
这么看来,调用一个方法好像需要很多步骤。索性objc_megSend会将匹配结果缓存在“快速映射表(fash map)里面”。每一个类都有这样的一块缓存,当然“快速请求路径”还是不如静态来的快,但是也不会慢很多。
objc_msgSend_struct;
objc_msgSend_fpret;
objc_msgSendSuper;
方法存储的方式大概是:
<return_type> Class_selector(id self ,SEL_cmd,…)
真正的函数其实和这个差不多,因为“尾调用优化 (tail- call optimization)技术”
:
如果某个函数的最后一项操作是调用某个函数的话,就会调用“尾调用优化”技术。编译器会生成调转至另一个函数所需的指令码,而且不会调用堆栈中推入新的“栈帧(frame stack)”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用的时候次啊会执行”尾调用优化”,这样做可以防止过做的发生“栈溢出”现象。
要点:
- 消息由接受者,选择子及参数构成。给某对象“发送消息(invoke a message)”也就相当于在该对象上“调用方法(call a method)”。
- 发给某对象的全部消息都要由“动态消息派发系统(dynamic message dispatch system)”来处理,该系统会查出对应的方法,执行其代码。
12.理解消息转发机制
若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译期向类发送了无法解读的消息并不会报错,因为在运行期可以继续给类中添加方法也就是“消息转发(message forwarding)”机制,程序员可经由此过程告诉对象应该如何处理位置消息。
关于错误初始化的报错:
此异常表明:消息接受者的类型是__NSCFNumber,而该接受者无法理解名为lowercaseString的选择子。
消息转发分为两大阶段:
- 先征询接受者,所属的类,看其是否能动态添加方法,以处理这个“位置的选择子(unknown selector)”,这叫做“动态方法解析(dynamic method resolution)”。
- 就是完整的消息转发机制(full forwarding mechanism)。 如果运行期喜用已经吧第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来响应包含该选择自的消息,此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。这里面要分成两个部分,首先,请接受者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发结束,若没有“备用的接受者”,则启动完整的消息转发机制,运行期喜用会把与系统有关的全部细节都封装到NSInvocation对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。
主要的转发路径为:
resolveInstanceMethod ----> forwardingTargetForSelector ----> forwardInvocation
要点:
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行时的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
- 对象可以把无法解读的某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还是没有办法处理选择子,那就启动完整的消息转发机制。
13.用“方法调配技术”调试“黑盒方法”
类的方法列表会把选择子的名字映射到相关的方法实现上,使得“动态消息派发系统”能够依据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型为:
id (*IMP) (id, SEL ,…)
获得想要交换的两个函数的方法:
Method class_getInstanceMethod(Class aClass,SEL aSelector)
交换的方法:
void method_exchangeImplementations(Method m1, Method m2)
Method originalMethod = class_getInstanceMethod(NSStringClass,@selector(lowercaseString));
Method swappedMethod =
class_getInstanceMethod(NSStringClass,@selector(uppercaseString));
method_exchangeImpLementations(originalMethod , swappedMethod);
实际应用其实很少交换,我们都是要添加一个方法。
要点:
- 在运行期,可以向类中新增或替代选择子所对应的方法实现。
- 使用另一份实现来替代原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现添加新功能。
- 一般来说,只要调试程序的时候次啊会需要在运行期修改方法实现,这种做法不适合滥用。
14.理解“类对象”的用意
每一个Objective-C 对象实例都是指向某块内存地址的指针。所以在声明变量时,类型后要跟上一个 * 字符:
NSString * pointerVariable = @“some String”;
描述OC对象所用的数据结构定义在运行期程序库的头文件中,id类型本身也在定义在这里:
typedef struct objc_object{
Class isa;
} * id ;
typedef struct objc_class * Class;
struct objc_class{
Class isa;
Class super_Class;
const char * name;
long version;
long info ;
long instance_size;
struct objc_ivar_list * ivars;
struct objc_method_list ** methodLists;
struct objc_cache * cache;
struct objc_protocol_list * protocols;
}
在类集成体系中查询类型信息,注意下这这个书里面呢mutableDic调用isMemberofClass对比NSMutableDic是返回YES但是我做测试写代码的时候发现并不是这样的,返回的还是NO。
NSMutableDictionary * dict =[NSMutableDictionary new];
[dict isMemberofClass: [NSDictionary class]]; NO
[dict isMemberofClass:[NSMutbaleDictionary class]]; NO
[dict isKindofClass:[NSDictionary class]]; YES
[dict isKindofClass:[NSArray class]]; NO
要点:
- 每个实例都有一个指向Class对象的指针,用以表明类型,而这些Class对象则构成类的集成体系。
- 如果对象类型无法在编译器确定,那么就应该使用类型消息查询方法来探知。
- 尽量使用类型消息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
第三章 接口和api设计
15.用前缀避免命名空间冲突
要点:
- 选择与你的公司,应用程序或二者都关联的名字做类名的前缀,并在所有代码中军使用这个前缀
- 若自己所开发的程序中使用了第三方库,则应为其中的名称加上前缀。
16.提供“全能初始化方法”
要点:
- 在类中提供一个全能初始化方法,并在文档中指明。其他初始化方法均应调用此方法。
- 若全能初始化方法和超类不同,则需覆写超类中对应的方法。
- 如果超类的初始化方法不适合子类,那么应该覆写这个超类方法,并抛出异常。
17.实现description方法
平时的时候想要看看打印的效果的时候我们通常使用NSLog,但是使用MVVM的时候传递的对象基本上都是以自定义model类型来传递的,那么直接log可能就会出现这样的情况:
object = <EOCPerson:0x7fd9a1600600>
显然model内部的成员变量就别想着看了。解决的办法很简单,在类中加入description方法:
- (NSString *)description{
return [NSString StringWithFormat:@<%@:%p,\%@ %@\>,[self class],self,_firstName,_lastName];
}
//这样打印出来的数据
<EOCPerson:0x7f249c030f0,"Bob Smith">
要点:
- 实现description方法返回一个有意义的字符串,用以描述该实例。
- 若想在调试的时候打印出详尽的对象描述信息,则应实现debugDescription。
18.尽量使用不可变对象
如果那可变对象放入容器(collection)之后再修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。因此,笔者建议大家尽量减少对象中的可变内容。
有时候可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部将readonly属性重新声明为readwrite。
也就是在.h使用readOnly 在.m readwrite。
要点:
- 尽量创建不可变的对象
- 若某属性仅可用于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性。
- 不要把可变的collection(容器)作为属性公开,而应提供相应方法,以此修改对象中的可变容器。
19.使用清晰而协调的命名方式
要点:
- 起名时应遵守标准的 Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
- 方法名要言简意赅,从左到右读起来要像个日常用语中的句子才好。
- 方法明理不要使用缩略后的类型名称
- 给方法起名时的第一要务就是确保其风格与你自己代码所要集成的框架相符。
20.为私有方法名加前缀
要点:
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区别开。
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
21.理解Objective-C 错误模型
在不是致命错误(fatal error)的情况下,我们是不会让程序直接抛出异常的,比如创建某个类的时候,Coder初始化没有给出一个必须要的参数的时候,我们选择给这个创建对象返回nil来使得Coder意识到创作对象的时候出现了错误。
要点:
- 只要发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
- 在错误不那么严重的情况下,可以指派“委托方法(delegate method)”来处理错误,也可以把错误消息放在NSError对象里,经由“输出参数”返回给调用者。
22.理解NSCopying协议
使用对象时经常需要拷贝它,在Objetive-C中,此操作通常通过copy完成。如果想让自己的类支持拷贝操作,那就实现NSCopying协议的- copyWithZone:
方法
- (id)copyWithZone:(NSZone *)zone{
Twentytwo * copy =[[self class] allocWithZone:zone];
return copy;
}
在官方的例子里面,提到了我们class-continuation里面包含一个实例变量的时候,我们怎么样防止内存管理语义导致的原对象copy之后生成的新对象copy2继承这个内部实例变量的内容。所以在做其他属性copy的时候对于这个不想要继承的内部成员变量我们需要mutableCopy。如同例子Twentytwo的例子一样。
[NSMutableArray copy] => NSArray
[NSArray mutableCopy] => NSMutableArray
这边顺便说一下为什么不可变的NSArray,NSArray,NSDictionary使用Copy。而可变的NSMutableString,NSMutableArray,NSDictionary使用MutableCopy。
因为有可能NSString获取的方式是通过mutableStr赋值的。为了防止当mutableStr更改的时候str在不知情的情况下更改。而NSMutableString如果是Copy来修饰的,那么这个容器就将失去可变的特性,而被Copy成一个不可变的字符串,数组或者是字典。
这里面带一下 ->这个东西是干嘛的,我的理解是当一个内部的实例变量需要被方法实现内部调用的时候就可以使用 copy->_friends这样。具体可以去看我的例子TwntytwoTest。
关于深拷贝(deep copy)和浅拷贝(shallow copy)
浅拷贝:只拷贝容器对象本身,而不复制其中数据。
深拷贝: 在拷贝对象自身时,将其底层数据也一并复制过去。
比如对NSSet对象的深拷贝:- initWithSet: copyItems:
, 如果items 设置为YES,就是深拷贝。
要点:
- 若想令自己所写的对象具备拷贝对象,则需实现NSCopying协议。
- 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying和NSMutableCopying协议。
- 复制对象时需要决定采用深拷贝还是浅拷贝,一般情况下应该尽量执行浅拷贝。
- 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
结尾
自己写的笔记首先是用Pages写的,写完之后放到简书里面以为也就剩下个排版了,结果发现基本上每一个点的总结都不让自己满意,但是又想早点放上去,总感觉自己被什么追赶着,哈哈,本来写完笔记的时候是2W字的,结果到第二次发表的时候发现就成了2.5W了,需要改进的东西还是太多,希望朋友们有什么改进的提议都可以告诉我,我会一直补充这个笔记,然后抓紧改GitHub上的代码~