`
啸笑天
  • 浏览: 3432990 次
  • 性别: Icon_minigender_1
  • 来自: China
社区版块
存档分类
最新评论

《Effective Objective C 2.0》笔记

 
阅读更多

第1章 熟悉objective-c 1
第1条:了解objective-c语言的起源 1
EZ_1:Objective-C的动态特性
Objective-C具有相当多的动态特性,基本的,也是经常被提到和用到的有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
1、动态类型
即运行时再决定对象的类型。这类动态特性在日常应用中非常常见,简单说就是id类型。id类型即通用的对象类,任何对象都可以被id指针所指,而在实际使用中,往往使用introspection来确定该对象的实际所属类:
id obj = someInstance;  
if ([obj isKindOfClass:someClass])  
{
    someClass *classSpecifiedInstance = (someClass *)obj;
    // Do Something to classSpecifiedInstance which now is an instance of someClass
    //...
}
动态类型识别常用方法:
-(BOOL)isKindOfClass:classObj  是否是classObj类或其子类
-(BOOL)isMemberOfClass:classObj是否是classObj的实例
-(BOOL)respondsTosSelector:selector  类中是否有这个方法
NSClassFromString(NSString*);由字符串得到类对象
NSStringFromClass([类名 Class]);由类名得到字符串
NSSelectorFromString(NSString*);根据方法名得到方法标识
(NSString*)NSStringFromSelector(SEL);得到SEL类型的方法名 
2、动态绑定
基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。在继续之前,需要明确Objective-C中消息的概念。由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。
动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。在Cocoa层,我们一般向一个NSObject对象发送-respondsToSelector:或者-instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。一个例子:
void dynamicMethodIMP(id self, SEL _cmd)  
{
    // implementation ....
}


//该方法在OC消息转发生效前被调用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL

    if (aSEL == @selector(resolveThisMethodDynamically)) {
        //向[self class]中新加入返回为void的实现,SEL名字为aSEL,实现的具体内容为dynamicMethodIMP class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);
        return YES;
    }
    return [super resolveInstanceMethod:aSel];
}  
当然也可以在任意需要的地方调用class_addMethod或者method_setImplementation(前者添加实现,后者替换实现),来完成动态绑定的需求。
3、动态加载
根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图。随着Retina iPad的推出,和之后可能的Retina Mac的出现,这个特性相信会被越来越多地使用。
深入运行时特性
基本的动态特性在常规的Cocoa开发中非常常用,特别是动态类型和动态绑定。由于Cocoa程序大量地使用Protocol-Delegate的设计模式,因此绝大部分的delegate指针类型必须是id,以满足运行时delegate的动态替换(在Java里这个设计模式被叫做Strategy?不是很懂Java,求纠正)。而Objective-C还有一些高级或者说更底层的运行时特性,在一般的Cocoa开发中较为少见,基本被运用与编写OC和其他语言的接口上。但是如果有所了解并使用得当的话,在Cocoa开发中往往可以轻易解决一些棘手问题。
这类运行时特性大多由/usr/lib/libobjc.A.dylib这个动态库提供,里面包括了对于类、实例成员、成员方法和消息发送的很多API,包括获取类实例变量列表,替换类中的方法,为类成员添加变量,动态改变方法实现等,十分强大。完整的API列表和手册可以在这里找到。虽然文档开头表明是对于Mac OS X Objective-C 2.0适用,但是由于这些是OC的底层方法,因此对于iOS开发来说也是完全相同的。
一个简单的例子,比如在开发Universal应用或者游戏时,如果使用IB构建了大量的自定义的UI,那么在由iPhone版转向iPad版的过程中所面临的一个重要问题就是如何从不同的nib中加载界面。在iOS5之前,所有的UIViewController在使用默认的界面加载时(init或者initWithNibName:bundle:),都会走-loadNibNamed:owner:options:。而因为我们无法拿到-loadNibNamed:owner:options的实现,因此对其重载是比较困难而且存在风险的。因此在做iPad版本的nib时,一个简单的办法是将所有的nib的命名方式统一,然后使用自己实现的新的类似-loadNibNamed:owner:options的方法将原方法替换掉,同时保证非iPad的设备还走原来的loadNibNamed:owner:options方法。使用OC运行时特性可以较简单地完成这一任务。
代码如下,在程序运行时调用+swizze,交换自己实现的loadPadNibNamed:owner:options和系统的loadNibNamed:owner:options,之后所有的loadNibNamed:owner:options消息都将会发为loadPadNibNamed:owner:options,由自己的代码进行处理。
+(BOOL)swizze {
    Method oldMethod = class_getInstanceMethod(self, @selector(loadNibNamed:owner:options:));
    if (!oldMethod) {
        return NO;
    }
    Method newMethod = class_getInstanceMethod(self, @selector(loadPadNibNamed:owner:options:));
    if (!newMethod) {
        return NO;
    }
    method_exchangeImplementations(oldMethod, newMethod);
    return YES;
}  
loadPadNibNamed:owner:options的实现如下,注意在其中的loadPadNibNamed:owner:options由于之前已经进行了交换,因此实际会发送为系统的loadNibNamed:owner:options。以此完成了对不同资源的加载。
-(NSArray *)loadPadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary *)options {
    NSString *newName = [name stringByReplacingOccurrencesOfString:@"@pad" withString:@""];
    newName = [newName stringByAppendingFormat:@"@pad"];
    //判断是否存在
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString* filepath = [[NSBundle mainBundle] pathForResource:newName ofType:@”nib”];
    //这里调用的loadPadNibNamed:owner:options:实际为为交换后的方法,即loadNibNamed:owner:options:
    if ([fm fileExistsAtPath:filepath]) {
        return [self loadPadNibNamed:newName owner:owner options:options];
    } else {
        return [self loadPadNibNamed:name owner:owner options:options]; 
    }
}  
当然这只是一个简单的例子,而且这个功能也可以通过别的方法来实现。比如添加UIViewController的类别来重载init,但是这样的重载会比较危险,因为你UIViewController的实现你无法完全知道,因此可能会由于重载导致某些本来应有的init代码没有覆盖,从而出现不可预测的错误。当然在上面这个例子中重载VC的init的话没有什么问题(因为对于VC,init的方法会被自动转发为loadNibNamed:owner:options,因此init方法并没有做其他更复杂的事情,因此只要初始化VC时使用的都是init的话就没有问题)。但是并不能保证这样的重载对于其他类也是可行的。因此对于实现未知的系统方法,使用上面的运行时交换方法会是一个不错的选择~ 

第2条:在类的头文件中尽量少引入其他头文件 4
除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量1、降低类之间的耦合(coupling),2、降低编译时间,3、避免循环引用的错误。

 有时无法使用向前声明:
1、比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
2、继承父类,必须引用头文件。

第3条:多用字面量语法,少用与之等价的方法 7
1、增强易读性;
2、提高安全性,用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
3、字面量语法有个小小的限制,就是除了字符串以外,所创建出来的对象必须属于Foundation框架才行。如果自定义了这些类的子类,则无法用字面量语法创建其对象。要想创建自定义子类的实例,必须采用“非字面量语法”(nonliteral syntax);
4、使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,则需复制一份: NSMutableArray *mutable = [@[@1, @2, @3, @4, @5]mutableCopy];


第4条:多用类型常量,少用#define预处理指令 11
1、不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。


2、只在编译单元内可见的常量(即目标文件范围的常量,m文件产量,类范围内的常量)
命名:前缀一般用k

demo:
// EOCAnimatedView.h #import
 @interface EOCAnimatedView : UIView 
- (void)animate;
 @end

 // EOCAnimatedView.m 
#import “EOCAnimatedView.h"
 static const NSTimeInterval kAnimationDuration = 0.3; 
@implementation EOCAnimatedView 
- (void)animate {
 [UIViewanimateWithDuration:kAnimationDuration animations:^(){
 }
@end 
// Perform animations }];

变量一定要同时用 static 与 const 来声明。如果试图修改由 const 修饰符所声明的变量, 那么编译器就会报错。在本例中,我们正是希望这样:因为动画播放时长为定值,所以不 应修改。而 static 修饰符则意味着该变量仅在定义此变量的编译单元中可见。编译器每收 到一个编译单元,就会输出一份“目标文件”(object file)。在 Objective-C 的语境下,“编译 单元”一词通常指每个类的实现文件(以 .m 为后缀名)。因此,在上述范例代码中声明的 kAnimationDuration 变量,其作用域仅限于由 EOCAnimatedView.m 所生成的目标文件中。假 如声明此变量时不加 static,则编译器会为它创建一个“外部符号”(external symbol)。此时 若是另一个编译单元中也声明了同名变量,那么编译器就抛出一条错误消息: 
duplicate symbol _kAnimationDuration in: EOCAnimatedView.o EOCOtherView.o

 实际上,如果一个变量既声明为 static,又声明为 const,那么编译器根本不会创建符号, 而是会像 #define 预处理指令一样,把所有遇到的变量都替换为常值。不过还是要记住:用这 种方式定义的常量带有类型信息。


3、全局常亮
命名:为避免名称冲突,最好是用与之相关的类名做前缀。系统框架中一般都 这样做。例如 UIKit 就按照这种方式来声明用作通知名称的全局常量。其中有类似 UIApplicatio nDidEnterBackgroundNotification 与 UIApplicationWillEnterForegroundNotification 这样的常量名。

demo:
// EOCLoginManager.h
#import
extern NSString *const EOCLoginManagerDidLoginNotification;
@interface EOCLoginManager : NSObject - (void)login;
@end
// EOCLoginManager.m #import "EOCLoginManager.h"
NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";
@implementation EOCLoginManager
 - (void)login {
// Perform login asynchronously, then call 'p_didLogin'. }
- (void)p_didLogin { [[NSNotificationCenter defaultCenter]
postNotificationName:EOCLoginManagerDidLoginNotification object:nil];
}@end


4、注意点

  • const 

const最好理解,修饰的东西不能被修改 指针类型根据位置的不同可以理解成3种情况:
 I 常量指针 // 初始化之后不能赋值,指向的对象可以是任意对象,对象可变。
 NSString * const pt1;
 II 指向常量的指针 // 初始化之后可以赋值,即指向别的常量,指针本身的值可以修改,指向的值不能修改
 const NSString * pt2;
 III 指向常量的常量指针
 const NSString * const pt3;

  • extern

等同于c,全局变量的定义,
//x .h 声明
extern const NSString * AA;
//x .m 定义
const NSString * AA = @"abc";

// 调用
#import "x.h"
或者再次申明
extern const NSString * AA;

  • static

等同于c,将变量的作用域限定于本文件?
不同于java C++里面的类变量,oc没有类变量

  • 结论


static
    // static变量属于本类,不同的类对应的是不同的对象
    // static变量同一个类所有对象中共享,只初始化一次

const
    // static const变量同static的结论I,只是不能修改了,但是还是不同的对象
    // extern const变量只有一个对象,标准的常量的定义方法
    // extern的意思就是这个变量已经定义了,你只负责用就行了

static const NSString * const ChatWindow_STR_TITLE_INDEX[] = { 
    @"msg_type_all",  
    @"msg_type_area",  
    @"msg_type_PM",  
    @"msg_type_team", 
    @"guild_title", 
    @"msg_type_system", 
    @"msg_type_world" 
}; 
我在.h文件里定义一个static变量,
I .h初始化
在不同的.m里调用都有值,这些值是否同一个对象?
【不同的对象】
II .m里初始化(or赋值)
初始化的值只在.m文件里有效果,即等同于c,将变量的作用域限定于本文件
其他.m文件调用的值都没有赋值。
【不同的对象】


第5条:用枚举表示状态、选项、状态码 14
http://justsee.iteye.com/blog/2164025

① 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。

② 用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的。而不会采用编译器所选的类型。

③ 在处理枚举类型的switch 语句中不要实现default分支,这样加入新的枚举类型后,编译器就会提示开发者,switch语句并未处理所有的枚举。

④ 可以指明用何种“底层数据类型”来保存枚举类型的变量。这样做的好处是,可以向前声明枚举变量了。若不指定底层数据类型,则无法向前声明枚举类型。因为编译器不清楚数据类型的大小。所以在用到此枚举类型时,也就不知道究竟该给变量分配多少空间了。
第2章 对象、消息、运行期 21

第6条:理解“属性”这一概念 21
1. Java 或 C++ 中,使用publish,private等关键字来声明变量的作用域。

这种写法的问题是:对象布局在编译器(compile time)就已经固定了。碰到访问其中变量的代码,编译器就把它替换为“偏移量”(offset)。这个偏移量是硬编码(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
这样子在修改了类定义后必须重新编译。否则offset是错误的。
例如某个代码库的代码使用了一份旧的类定义,如果和其相链接的代码使用了新的定义,那么运行时就会出现不兼容现象。
OC的做法,就是把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义改变了,那么存储的偏移量也就变了。这样的话,无论何时访问实例变量,总能使用正确的偏移量。
甚至还可以在运行期间向类中新添实例变量。
这就是稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。
 
2. 如果不想令编译器自动合成存取方法,则可以自己实现。如果只实现了其中一个存储方法,则另一个还是会由编译器来合成。
 
3. 使用 @dynamic 关键字可以阻止编译器自动合成。而且在编译访问属性的代码时,及时编译器发现定义的存取方法,也不会报错。
它相信这些在运行期能找到。
 
4. 原子性
默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity);
 
5. 读/写权限
① 具备 readwrite 特质的属性拥有“获取方法”和“设置方法”。@synthesize 会自动生成这两个方法。
② readonly 特质仅拥有获取方法。
 
6. 内存管理语义
属性用于封装数据,而数据则要有“具体的所有权语义”(concrete ownership semantic);
① assign:设置器置灰执行针对“纯量类型”的简单赋值操作。
② strong:设置新值时,会先保留新值,并释放旧值,然后再将新值设置上去。
③ weak:设置新值时,既不保留新值,也不释放旧值。当属性所指的对象遭到摧毁时,属性值也会清空。
④ unsafe_unretained:语义和assign想同,但适用于对象。该特质表达一种“非拥有”,当目标对象被摧毁,属性值不会自动清空,这点和weak不同。
⑤ copy:设置新值时不保留新值,将其“拷贝”。只要属性所用的对象是“可变的”(mutable),就应该在设置新属性值时拷贝一份。
 
7. 方法名
① getter = <name> 指定“获取器”的方法名。如果某属性时Boolean型,而想在其获取器上加“is”前缀。则可指定。
例如说有个属性这样定义:
@property (nonatomic, getter = isOn) BOOL on;
 
② setter = <name> 指定“设置方法”;
 
注:若是自己来实现这些存储方法,那么应该保证其具备相关属性所声明的特质。
例如声明特质为copy,则要在复制的时候使用copy。
 
决不应该在init方法(或者dealloc方法)中使用存取方法;
 
8. 在iOS中,使用同步锁的开销较大。一般情况下并不要求属性必须时原子的,因为这并不能保证“线程安全”,若要实现“线程安全”需采用更深层次的锁定机制才行。

第7条:在对象内部尽量直接访问实例变量 28
1. 建议在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。

直接访问和存取方法的区别:
① 由于不经过OC的“方法派发”(method dispatch)步骤。所以直接访问实例变量的速度当然比较快。
这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
 
② 直接访问实例变量时,不会调用其“设置方法”;
 
③ 直接访问实例变量时,不会触发“键值观测”(key-Value Observing,KVO)通知。
 
④ 通过属性来访问有助于排查与之相关的错误,因为可以给获取器和设置器中新增断点,监控该属性的调用者及其访问时机。
 
 
2. 在初始化方法(或dealloc)中,总是直接访问实例变量。因为子类可能会“覆写”(override)设置方法。
 
3. 惰性初始化(lazy initialization)
这种情况侠必须通过获取器来访问属性,否则实例变量就永远不会初始化。
 
- (id)brain {
     if(!_brain){
          _brain = [Brain new];
     }
     return _brain;

第8条:理解“对象等同性”这一概念 30
1. 若想检测对象的等同性,请提供“isEqual”与“hash”方法。

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. - (BOOL)isEqual:(id)object  
  2. {  
  3.      if(self == object) return YES;  
  4.      if([self class] != [object class] ) return NO;  
  5.      ConcreteId *otherId = (ConcreteId*)object;  
  6.      if(![someProperty isEqual:otherId.someProperty])  
  7.           return NO;  
  8.      // ……… 比较其他属性值  
  9.      return YES;  
  10. }  
① 特定类所具有的等同性判定方法
如果受测的参数与接收该消息的对象都属于同一类,那么就调用自己编写的判定方法。否则使用超类来判断。
[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. - (BOOL)isEqual:(id)object  
  2. {  
  3.      if([self class] ==[ object class ])  
  4.      {       
  5.           return [self isEqualToPerson:(EOCPerson*)object};  
  6.      }else {  
  7.           return [self isEqual:object];  
  8.      }  
  9. }  
2. 相同的对象必须具备相同的哈希码,但是两个哈希码相同的对象却未必相同。
 
3. 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
 
① 等同性判定的执行深度。
如果某个对象是从数据库创建出来的,其中就有可能有个属性是“唯一标识符”,这种情况下只需使用这个属性进行判断而无需全部属性都判断一遍。
 
4. 编写hash时,应该使用计算速度快而且哈希码碰撞几率低的算法。
 
 
5. 容器中可变类的等同性。
① 在容器中放入可变类对象的时候,就不应该再修改其哈希码了。
如果放入collection后其哈希码又变化了,那么其所在的这个“箱子数组”对它来说就是“错误”的。
 
使用NSMutableSet 和 NSMutableArray 对象测试一下。先把一个数组加入set中:
[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. NSMutableSet *set = [NSMutableSet new];  
  2. NSMutableArray *arrayA = [@[@1@2] mutableCopy];  
  3. [set addObject:arrayA];  
  4. NSLog(@“set = %@“,set);  
  5. // set = {((1,2))}  
再向set加入一个数组,此数组与前一个数组所含的对象相同,顺序也相同:
[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. NSMutableArray *arrayB = [@[@1@2] mutableCopy];  
  2. [set addObject:arrayB];  
  3. NSLog(@“set = %@“,set);  
  4. // set = {((1,2))}   
此时仍只有一个对象,因为刚才要加入的那个数组对象和set中已有的数组对象相等,所以set并不会改变:
[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. NSMutableArray *arrayC = [@[@1] mutableCopy];  
  2. [set addObject:arrayC];  
  3. NSLog(@“set = %@“,set);  
  4. // set = {((1),(1,2))}    
然后我们再改变arrayC的内容:
[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. [arrayC addObject:@2];  
  2. NSLog(@“set = %@“,set);  
  3. // set = {((1,2),(1,2))}    
此时set中就包含了两个彼此相等的数组,根据set语义是不允许出现这种情况的。然后现在却无法保证这一点。
若是拷贝此set
[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. NSSet *setB = [set copy];  
  2. NSLog(@“setB = %@“,setB);  
  3. // setB = {((1,2))}   
所以说,如果把某个对象放入set后又修改其内容,那么后面的行为将很难预测。

第9条:以“类族模式”隐藏实现细节 35

第10条:在既有类中使用关联对象存放自定义数据 39

第11条:理解objc_msgsend的作用 42

第12条:理解消息转发机制 46

第13条:用“方法调配技术”调试“黑盒方法” 52

第14条:理解“类对象”的用意 56

.第3章 接口与api设计 60

第15条:用前缀避免命名空间冲突 60

第16条:提供“全能初始化方法” 64

第17条:实现description方法 69

第18条:尽量使用不可变对象 73

第19条:使用清晰而协调的命名方式 78

第20条:为私有方法名加前缀 83

第21条:理解objective-c错误模型 85

第22条:理解nscopying协议 89

第4章 协议与分类 94

第23条:通过委托与数据源协议进行对象间通信 94

第24条:将类的实现代码分散到便于管理的数个分类之中 101

第25条:总是为第三方类的分类名称加前缀 104

第26条:勿在分类中声明属性 106

第27条:使用“class-continuation分类”隐藏实现细节 108

第28条:通过协议提供匿名对象 114

第5章 内存管理 117

第29条:理解引用计数 117

第30条:以arc简化引用计数 122

第31条:在dealloc方法中只释放引用并解除监听 130

第32条:编写“异常安全代码”时留意内存管理问题 132

第33条:以弱引用避免保留环 134

第34条:以“自动释放池块”降低内存峰值 137

第35条:用“僵尸对象”调试内存管理问题 141

第36条:不要使用retaincount 146

第6章 块与大中枢派发 149

第37条:理解“块”这一概念 149

第38条:为常用的块类型创建typedef 154

第39条:用handler块降低代码分散程度 156

第40条:用块引用其所属对象时不要出现保留环 162

第41条:多用派发队列,少用同步锁 165

第42条:多用gcd,少用performselector系列方法 169

第43条:掌握gcd及操作队列的使用时机 173

第44条:通过dispatch group机制,根据系统资源状况来执行任务 175

第45条:使用dispatch_once来执行只需运行一次的线程安全代码 179

第46条:不要使用dispatch_get_current_queue 180

第7章 系统框架 185

第47条:熟悉系统框架 185

第48条:多用块枚举,少用for循环 187

第49条:对自定义其内存管理语义的collection使用无缝桥接 193

第50条:构建缓存时选用nscache而非nsdictionary 197

第51条:精简initialize与load的实现代码 200

第52条:别忘了nstimer会保留其目标对象 205

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics