读了一个多星期,终于好好地把Effective Objective-c读完了
这本书按照这三类来进行总结
概念类:讲解了一些概念性知识。
规范类:讲解了一些为了避免一些问题或者为后续开发提供便利所需要遵循的规范性知识。
技巧类:讲解了一些为了解决某些特定问题而需要用到的技巧性知识。
第1条:熟悉Objective-C
1. 运行期组件
oc是一门消息结构而并非函数调用的语言,运行时所执行的代码由运行环境来决定,而使用函数调用的语言,则由编译器决定。使用消息结构的语言,不论是否是多态,总是在运行时才会去查找所要执行的方法,编译器甚至不关心接受消息的对象是何种类型。接受消息的对象问题也要在运行时处理,其过程叫做动态绑定dynamic binding,其实现原理是由运行期组件完成(runtime component),而并非编译器,使用Objective-C的面向对象特性所需的全部数据结构以及函数都在运行期组件里面。运行期组件中含有全部内存管理方法。运行期组件本质上是一种与开发者所编写的代码相链接的动态库(dynamic library),其代码能把开发者所编写的所有程序粘合起来,所以只要更新运行期组件,即可提升应用程序性能。而那种许多工作都在编译期完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。
书中特别强调了Objective-C是C的超集superset,所以C语言中的所有功能在编写Objective-C代码时依然适用。因此,必须同时掌握C与Objective-C这两门语言的核心概念,方能写出高效的Objective-C代码来。其中尤为重要的是要理解C语言的内存模型memory model
,这有助于理解Objective-C的内存模型
及其引用计数reference counting机制的工作原理
。
NSString *someString = @"The string";
NSString *anotherString = someString;
此内存布局图演示了一个分配在堆中的NSString实例(@"The string"
),有两个分配在栈上的指针(someString
,anotherString
)指向该实例
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。
Objective-C将堆内存管理抽象出来了。不需要malloc
及free
来分配或释放对象所占内存。Objective-C运行期环境把这部分工作抽象为一套内存管理架构,名叫引用计数
在Objective-C代码中,有时会遇到定义里不含*的变量,它们可能会使用栈空间stack space
。这些变量所保存的不是Objective-C对象。比如CoreGraphics
框架中的CGRect
就是例子:
CGRect是C结构体,其定义是:
struct CGRect {
CGPoint origin,
CGSize size;
};
typedef struct CGRect CGRect;
整个系统框架都在使用这种结构体,因为如果改用Objective-C对象来做的话,性能会受到影响。与创建结构体相比,创建对象还需要额外开销,例如分配及释放堆内存等。如果只需保存 int
,float
,double
,char
等非对象类型nonobject type
,那么通常使用CGRect这种结构体就可以了。(不含*的变量,可能会使用栈空间。结构体保存非对象类型
)
第6条:理解“属性”这一概念
1. 存取方法
@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
@interface EOCPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end
//访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用:
aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];
NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];
如果我们不希望编译器自动生成存取方法的话,需要设置@dynamic 字段:
@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName, lastName;
@end
2. 属性特质
原子性:
nonatomic
:不使用同步锁
atomic
:加同步锁,确保其原子性
区别:具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。这也就是说,如果两个线程读写同一个属性,那么不论何时,总能看到有效的属性值。若是不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值时,另外一个线程也许会突然闯入,把尚未修好的属性值读取出来。发生这种情况时,线程读到的属性值可能不对。
如果开发过iOS程序,你就会发现,其中所有属性都是nonatomic
。这样做的历史原因是:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是原子的,因为这并不能保证线程安全thread safety
,若要实现线程安全的操作,还需采用更加深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic
,也还是会读到不同的属性值。因此,开发iOS程序时一般都会用到nonatomic
属性。但是在开发Mac OS X
程序时,使用atomic
属性通常不会有性能瓶颈。
读写
readwrite
:同时存在存取方法
readonly
:只有获取方法
内存管理
assign
:纯量类型(scalar type) 例如CGFloat或者NSInteger的简单赋值操作
strong
:拥有关系(owning relationship),设置新值的时候,保留新值,释放旧值,再设置新值
weak
:非拥有关系(nonowning relationship),设置新值的时候,既不保存新值,也不释放旧值。属性所指的对象遭到摧毁时,属性也会清空
unsafe_unretained
:类似assign,适用于对象类型(object type),非拥有关系,当目标对象遭到摧毁时,属性值不会清空(unsafe),这一点和weak有区别
copy
:此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其拷贝(copy),当属性特质是NSString *时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类,表示一种可以修改其值得字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份不可变(immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是可变的(mutable),就应该在设置新属性值时拷贝一份。
如果是copy,需要注意初始化方法
- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName
{
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
第8条:理解“对象等同性”这一概念
==操作符比较的是指针值,也就是内存地址。
然而有的时候我们只是想比较指针所指向的内容,在这个时候,就需要通过isEqual:方法来比较。
而且,如果已知两个对象是字符串,最好通过isEqualToString:
方法来比较。
对于数组和字典,也有isEqualToArray:
方法和isEqualToDictionary:
方法。
另外,如果比较的对象类型和当前对象类型相同,就可以采用自己编写的判定方法,否则调用父类的isEqual:
方法:
- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {
//先比较对象类型,然后比较每个属性
if (self == object) return YES;
if (![_firstName isEqualToString:otherPerson.firstName])
return NO;
if (![_lastName isEqualToString:otherPerson.lastName])
return NO;
if (_age != otherPerson.age)
return NO;
return YES;
}
- (BOOL)isEqual:(id)object {
//如果对象所属类型相同,就调用自己编写的判定方法,如果不同,调用父类的isEqual:方法
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson*)object];
} else {
return [super isEqual:object];
}
}
//这是书本上一个比较好的hash方法
- (NSInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger secondNameHash = [_secondName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ secondNameHash ^ ageHash;
//这种做法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁地重复。当然,此算法生成的哈希码还是会碰撞collision,不过至少可以保证哈希码有多种可能的取值。编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。
}
第11条:理解objc_msgSend的作用
在OC中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数.
然而对象收到 消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
在OC中,给对象发送消息的语法是:
id returnValue = [someObject messageName:parameter];
这里,someObject叫做“接收者(receiver)”,messageName:叫做"选择子(selector)",选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:
void objc_msgSend(id self, SEL cmd, ...)
第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的,所以这个函数就是参数个数可变的函数。
因此,上述以OC形式展现出来的函数就会转化成如下函数:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
这方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码,如果找不到就沿着继承体系继续向上查找,等找到合适的方法之后再跳转,如果最终还是找不到相符的方法,那就执行消息转发操作(message forwarding)。
这么说来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend会将匹配结果缓存在"快速映射表"(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然啦,这种"快速执行路径"(fast path) 还是不如"静态绑定的函数调用操作"(statically bound function call)那样迅速,不过只有把选择子缓存起来了,也就不会慢很多,实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。
第12条:理解消息转发机制
如果对象所属类和其所有的父类都无法解读收到的消息,就会启动消息转发机制(message forwarding)。
尤其我们在编写自己的类时,可在消息转发过程中设置挂钩,用以执行预定的逻辑,而不应该使应用程序崩溃。
消息转发分为两个阶段:
1.征询接受者,看它能否动态添加方法,以处理这个未知的选择子,这个过程叫做动态方法解析(dynamic method resolution)。
如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。首先,请接收者看看有没有其他对象能处理这条消息。
如果有,则运行期系统会把消息转给那个对象,于是消息转发过程结束。
如果没有,则启动完整的消息转发机制(full forwarding mechanism),运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。
//EOCAutoDictionary.h
#import
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
//EOCAutoDictionary.m
#import "EOCAutoDictionary.h"
#import
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;
- (id)init {
if ((self = [super init])) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self,selector,(IMP)autoDictionarySetter, "v@:@");
} else {
class_addMethod(self,selector,(IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
在本例中,EOCAutoDictionary类将属性设置为
@dynamic
,也就是说编译器无法自动为其属性生成set和get方法,因此我们需要动态给其添加set和get方法。
我们实现了resolveInstanceMethod:方法:首先将选择子转换为String,然后判断字符串是否含有set字段,如果有,则增加处理选择子的set方法;如果没有,则增加处理选择子的get方法。其中class_addMethod可以给类动态添加方法。
实现增加处理选择子的get方法:
id autoDictionaryGetter(id self, SEL _cmd) {
// Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
// The key is simply the selector name
NSString *key = NSStringFromSelector(_cmd);
// Return the value
return [backingStore objectForKey:key];
}
在这里,键的名字就等于方法名,所以在取出键对应的值之前,要将方法名转换为字符串。
实现增加处理选择子的set方法:
void autoDictionarySetter(id self, SEL _cmd, id value) {
// Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
/** The selector will be for example, "setOpaqueObject:".
* We need to remove the "set", ":" and lowercase the first
* letter of the remainder.
*/
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
// Remove the ':' at the end
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
// Remove the 'set' prefix
[key deleteCharactersInRange:NSMakeRange(0, 3)];
// Lowercase the first character
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
因为key的名字对应了属性名,也就是没有set,首字母小写,尾部没有:的字符串。然而,将set方法转换为字符串后,我们需要将set方法的这些“边角”都处理掉。最后得到了“纯净”的键后,再进行字典的赋值操作。
这里需要注意:
class_addMethod(Class cls, SEL name, IMP imp, const char *types)中的types
比如:”v@:”意思就是这已是一个void类型的方法,没有参数传入。
比如 “i@:”就是说这是一个int类型的方法,没有参数传入。
比如”i@:@”就是说这是一个int类型的方法,又一个参数传入。
所以v@:@就是说 void类型的方法,有一个参数(set方法)
@@: 就是说 返回id类型的方法,没有参数(get方法)
第14条:理解“类对象”的用意
在运行期程序库的头文件里定义了描述OC对象所用的数据结构:
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;
};
在这里,isa指针指向了对象所属的类:元类(metaclass),metaclass用来表述类对象本身所具备的元数据。类方法定义于此,它是整个结构体的第一个变量。super_class定义了本类的超类,来确立继承关系,而isa指针描述了实例所属的类。
我们使用isMemberOfClass:能够判断出对象是否为某个特定类的实例;
而isKindOfClass:方法能够判断出对象是否为某类或其派生类的实例。
这两种方法都是利用了isa指针获取对象所属的类,然后通过super_class类在继承体系中游走。在OC语言中,必须使用查询类型信息的方法,方能完全了解对象的真实类型。因为对象类型无法在编译期决定。
尤其注意在collection里获取对象时,将它们从collection中取出来的类型通常是id,不是强类型的(strongly typed),如果想要知道具体类型,那就可以使用类型信息查询方法。
所以如果我们对这些对象的类型把握不好,那么就会有可能造成对象无法响应消息的情况。因此,在我们从集合里取出对象后,通常要进行类型判断:
- (NSString*)commaSeparatedStringFromObjects:(NSArray*)array {
NSMutableString *string = [NSMutableString new];
for (id object in array) {
if ([object isKindOfClass:[NSString class]]) {
[string appendFormat:@"%@,", object];
} else if ([object isKindOfClass:[NSNumber class]]) {
[string appendFormat:@"%d,", [object intValue]];
} else if ([object isKindOfClass:[NSData class]]) {
NSString *base64Encoded = /* base64 encoded data */;
[string appendFormat:@"%@,", base64Encoded];
} else {
// Type not supported
}
}
return string;
}
第21条:理解Objective-C错误类型
在OC中,我们可以用NSError描述错误。
使用NSError可以封装三种信息:
Error domain:错误范围,类型是字符串
Error code :错误码,类型是整数
User info:用户信息,类型是字典
1.通过委托协议来传递NSError,告诉代理错误类型。
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
2.作为方法的“输出参数”返回给调用者
- (BOOL)doSomething:(NSError**)error
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) {
// There was an error
}
//自定义错误
//EOCErrors.h
extern NSString *const EOCErrorDomain;
//定义错误码
typedef NS_ENUM(NSUInteger, EOCError) {
EOCErrorUnknown = –1,
EOCErrorInternalInconsistency = 100,
EOCErrorGeneralFault = 105,
EOCErrorBadInput = 500,
};
// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain"; //定义错误范围
第22条:理解NSCopying协议
如果我们想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法- (id)copyWithZone:(NSZone*)zone
例子:
- (id)copyWithZone:(NSZone*)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
//之所以是copy->_friends,而不是copy.friends是因为friends并不是属性,而是一个内部使用的实例变量。
copy->_friends = [_friends mutableCopy];
return copy;
}
1. 复制可变的版本:
遵从协议
- (id)mutableCopyWithZone:(NSZone*)zone;
注意:拷贝可变型和不可变型发送的是copy和mutableCopy消息,而我们实现的却是
- (id)copyWithZone:(NSZone*)zone和-(id)mutableCopyWithZone:(NSZone*)zone方法。
如果我们想获得某对象的不可变型,统一调用copy方法;
如果我们想获得某对象的可变型,统一调用mutableCopy方法。
例如数组的拷贝:
-[NSMutableArray copy] => NSArray-[NSArray mutableCopy] => NSMutableArray
第29条:理解引用计数
尽管在iOS系统已经支持了自动引用计数,但仍然需要开发者了解其内存管理机制。
- 计数器的操作:
retain:递增保留计数。
release:递减保留计数
autorelease :待稍后清理“自动释放池时”,再递减保留计数。
注意:在对象初始化后,引用计数不一定是1,还有可能大于1。因为在初始化方法的实现中,或许还有其他的操作使得引用计数+1,例如其他的对象也保留了此对象。
有时,我们无法确定在某个操作后引用计数的确切值,而只能判断这个操作是递增还是递减了保留计数。 - 自动释放池:
将对象放入自动释放池之后,不会马上使其引用计数-1,而是在当前线程的下一次事件循环时递减。
使用举例:如果我们想释放当前需要使用的方法返回值是,可以将其暂时放在自动释放池中
(NSString*)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease];
}
- 保留环(retain cycle)即循环引用
对象之间相互用强引用指向对方,会使得全部都无法得以释放。解决方案是讲其中一端的引用改为弱引用(weak reference),在引用的同时不递增引用计数。
第30条:以ARC简化引用计数
使用ARC,可以省略对于引用计数的操作,让开发者专注于开发本身:
if ([self shouldLogMessage]) {
NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
NSLog(@"message = %@", message);
[message release]; ///< Added by ARC
}
显然这里我们不需要message对象了,那么ARC会自动为我们添加内存管理的语句。
因此,在ARC环境下调用内存管理语句是非法的:
retain
release
autorelease
dealloc
注意:ARC只负责管理OC对象的内存,CoreFoundation对象不归ARC管理
第37条:理解“块”这一概念
块(Block)分为三类:栈块
堆块
全局块
1. 栈块
//定义块的时候,其所占内存区域是分配在栈中的,而且只在定义它的那个范围内有效:
void (^block)();
if ( /* some condition */ ) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
block();
//上面定义的两个块只在if else语句范围内有效,一旦离开了最后一个右括号,如果编译器覆写了分配给块的内存,那么就会造成程序崩溃。
2.堆块
//为了解决这个问题,我们可以给对象发送copy消息,复制一份到堆里,并自带引用计数:
void (^block)();
if ( /* some condition */ ) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();
3.全局块
void (^block)() = ^{
NSLog(@"This is a block");
};
第47条:熟悉系统框架
Foundation框架
:NSObject,NSArray,NSDictionary等
CFoundation框架
:C语言API,Foundation框架中的许多功能,都可以在这里找到对应的C语言API
CFNetwork框架
:C语言API,提供了C语言级别的网络通信能力
CoreAudio框架
:C语言API,操作设备上的音频硬件
AVFoundation框架
:提供的OC对象可以回放并录制音频和视频
CoreData框架
:OC的API,将对象写入数据库
CoreText框架
:C语言API,高效执行文字排版和渲染操作
用C语言来实现API的好处:可以绕过OC的运行期系统,从而提升执行速度。