Effective OC

一、在类的头文件中尽量少引入其它头文件

处理方法:
在 .h文件中尽量使用@class声明需要引入的类,然后在.m文件中才真正的引入对应的类。

优点:

  • 这样可以减少类的使用者所需引入的头文件数量,减少编译时间。
  • 解决两个类互相引用的问题(实际上使用#import不会导致死循环,但两个类里有一个无法被正确编译)

二、多用字面量语法,少用与之等价的方法

字面量语法理解:

  • 创建数组是直接用@[ ],创建字典和NSNumber等类似。若想创建可变对象,则需复制一份:[@[object1, object2 ] mutableCopy]
  • 通过取下标作为访问数据下标或字典中的键所对应的元素。

优点:

  • 使用字面量语法,更加简明扼要。
  • 用字面量创建数据或字典的时候,若值中有nil,则会抛出异常。

三、多用类型常量,少用#define预处理指令

用法:类型常量用static const定义。static的作用是声明局部变量;const的作用是声明变量只读。如果声明全局变量,使用extern修饰。
优点:预处理指令定义常量不包含类型信息,用static const定义的包含类型信息。

四、枚举和switch的结合使用

在处理枚举类型的switch语句中,如果不实现default分支,则加入新枚举之后,编译器就会提示switch语句未处理所有枚举。

五、对“属性”的理解

对属性的处理:

eg.

@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
  • 编译器会自动编写访问这些属性所需的方法。
  • 编译器会自动向类中添加适当类型的实例变量,并且在属性名前面加下划线。如上面所示的属性则会生成_firstName和_lastName两个实例变量。也可以通过在实现代码里用@synthesize语法来指定实例变量的名字
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
@dynamic:

eg.

@implementation EOCPerson
@dynamic firstName;
@end

@dynamic 就是要来告诉编译器,代码中用@dynamic修饰的属性,其getter和setter方法会在程序运行的时候或者用其他方式动态绑定,以便让编译器通过编译。其主要的作用就是用在NSManageObject对象的属性声明上,由于此类对象的属性一般是从Core Data的属性中生成的,Core Data框架会在程序运行的时候为此类属性生成getter和Setter方法。
假设有这么个场景,B类,C类分别继承A类,A类实现某个协议(@protocol),协议中某个属性( somePropety )我不想在A中实现,而在B类,C类中分别实现。如果A中不写任何代码,编译器就会给出警告:
这时你用@dynamic somePropety;编译器就不会警告,同时也不会产生任何默认代码。

属性性质:(assign、strong、copy等)

属性性质是在使用存取方法的时候才起效的。
eg.
@property (nonatomic, copy) NSString *b;
使用self.b = aString;才可以达到copy效果。

六、关联对象

日常开发中有时需要在对象中存放相关信息,通常做法是从要使用对象所属的类继承一个子类,然后改写子类,比如给子类添加额外的属性信息等。但是有时候类的实例是通过某种别的机制创建的,我们无法创建出自己所需要的子类。那么Objective-C中提供一种别的方法来解决这个问题,这就是“关联对象“。
  可以为某对象关联许多其他对象,这些对象通过“键”来区分,储存对象值的时候可以指明“存储策略”,用以维护相关的内存语义。这些内存语义分别和属性中定义的相对应。 (objc_AssociationPolicy)

关联类型 等效的@property
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatiomic,copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy
关联对象相关方法
  • void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy)此方法用于通过使用给定的键和策略为某对象设置关联对象值。
  • id objc_getAssociatedObject(id object, void *key)通过给定键从某对象中获取对应的关联对象值。
  • void objc_removeAssociatedObjects(id object)移除指定对象中的全部关联对象。
在关联对象中,如果想要两个键匹配同一个值,则二者必须是完全相同的指针。因此在设置关联对象的时候,都是通过使用静态全局变量作为键
使用举例

例如在iOS中我们都是用过UIAlert类,当用户要处理点击事件的时候,需要通过委托协议来实现。这时候就需要把视图和事先动作的代码分开。例如:

- (void)userAlert {
    UIAlert *alert = [[UIAlert alloc] initWithTitle:@"Alert" 
    message:@"do you want to close?" 
    delegate: self
   cancelButtonTitle:@"Cancel"
    otherButtonTitles:@"OK",nil];
    [alert show];
}

#param -mark UIAlertView Delegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger:)buttonIndex{
    if(buttonIndex == 0){
        //do action
    } else {
        //do action
    }
}

通常都是这么做,但是如果代码中使用多个UIAlerView的时候,还需要通过在回调中判断alertView的类型,然后再去处理响应的逻辑。要是能够是创建视图的时候,就把每个按钮响应的逻辑写好,那就简单多了。于是可以:

_alertView = [[UIAlertView alloc] initWithTitle:@"Alert"
message:@"This is deprecated?" 
delegate:self 
cancelButtonTitle:@"Cancel" 
otherButtonTitles:@"Ok", nil];
    void (^block)(NSInteger) = ^(NSInteger buttonIndex){
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doOk];
        }
    };
    objc_setAssociatedObject(self.alertView, MyAlertViewKey, block, OBJC_ASSOCIATION_COPY);

#pragma -mark UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    void (^block)(NSInteger) = objc_getAssociatedObject(alertView, MyAlertViewKey);
    block(buttonIndex);
}

这种方法很有用,但是,只应该在其他办法行不通的时候才去考虑使用它。若是滥用,则很快就会令代码失控,使其难以调试。“保留环”(retain cycle)产生的原因很难查明。

七、用“方法调配技术”调试“黑盒方法”

  • 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  • 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
  • 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做 IMP,其原型如下:
id (*IMP)(id, SEL, ...)

NSString 类可以响应 lowercaseSting、uppercaseString、capitalizedString 等选择子,这个张映射表中的每个选择子都映射到了不同的 IMP 之上,如图 NSString 类的选择子映射表:


  Objective-C 运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。
  经过几次操作之后,类的方法表就会变成这个样子:

  在新的映射表中,多了一个名为 newSelector 的选择子,capitalizedString 的实现也变了,而 lowercaseString 与 uppercaseString 的实现则互换了。上述修改均无须编写子类,只要修改了 “方法表” 的布局,就会反映到程序中所有的 NSString 实例之上。
  想交换方法实现,可用下列函数:
void method_exchangeImplementations(Method m1, Method, m2)
  此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:
Method class_getInstanceMethod(Class aClass, SEL aSelector)

此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面提到的 lowercaseString 与 uppercaseString 方法实现:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

从现在开始,如果在 NSString 实例上调用 lowercaseString,那么执行的将是 uppercaseString 的原有实现,反之亦然。
  在实际应用中,像这样直接交换两个方法实现的,意义并不大。因为 lowercaseString 与 uppercaseString 这两个方法已经各自实现的很好了,没必要再交换了。但是,可以通过这一手段来为既有的方法实现增添新功能。
  比方说,想要在调用 lowercaseString 时记录某些信息,这时就可以通过交换方法实现来达成此目标。我们新编写一个方法,在此方法中实现所需的附加功能,并调用原有实现。
  新方法可以添加至 NSString 的一个 “分类”(category)中:

@interface NSString (EOCMyadditions)
- (NSString *)eoc_myLowercaseString;
@end

上述新方法将与原有的 lowercaseString 方法互换,交换之后的方法表如图:


交换 lowercaseString 与 eoc_myLowercaseString 的方法实现
  新方法的实现代码可以这样写:

- (NSString *)eoc_myLowercaseString {2 NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;5
}

这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和 lowercaseString 方法交换的。所以,在运行期,eoc_myLowercaseString 选择子实际上对应于原有的 lowercaseString 方法实现。最后,通过下列代码来交换这两个方法实现:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
NSString *string = @"ThIs is tHe stRiNg";
NSString *lowercaseString = [string lowercaseString];

执行完上述代码之后,只要在 NSString 实例上调用 lowercaseString 方法,就会输出一行记录消息:

FileLocation: NSString+EOCMyadditions.m (33) Function: -[NSString(EOCMyadditions) eoc_myLowercaseString] ThIs is tHe stRiNg => this is the string

通过此方案,开发者可以为那些 “完全不知道其具体实现的”(completely opaque,“完全不透明的”)黑盒方法增加日志记录功能,这非常有助于程序调试。然而,此做法只在调试程序时有用。很少有人在调试程序之外的场合用上述 “方法调试技术” 来永久改动某个类的功能。不能仅仅因为 Objective-C 语言里有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护。

八、提供“全能初始化方法”

详细解析链接:[http://www.jianshu.com/p/e11c6366c2bd]

提供必要信息的初始化方法。

eg.

@interface YXRectangle : NSObject
@property (nonatomic,readonly) float width;
@property (nonatomic,readonly) float height;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
@end

@implementation YXRectangle
//全能初始化方法
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height
{
    self = [super init];
    if (self) {
        _width = width;
        _height = height;
    }
    return self;
}
@end 

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height这个方法就是全能初始化方法。其他的初始化方法都应该调用这个方法来创建对象。

  • 在类中提供一个全能初始化方法,并于文档中说明,其他方法都应该调用此方法
  • 若全能初始化方法与父类不同,则需覆写超类中的对应方法
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类的方法,并在其中抛出异常

九、实现description方法

NSLog打印自定义对象的时候,打印出来的事description方法返回的NSString,实现description方法可以按自己要求使NSLog打印出需要的数据。

- (NSString*)description {
    return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", 
            [self class], self, _firstName, _lastName];
}

十、理解NSCopying协议

详细内容:http://www.jianshu.com/p/4346a02d078f

使用对象时经常需要拷贝它。在Objective-C中,此操作通过copy方法完成。如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone
为何会出现NSZone呢?因为以前开发程序时,会据此把内存分成不同的"区"(zone),而对象会创建在某个区里面。现在不用了,每个程序只有一个区:"默认区"(default zone)。所以说,尽管必须实现这个方法,但是你不必担心其中的zone参数。
copy方法由NSObject实现,该方法只是以"默认区"为参数来调用"copyWithZone:"。我们总是想覆写copy方法,其实真正需要实现的却是"copyWithZone:"方法,这个问题大家一定要注意。
若想使某个类支持拷贝功能,只需声明该类遵从NSCopying协议,并实现其中的那个方法即可。比方说,有个表示个人信息的类,可以在其接口定义中声明此类遵从NSCopying协议:

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;

- (id)initWithFirstName:(NSString*)firstName 
            andLastName:(NSString*)lastName;
@end

然后,实现协议中规定的方法:

- (id)copyWithZone:(NSZone*)zone {
    Person *copy = [[[self class] allocWithZone:zone] 
                    initWithFirstName:_firstName 
                          andLastName:_lastName];
    return copy;
}
  • 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议
  • 如果自定义的对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议
  • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝
  • 如果你写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法

十一、使用分类(类别)

如果您需要将一个方法添加到现有的类中,那么可能添加功能来使自己的应用程序更容易做某些事情,最简单的方法就是使用一个类别。

声明类别的语法使用@interface关键字,就像标准的Objective-C类描述一样,但不表示从子类继承任何继承。相反,它在括号中指定类别的名称,如下所示:

@interface ClassName(CategoryName)
@end

即使您没有原始的实施源代码(例如标准的Cocoa或Cocoa Touch类),也可以为任何类声明一个类别。您在类别中声明的任何方法将可用于原始类的所有实例以及原始类的任何子类。在运行时,类别添加的方法与由原始类实现的方法之间没有区别。
e.g..

#import“XYZPerson.h”
 @interface XYZPerson(XYZPersonNameDisplayAdditions)
 - (NSString *)lastNameFirstNameString;
@end 

在这个例子中,XYZPersonNameDisplayAdditions类别声明一个额外的方法来返回必要的字符串。
即使类中添加的任何方法都可用于类及其子类的所有实例,您需要在要使用其他方法的任何源代码文件中导入类别头文件,否则将遇到编译器警告和错误。

类别实现可能如下所示:
#import“XYZPerson + XYZPersonNameDisplayAdditions.h”
@implementation XYZPerson(XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    返回[NSString stringWithFormat:@“%@,%@”,self.lastName,self.firstName];
}
@end

一旦声明了类别并实现了这些方法,就可以使用任何类的任何实例,就像它们是原始类接口的一部分一样:

除了向现有类添加方法之外,还可以使用类别将多个源代码文件中的复杂类的实现进行拆分。例如,如果几何计算,颜色和渐变等特别复杂,您可能会将自定义用户界面元素的绘图代码放在单独的文件中。或者,您可以为类别方法提供不同的实现,具体取决于您是否为OS X或iOS编写应用程序。

类别可以用于声明实例方法或类方法,但通常不适合声明其他属性。在类别界面中包含属性声明是有效的语法,但不可能在类别中声明其他实例变量。这意味着编译器不会合成任何实例变量,也不会合成任何属性访问器方法。您可以在类别实现中编写自己的访问器方法,但是您将无法跟踪该属性的值,除非原始类已经存储了该值。

通过关联使用类别实现添加属性
#import <Foundation/Foundation.h>
@interface UIScrollView (BTCustom)
@property (assign, nonatomic) CGFloat oldContentOffsetY;
@end 

#import "UIScrollView+BTCustom.h"
#import <objc/runtime.h>
static NSString * const OldContentOffsetY = @"OldContentOffsetY";
@implementation UIScrollView (BTCustom)
@dynamic oldContentOffsetY;
- (void)setOldContentOffsetY:(CGFloat)oldContentOffsetY
{
    objc_setAssociatedObject(self, (__bridge const void *)(OldContentOffsetY), @(oldContentOffsetY), OBJC_ASSOCIATION_COPY);
}
- (CGFloat)oldContentOffsetY
{
    return [objc_getAssociatedObject(self, (__bridge const void *)(OldContentOffsetY)) floatValue];
}
@end

十二、GCD

dispatch_sync将一个块提交到调度队列以进行同步执行。不同的是dispatch_async,在块完成之前,此函数不会返回。调用此功能并定位当前队列会导致死锁。队列中的任务是串行执行还是并发执行,由dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);中的attr参数决定。指定DISPATCH_QUEUE_SERIAL(或NULL)创建串行队列或指定DISPATCH_QUEUE_CONCURRENT创建并发队列。
Global Dispatch Queue即Concurrent Dispatch Queue多个现场并行执行

十三、ARC

ARC只负责管理Objective-C对象的内容。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。

十四、用“僵尸对象”调试内存管理问题

大家都知道,向业已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。

所幸Cocoa提供了“僵尸对象”(Zoombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

十五、块(block)

这意味着如果您在定义块的时间和调用时间之间更改变量的外部值,如下所示:

int anInteger = 42;
void(^ testBlock)(void)= ^ {
NSLog(@“Integer is:%i”,anInteger);
};
anInteger = 84;
testBlock();

该块捕获的值不受影响。这意味着日志输出仍将显示:

整数是:42

这也意味着块不能更改原始变量的值,甚至不能更改捕获的值(它被捕获为const变量)。

全局块、栈块及堆块

定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。例如,下面这段代码就有危险:

void (^block)();
    if (/*some condition */) {
        block = ^{
            NSLog(@"Block A");
        };
    }
    else {
        block = ^{
            NSLog(@"Block B");
        };
    }
    block();
使用__block变量共享存储

如果需要能够从块中更改捕获变量的值,则可以使用__block原始变量声明中的存储类型修饰符。
这意味着变量存在于原始变量的词汇范围和在该范围内声明的任何块之间共享的存储中。

例如,您可能会重写上述示例,如下所示:

__block int anInteger = 42;
void(^ testBlock)(void)= ^ {
NSLog(@“Integer is:%i”,anInteger);
};
anInteger = 84;
testBlock();

因为anInteger被声明为一个__block变量,它的存储与块声明共享。这意味着日志输出现在将显示:

Integer is:84

这也意味着块可以修改原始值,如下所示:

__block int anInteger = 42;
void(^ testBlock)(void)= ^ {
NSLog(@“Integer is:%i”,anInteger);
anInteger = 100;
};
testBlock();
NSLog(@“原始变量的值现在是:%i”,anInteger);

这一次,输出将显示:

Integer is:42
原始变量的值现在为:100

十六、用“僵尸对象”调试内存管理问题

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容