《Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法》
在看这本书的时候,对每一个章节里面的例子,都有一一去验证,并对一些验证记录在笔记。
在项目中很多场景下都有尝试去用,效果还是比较可观的。
只记录有用的。
1 -> Objective-C的特性
Objective-C:消息结构语言,运行时所执行的代码由运行时环境决定;
C++:使用函数调用的语言,由编译器决定。
小结:
Objective-C为C语言添加了面向对象特性,是其超集;Objective-C使用动态绑定的消息结构,在运行时才会检查对象类型,接收到消息后,执行代码,由运行期的环境而非编译器来决定。
2 -> 在类的头文件中尽量少引用其它头文件
#import<Foundation/Foundation.h>
#import“JDSEmployer.h” (这样会一并引入该文件的所有的内容,增加编译时间)
@class JDSEmployer; (“向前申明”):可以节省编译时间,将引入头文件的时间尽量延后,只在确实需要时才引入;降低相互依赖问题;向前申明也可以解决两个类相互引用的问题
#include "JDSPerson.h"
@interfaceJDSPerson :NSObject
@property(nonatomic,copy)NSString*firstName;
@property(nonatomic,copy)NSString*lastName;
@property(nonatomic,strong)JDSEmployer*employer;
@end
小结:
1、必要的情况下,才引用头文件;一般,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件,来降低类之间的耦合。
2、无法适用向前声明时,比如要声明某个类遵循的协议,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
3 -> 多用字面量语法,少用与之等价的方法
NSNumber*someNumber = [NSNumbernumberWithInt:1];
NSArray*animals = [NSArrayarrayWithObjects:@"cat",@"dog",nil];
使用下面的字面量语法代码更整洁。
NSArray*animals =@[@"cat",@"dog"];NSNumber*someNumber =@1; idobj1 =@"1"; idobj2 =nil; idobj3 =@"3"; NSArray*arrayA = [NSArrayarrayWithObjects:obj1,obj2,obj3,nil];
这种方法不会奔溃,一次处理到nil时就会结束
NSLog(@"%@",arrayA);打印:(1)
NSArray*arrayB =@[obj1,obj2,obj3];
这种方法会抛出异常 NSLog(@"%@",arrayB);
打印:*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]'
小结:
1、应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
2、应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
3、用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
4- >多用类型常量,少用 #define 预处理指令
static const NSTimeIntervalkAnimationDuration =0.3;
试图修改 这个常量,编译就会报错
不加static:另一个文件声明同样的变量名,编译器会报错:
duplicate symbol _kAnimationDuration in:
EOCAnimatedView.o
EOCOtherView.o
如果一个变量既声明为static,又声明为const,那么编译器根本不会创建符号,而是会像#define预处理指令一样,把所有遇到的变量都替换为常值。不过还是要记住:用这种方式定义的常量带有类型信息。
.m实现文件中这样去声明一个变量:NSString *const EOCStringConstant = @“VALUE”,那么需要给外部文件使用的话,这样写:extern NSString *const EOCStringConstant;
.h extern NSString*constnotyfication;
.m NSString *constnotyfication =@"notyfication";
上面的例子可以写为:
// EOCAnimatedView.h
extern const NSTimeInterval EOCAnimatedViewAnimationDuration;
// EOCAnimatedView.m
const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3;
小结:
1、不要用#define定义常量,只是代码替换,并没有减少代码量,有人重新定义此常量编译器也不会报错,导致常量值出错。
2、在实现文件中使用static const来定义“只在编译单元内可见的常量”(translation-unit-specific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀,在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
3、const 不可变原则 :其右边的总不能被修改,能提高代码的安全性,降低程序员的沟通成本。
5 -> 用枚举表示选项、状态码
typedefNS_ENUM(NSUInteger, JSConnectionState) {
JSConnectionStateDisconnected,
JSConnectionStatingConnecting,
JSDisConnectionStatedConnected,
};
用命名描述清楚,状态值含义;
用枚举作为switch 参数;可以不用写default分支:如上图所示,在枚举中添加的新的值而又未被使用就会发出警告
小结:
1、应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起一个易懂的名字。
2、多个枚举状态可以同时使用,应该将选项值定义为2的冥,以便通过通过按位或操作将其组合起来
3、不需要枚举值互相组合用NS_ENUM, 如果需要互相组合用NS_OPTIONS,NS_ENUM、NS_OPTIONS:需要指明其底层数据结构,可以确保是程序员想要的数据类型,而不是编译器所选的类型。
4、处理switch放语句,不需要实现default分支,加入新的枚举值后,编译器会报错:switch并为处理所有枚举。
6 -> 理解“属性”这一概念
1、原子性
atomic:原子属性,默认就是atomic。需要消耗大量的资源。
nonatomic:非原子属性。适合内存小的移动设备。
默认情况下,由编译器所合成(synthesize)的方法会通过锁定机制来确保它是原子的(atomic)。如果属性具有nonatomic特质,则不使用同步锁。
需要注意的是,开发iOS程序时一般都会使用nonatomic属性,这是因为在ios中使用同步锁的开销较大,这会带来性能问题。但是在Mac OS X中,使用atomic属性通常都不会有性能瓶颈。
一般情况下,并不要求属性必须是原子的,因为这并不能保证“线程安全”,若要实现线程安全,需要更为深层的锁定机制才行。例如一个线程要连续多次读取某属性的过程中,另一个线程对该属性值进行了修改,那么即便将属性声明为atomic,还是会读到不同的属性值。
2、读/写权限
readwirte:表明属性具有读取和设置方法。
readonly: 表明属性只拥有读取方法
若属性由@synthesize实现,则编译器才会自动合成与其读写权限相关的方法。
3、内存管理语义
内存管理语义仅会影响属性的“设置方法”。编译器在合成存取方法时,要根据此特质来决定所生成的代码。如果自己来编写存取方法,那么就必须与相关属性所声明的特质相符。
assign:只执行针对“纯量类型”(scalar type,例如CGFloat或NSInteger等)的简单赋值操作。
strong:表明该属性定义了一种拥有关系。为该属性设置新值时,会先保留新值,并释放旧值,最后再设置新值。
weak:表明该属性定义了一种非拥有关系。为该属性设置新值时,既不保留新值,也不释放旧值。此特质跟assign类似,然而在属性所指对象被销毁时,该属性也会清空(nil out)。
unsafe-unretained:此特质的语义跟assign相同,但它适用于对象类型。表明该属性定义了一种非拥有关系,在属性所指对象被销毁时,该属性不会自动清空,这点跟weak不同。
copy:此特质所表达的所属关系跟strong类似。然而设置方法并不保留新值,而是将其拷贝。只要实现属性所用的对象是可变的(mutable),就应该在设置新属性值时拷贝一份。当属性类型为NSString*时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能是NSMutableString类型的实例。
4、方法名
可通过如下特质来指定存取方法的方法名。
getter=:指定“获取方法”的方法名。
例如:@property(nonatomic,getter=isOn)BOOLon;
setter=:指定“设置方法”的方法名,这种用法不太常见。
小结:
1、通过“特质”来指定存储数据所需的正确语义。
2、开发 iOS 程序时应使用 nonatomic 属性,因为 atomic 属性会严重影响性能。
7 -> 在对象内部尽量直接访问实例变量
NSString*oldName1 =self.firstName; //通过属性访问
NSString*oldName2 = _firstName; // 直接访问
小结:
1、在对象内部读取数据时,应该直接访问实例变量来读,写入数据时,则应该通过属性来写
2、在初始化方法和 dealloc方法中,总是应该直接通过实例变量来读写数据
3、有时会使用惰性初始化技术配置某分数据,那么必须通过属性来读取数据
4、直接访问实例变量,不会掉用“设置方法”,内存管理语义就没用了,相同不会触发“键值对-KVO”
8 -> 理解“对象等同性”这一概念
比较对象的等同性是一个非常有用的功能。不过按照==操作符比较的是比较两个对象的指针地址,而不是其所指的对象。应该使用NSObject协议中的isEqual方法来判断两个对象的等同性。NSObject类对isEqual方法的默认实现是当且仅当两个对象的指针值相等时,才判定这两个对象相等,这时hash方法返回的值也必须相等。
例如:
- (BOOL)isEqual:(id)object{
if([selfclass] == [objectclass]) {
return[selfisEqualToPerson:(JDSPerson*)object];
}else{
return[superisEqual:object];
}
returnNO;
}
- (BOOL)isEqualToPerson:(JDSPerson*)otherPerson{
if(self== otherPerson) {
returnYES;
}
if([_firstNameisEqualToString:otherPerson.firstName] &&. [_lastNameisEqualToString:otherPerson.lastName] &&_age!=otherPerson.age) {
returnYES;
}else{
returnNO;
}
}
计算hash值的方法可实现如下,这样既能保持高效率,又能使生成的hash码至少落在一定范围之内,不会频繁重复:
-(NSUInteger)hash{
NSIntegerfistNameHash = [_firstNamehash];
NSIntegerlastNameHash = [_lastNamehash];
NSIntegerageHash =_age;
returnfistNameHash^lastNameHash^ageHash;
}
小结:
1、若要检查对象的等同性,请提供isEqual和hash方法。
2、相同的对象必须具有相同的hash码,但拥有相同hash码的对象却不一定相同。
3、不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
4、编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
9 -> 以“类族模式”,隐藏实现细节
在Cocoa中,许多类实际上是以类簇的方式实现的,即它们是一群隐藏在通用接口之下的与实现相关的类。例如创建NSString对象时,实际上获得的可能是NSLiteralString、NSCFString、NSSimpleCString、NSBallOfString或者其他未写入文档的与实现相关的对象。
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance,
};
@interfaceEOCEmployee :NSObject
@property(copy)NSString*name;
@propertyNSUIntegersalary;
// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
// Make Employees do their respective day's work
- (void)doADaysWork;
@end
@implementationEOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
switch(type) {
caseEOCEmployeeTypeDeveloper:
return[EOCEmployeeDeveloper new];
break;
caseEOCEmployeeTypeDesigner:
return[EOCEmployeeDesigner new];
break;
caseEOCEmployeeTypeFinance:
return[EOCEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
// Subclasses implement this.
}
@end
类簇的实体子类的实现示例:
@interfaceEOCEmployeeDeveloper : EOCEmployee
@end
@implementationEOCEmployeeDeveloper
- (void)doADaysWork {
[selfwriteCode];
}
@end
判断某对象是否位于类簇中,不要直接检测两个“类对象”(class)是否相等,而应该使用类型信息查询方法:
idmaybeAnArray =/* ... */;
if([maybeAnArray isKindOfClass:[NSArray class]]) {
// Will be hit
}
小结:
1、类簇模式可以把实现细节隐藏在一套简单的公共接口后面。
2、系统框架中经常使用类簇。
3、从类簇的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
10 -> 在既有类中使用关联对象存放自定义数据
管理关联对象的方法有:
// Sets up an association of object to value with the given key and policy.
voidobjc_setAssociatedObject(idobject,void*key,idvalue,objc_AssociationPolicy policy)
// Retrieves the value for the association on object with the given key.
idobjc_getAssociatedObject(idobject,void*key)
// Removes all associations against object.
voidobjc_removeAssociatedObjects(idobject)
🌰:
- (IBAction)button:(id)sender {
UIAlertView*alertView = [[UIAlertViewalloc]initWithTitle:@"action"message:@"ceshieryi"delegate:selfcancelButtonTitle:@"cancel"otherButtonTitles:@"continue",nil];
void(^block)(NSInteger)=^(NSIntegerbutSelected){
if(butSelected ==0) {
NSLog(@"cancel");
}else{
NSLog(@"continue");
}
};
objc_setAssociatedObject(alertView,alertViewKey, block,OBJC_ASSOCIATION_COPY);
[alertViewshow];
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
void(^blockclick)(NSInteger) = objc_getAssociatedObject(alertView, alertViewKey);
blockclick(buttonIndex);
}
这样的好处是可以把实现方法跟调用的位置放在一起,代码看起来方便!!!
小结:
1、可以通过“关联对象”(associated objects)机制将两个对象连起来。
2、定义关联对象时可指定内存管理语义(OBJC_ASSOCIATION_COPY),用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”
3、只有在其它做法不可行时才应选用关联对象,因为这种做法通常会引入难查找的bug。
11、- > 理解obj_msgSend 的作用
C语言中有静态绑定和动态绑定两种函数调用方式。Objective-C作为c语言的超集,向对象发送消息时使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而在对象收到消息后究竟调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
C语言是静态绑定
#import
voidprintfHello(){
printf("hello");
}
voidprintfGoodBey(){
printf("hello");
}
voiddoTheThing(inttype){
if(type ==0) {
printfHello();
}else{
printfGoodBey();
}
}
OC是动态绑定
#import
voidprintfHello(){
printf("hello");
}
voidprintfGoodBey(){
printf("GoodBey");
}
voiddoTheThing(inttype){
void(*fun)();
if(type ==0) {
fun =printfHello;
}else{
fun =printfGoodBey;
}
fun();
}
objc_msgSend函数会根据接收者与选择子的类型来调用适当的方法。为了完成此操作,该函数需要在接收者所属的类中搜寻其“方法列表”,如果找到与选择子名称相符的方法,就跳转到其实现代码;否则即沿着继承体系继续向上查找;如果仍未找到,就执行“消息转发”(message forwarding)操作。
objc_msgSend调用方法有个优化操作。它会将匹配结果缓存在“快速映射表”(fast map)里。每个类都有这样一块缓存,若是稍后还发送相同的消息,就会加快执行效率。
前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由Objective-C运行环境中的另外一些函数来处理:
objc_msgSend_stret 待发送的消息要返回结构体时用
objc_msgSend_fpret 待发送的消息返回浮点类型时用
objc_msgSendSuper 如果给超类发消息时用
小结:
1、消息由接收者、选择子和参数构成。给某对象“发送消息”(invoke a message),相当于在该对象上“调用方法”(call a method)
2、发送给某对象的全部消息都要经过“动态消息派发机制”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
12 -> 理解消息转发机制
动态方法解析: 向当前对象的所属类发送resolveInstanceMethod:(针对实例方法)或resolveClassMethod(针对类方法)消息,检查是否动态向该类添加了方法。使用此方案的前提是:相关的实现代码已经写好,只等着运行时直接插在类中。此方案常用来实现@dynamic属性
#import"JDSPerson.h"
#import
@interfaceJDSPerson()
@property(nonatomic,strong)NSMutableDictionary*backingstore;
@end
@implementationJDSPerson
@dynamicfirstName,lastName;
- (instancetype)init
{
self= [superinit];
if(self) {
if(!_backingstore) {
_backingstore= [NSMutableDictionarynew];
}
}
returnself;
}
关键在于resolveInstanceMethod:方法的实现代码:
/**
* 如果尚未实现的方法是实例方法,则调用此函数
*
* @param selector未处理的方法
*
* @return返回布尔值,表示是否能新增实例方法用以处理selector
*/
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString*selectorstring =NSStringFromSelector(sel);
if([selectorstringhasPrefix:@"set"]) {
// 添加 setter 方法
class_addMethod(self, sel, (IMP)autoDictionarysetter,"v@:@");
}else{
// 添加 getter 方法
class_addMethod(self, sel, (IMP)autodictionaryGetter,"@@:");
}
returnYES;
}
/**
* 如果尚未实现的方法是类方法,则调用此函数
*
* @param selector未处理的方法
*
* @return返回布尔值,表示是否能新增类方法用以处理selector
*/
//+ (BOOL)resolveClassMethod:(SEL)selector;
voidautoDictionarysetter(idself,SEL_cmd,idvalue){
JDSPerson*typeSelf = (JDSPerson*)self;
NSMutableDictionary*backingstore = typeSelf.backingstore;
NSString*selectorString =NSStringFromSelector(_cmd);
NSMutableString*key = [selectorStringmutableCopy];
//delete @":"
[keydeleteCharactersInRange:NSMakeRange(key.length-1,1)];
//delete @"set"
[keydeleteCharactersInRange:NSMakeRange(0,3)];
NSString*lowercasefirstchar = [[keysubstringToIndex:1]lowercaseString];
[keyreplaceCharactersInRange:NSMakeRange(0,1)withString:lowercasefirstchar];
if(value) {
[backingstoresetObject:valueforKey:key];
}else{
[backingstoreremoveObjectForKey:key];
}
}
idautodictionaryGetter(idself,SEL_cmd){
JDSPerson*typeSelf = (JDSPerson*)self;
NSMutableDictionary*backingstore = typeSelf.backingstore;
NSString*key =NSStringFromSelector(_cmd);
NSString*value = [backingstoreobjectForKey:key];
return[backingstoreobjectForKey:key];
}
@end
- (void)viewDidLoad {
[superviewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
JDSPerson*person = [JDSPersonnew];
person.firstName=@"jds";
NSLog(@"%@",person.firstName);
//输出:jds
}
/**
* 此方法询问是否能将消息转给其他接收者来处理
*
* @param aSelector未处理的方法
*
* @return如果当前接收者能找到备援对象,就将其返回;否则返回nil;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector;
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
标准消息转发: 经过上述两步之后,如果还是无法处理选择子,则启动完整的消息转发机制。我们需要重写methodSignatureForSelector:和forwardInvocation:实例方法。runtime发送 methodSignatureForSelector:消息获取选择子对应的方法签名,即参数与返回值的类型信息。runtime则根据方法签名创建描述该消息的NSInvocation,以创建的NSInvocation对象作为参数,向当前对象发送forwardInvocation:消息。forwardInvocation:方法定位能够响应封装在此NSInvocation中的消息的对象。此NSInvocation对象将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。在这个方法中我们可以实现一些更复杂的功能,可对消息内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息,只抛出异常导致程序退出
/**
* 消息派发系统通过此方法,将消息派发给目标对象
*
* @param anInvocation之前创建的NSInvocation实例对象,用于装载有关消息的所有内容
*/
- (void)forwardInvocation:(NSInvocation*)anInvocation;
这个方法可以实现的很简单,通过改变调用的目标对象,使得消息在新目标对象上得以调用即可。然而这样实现的效果与 备援接收者 差不多,所以很少人会这么做。更加有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数、修改 selector 等等。
小结:
1、若对象无法响应某个选择子,则进入消息转发流程
2、通过运行期的动态方法解析功能,可以在需要用到某个地方时再将其加入类中
3、对象可以将其无法解读的某些选择子转交给其他对象来处理
4、经过上述两步后还是没办法处理选择子,就启动完整的消息转发机制。
13 -> 用“方法调试技术”调试“黑盒方法”
1、用运行时对特性获取两个方法的实现,然后进行交换
- (void)viewDidLoad {
[superviewDidLoad];
MethodoriginalMethod =class_getInstanceMethod([selfclass],@selector(greenBtn:));
MethodswappedMethod =class_getInstanceMethod([selfclass],@selector(yellowBtn:));
method_exchangeImplementations(originalMethod, swappedMethod);
}
- (IBAction)greenBtn:(id)sender {
self.view.backgroundColor= [UIColorgreenColor];
}
- (IBAction)yellowBtn:(id)sender {
self.view.backgroundColor= [UIColoryellowColor];
}
2、实现dealloc打印
- (void)viewDidLoad {
[superviewDidLoad];
MethodoriginalMethod =class_getInstanceMethod([selfclass],NSSelectorFromString(@"dealloc"));
MethodswappedMethod =class_getInstanceMethod([selfclass],@selector(swappedDealloc));
method_exchangeImplementations(originalMethod, swappedMethod);
}
- (void)swappedDealloc{
NSLog(@"%@:dealloc",NSStringFromClass([selfclass]));
[selfswappedDealloc];
⚠️⚠️⚠️看起来像是进入了无限递归,因为交换了方法,这里实际是在执行:dealloc 这个方法
}
- (IBAction)back:(id)sender {
[selfdismissViewControllerAnimated:YEScompletion:nil];
}
小结:
1、在运行期,可以向类中新增或替换方法实现
2、例如第二个🌰一样用一个新的实现方法替换原来的实现,可以在原有的实现中加入新的功能
3、⚠️⚠️⚠️不宜滥用
4、http://www.cocoachina.com/ios/20150911/13260.html 可以解决button重复点击
http://www.jianshu.com/p/e4f1fb537af9
14 -> 理解“类对象”的用意
“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,“内省”),这个强大有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都遵从此协议。在程序中,不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
类型信息查询方法包括isMemberOfClass:(判断对象是否为某个特定类的实例),isKindOfClass:(判断对象是否为某类或其派生类的实例)。像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系里游走。
另外一种可精确判断出对象是否为某类实例的办法是:
idobject =/* ... */;
if([object class] == [EOCSomeClass class]){
// 'object' is an instance of EOCSomeClass
}
即使这样,应尽量使用类型信息查询方法,而不应直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息转发机制的对象。比如,某对象可能会把它收到的所有选择子都转发给另外一个对象。这样的对象叫做代理,此种对象均以NSProxy为根类。
isMemberOfClass: 能够判断出对象是否为某个特定类的实例。
isKindOfClass: 能够判断出对象是否为某类或其派生类的实例。
NSMutableDictionary*dic = [NSMutableDictionarynew];
[dicisMemberOfClass:[NSDictionaryclass]];//no
[dicisMemberOfClass:[NSMutableDictionaryclass]];//yes
[dicisKindOfClass:[NSDictionaryclass]];//yes
[dic isKindOfClass:[NSArrayclass]];//no
小结:
1、每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
2、如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知
3、尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
15 -> 用前缀避免命名空间冲突
Objective-C没有其他语言哪种内置的命名空间(namespace)机制。
避免命名冲突的唯一办法就是变相实现命名空间:为所有名称都加上适当的前缀。
创建应用程序时一定要注意:Apple宣称其保留使用所有“两字母前缀”的权利,所以你自己选用的前缀应该是三个字母或者更多。
这么做还有一个好处:如果此符号出现在栈回溯信息中,则很容易就能判明问题源自哪块代码。
小结:
1、选择与你的公司、应用程序或者二者皆有关联之名作为类名的前缀,并在所有代码中均使用这一前缀。
2、若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
16 -> “提供全能初始化方法”
我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)。
如果创建类的实例的方式不止一种,那么这个类就会有多个初始化方法。
这当然会很好,不过仍然要在其中选定一个作为“全能初始化方法”,令其他初始化方法都来调用它。
于是,只有在全能初始化方法中,才会存储内部数据。
这样的话,当底层数据存储机制改变时,只需要修改此方法的代码就好,无需改动其他初始化方法。
类继承时需要注意的一个重要问题:如果子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法。每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,然后再执行与本类有关的任务。
如不用:就抛出异常
- (instancetype)initWithWidth:(float)wigth
{
@throw[NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithFrame: insteat"userInfo:nil];
}
小结:
1、 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
2、若全能初始化方法与超类不同,则需覆写超类中的对应方法。
3、如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
17 -> 实现description 方法
覆写description方法,否则打印信息时,就会调用NSObject类所实现的默认方法。
- (NSString*)description
{
return[NSStringstringWithFormat:@"<%@:%p:%@>",[selfclass],self,_lastName];
}
用LLDB “op” 完成打印
小结:
1、 实现description方法返回一个有意义的字符串,用以描述该实例。
2、若想在调式时打印出更详尽的对象描述信息,则应实现debugDescription方法。
18 -> 尽量使用不可变对象
一般情况下,对外公开的接口一般声明为readOnly,也可以在类内部实现重新声明为readwrite,这样可以在类内部修改参数,在类的实现代码内部设置这些属性了。
写一个类别去改写属性
小结:
1、尽量创建不可变的对象。
2、若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性。
3、不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
19 -> 使用清晰而协调的命名方法
主要了解命名规范,OC的命名和其他语言比较起来,OC方法写起来比较长,但更像是一句通熟易懂的话。
小结:
1、起名时应遵循标准的OC规范,这样的接口更容易让开发者读懂
2、方法名要言简意赅,从左至右读起来像日常语句一样
3、方法名不要使用缩略后的类型名称
4、必须保证方法名的风格与自己的代码或所集成的框架相符
20 -> 为私有方法名加前缀
- (void)publicMethod{
}
私有方法可以加前缀,区分开来
-(void)p_privateMethod{
}
小结:
1、私有方法加上前缀,这样很容将公共方法与其区分开来
2、不能使用单一的下划线做私有方法的前缀,这种做法是预留给苹果公司的的
21 -> 理解OC的错误模型
- (void)publicMethod{
idsomerResource =/* */;
if(/*check for error*/) {
@throw[NSExceptionexceptionWithName:@"exceptionName"
reason:@"There was as error"
userInfo:nil];
}
[somerResource doSomething];
[somerResourcerelease];
}
抛出异常的方式不要轻易使用,这样会造成内存泄漏,只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常
NSError
Error domain (错误范围,其类型为字符串)
Error code (错误代码,其类型为整型)
User info (用户信息,其类型为字典)
小结:
1、只有发生了可使整个程序奔溃的严重错误时,才跑出异常
2、错误不严重的时候可以指派“委托方法”(delegate)来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。
22 -> 理解NSCoping协议
@interfacePerson :NSObject
@property(nonatomic,copy)NSString*name;
@property(nonatomic,copy)NSString*image;
@property(nonatomic,copy)NSString*total;
@property(nonatomic,copy)NSString*name_enabled;
-(id)copyWithZone:(NSZone*)zone
{
Person*person = [self.classallocWithZone:zone];
person.name= [self.namecopyWithZone:zone];
person.image= [self.imagecopyWithZone:zone];
person.total= [self.totalcopyWithZone:zone];
person.name_enabled= [self.name_enabledcopyWithZone:zone];
returnperson;
}
只有遵守的以上两个协议,才能进行拷贝和对象序列化的保存
对象数组序列化:
- (void)writeDataWithArray:(NSArray*)array andName:(NSString*)name{
NSData*boadData = [NSKeyedArchiverarchivedDataWithRootObject:array];
[[NSUserDefaultsstandardUserDefaults]setObject:boadDataforKey:name];
[[NSUserDefaultsstandardUserDefaults]synchronize];
}
- (NSArray*)getDataWithIdentifier:(NSString*)name
{
NSData*boardData = [[NSUserDefaultsstandardUserDefaults]objectForKey:name];
return[NSKeyedUnarchiverunarchiveObjectWithData:boardData];
}
小结:
1、对象需要拷贝,必须遵守NSCopying协议
2、对象分为可变和不可变版本,必须同时遵守NSCopying MutableNSCopying协议
3、一般情况下执行浅拷贝:浅拷贝是“影子”,深拷贝是“克隆人”;
4、对象需要深拷贝,可以考虑新增一个专门执行深拷贝的方法
23 -> 通过委托与数据协议进行对象间通信
#import
@classJDNetworkingFetcher;
@protocolJDNetworkingFetcherDelegate
@required //代理必须要实现的方法
- (void)netWorkFetcher:(JDNetworkingFetcher*)fetcher didReceiveData:(NSData*)data;
@optional //可供代理按需实现此方法 也叫做“可选方法”
- (void)netWorkFetcher:(JDNetworkingFetcher*)fetcher didFailWithError:(NSError*)error;
@end
@interfaceJDNetworkingFetcher :NSObject
@property(nonatomic,weak)iddelegate;
@end
如果委托者执行可选方法,那么必须提前使用类型信息查询方法,判断这个代理能否选用相关选择子;这样做的好处是可以避免,没有实现相关法方法而导致程序的奔溃:
#import"JDNetworkingFetcher.h"
@implementationJDNetworkingFetcher
- (void)didRequestData:(NSData*)receiveData{
NSData*data = receiveData;
if([_delegaterespondsToSelector:@selector(netWorkFetcher:didReceiveData:)]) {
[_delegatenetWorkFetcher:selfdidReceiveData:data];
}
}
BTW:
@optional
//新增很多可选的方法,会频繁执行以上方法。那么除了第一次是有效的,后面其实再执行就多余了,因此可以把某个协议的信息缓存起来,优化效率
- (void)netWorkFetcher:(JDNetworkingFetcher*)fetcher didUpDataProgress:(float)progress;
.
.
.
这就比较频繁了 ,得优化。。。
@end
定义一个结构体
@interfaceJDNetworkingFetcher :NSObject
{
structdata{
unsignedintfieldA:8;// 8个二进制位 可以表示0到255之间
unsignedintfieldB:4;// 4个二进制位
unsignedintfieldC:2;// 2个二进制位
unsignedintfieldD:1;// 1个二进制位 可以表示0或者1两个值
};
//C语言的特性:"位段"数据类型
}
#import"JDNetworkingFetcher.h"
@interfaceJDNetworkingFetcher()
{
struct{
unsignedintdidReceiveData :1;
unsignedintdidFailWithError :1;
unsignedintdidUploadProgress :1;
}delegateFlags;
}
@end
@implementationJDNetworkingFetcher
-(void)setDelegate:(id)delegate{
_delegate= delegate;
_delegateFlags.didReceiveData= [_delegaterespondsToSelector:@selector(netWorkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError= [_delegaterespondsToSelector:@selector(netWorkFetcher:didFailWithError:)];
_delegateFlags.didUploadProgress= [_delegaterespondsToSelector:@selector(netWorkFetcher:didUploadProgress:)];
}
//缓存委托者是否能响应选择子,缓存功能的实现下载set方法内,这样每次调用delegate相关方法,就直接查询结构体的的标志位
if(_delegateFlags.didReceiveData) {
[_delegatenetWorkFetcher:selfdidReceiveData:receiveData];
}
小结:
1、委托模式为对象提供一套接口,使其能告知其他对象
2、委托者把接口定义成协议,在协议中把可能需要处理的事件定义成方法
3、有必要时,可实现含有段的结构体,将是否能相关协议方法这一信息缓存至其中
24 -> 将类的实现代码分散到便于管理的数个分类中
通过分类机制,把类代码分成很多个易于管理的小模块,需要在类的.h文件中引入分类 的头文件,可以按照不同的功能区分类别
#import
#import"JDSPerson+Play.h"
#import"JDSPerson+Work.h"
#import"JDSPerson+Friendship.h"
@interfaceJDSPerson :NSObject
@property(nonatomic,copy)NSString*firstName;
@property(nonatomic,copy)NSString*lastName;
- (instancetype)initWithWidth:(float)wigth;
@end
@interfaceJDSPerson (Friendship)
- (void)addFreind:(JDSPerson*)person;
@end
@interfaceJDSPerson (Work)
-(void)performDaysWork;
@end
@interfaceJDSPerson (Play)
- (void)goTosports;
@end
小结:
1、使用分类实现代码划分成易于管理的小块
2、将应该视为“私有”的方法归入名叫private的分类中,隐藏实现细节
25 -> 总是为第三方类的分类名称加前缀
小结:
1、分类命名重复,后者会覆盖前者的实现,特别是在用到第三方代码的时候,使用前缀可以减少此错误出现的概率
2、向第三方类添加分类时,总是要给其名称和方法加上专用的前缀
26 -> 勿在分类中声明属性
#import"JDSPerson.h"
@interfaceJDSPerson (Friendship)
@property(nonatomic,strong)NSArray*freinds;
@end
在分类中添加属性会报错误⚠️,可以声明变量为 @dynamicfreinds,编译器不会抱错误⚠️;
用运行时关联对象解诀不能合成实例变量的问题:
#import"JDSPerson+Friendship.h"
#import
staticchar *constkfreindsKey ="kFriendsKey";
@implementationJDSPerson (Friendship)
-(NSArray*)freinds{
returnobjc_getAssociatedObject(self,kfreindsKey);
}
-(void)setFreinds:(NSArray*)freinds{
objc_setAssociatedObject(self,
kfreindsKey,
freinds,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
小结:
1、把分装的数据所用到的全部属性都定义在主接口里
2、在分类中尽量不要定义属性
27 -> 使用分类隐藏实现细节
#import"JDSPerson+Play.h"
通过分类,定义实例变量和方法
@interfaceJDSPerson()
@property(nonatomic,strong)NSMutableDictionary*backingstore;
@property(nonatomic,readwrite,copy)NSString*addres;
@end
小结;
1、通过分类向类中新增实例变量,需要遵守的协议也可卸乳分类中
2、如果属性在主接口声明为只读,类的内部又要用设置方法修改此属性,那么可以在.m中扩展为可读写
3、把私有方法原型声明在分类里面
28 -> 通过协议提供匿名对象
@property(nonatomic,weak)iddelegate;
由于该属性是id,所以任何类的对象都可以充当这个属性,即使该类不继承自NSObject,只要遵循lykDelegate协议就可以。如果有需要,可以在运行期查出此对象所属的类型。
NSDictionary:在字典中,键的标准内存语义是“设置时拷贝”,值得内存语义是“设置时保留”。因此,可变版本中,设置键值对所用的方法是
- (void)setObject:(ObjectType)anObject forKey:(KeyType )aKey;
表示键的那个参数可以是任意类型,只要遵从NSCopying协议就行,这样,就可以向对象发送拷贝消息了。
小结:
1.协议可以提供匿名类型。具体对象类型可以淡化为id类型,协议里规定了对象应该实现的具体方法。
2.使用匿名对象来隐藏类型名称。
3.类型不重要,重要的是对象能够响应的方法,这种情况可以用匿名对象来表示。
29 -> 理解引用计数
Retain:递增保留计数
release:递减保留计数
autorelease:带稍后清理“自动释放池”时,再递减保留计数。
1、属性存储方法中的内存管理:
属性为strong 或者retain 时 会保留新值,释放旧值,然后更新实例变量,令其指向新值
-(void)setFirstName:(NSString*)firstName{
[firstNameretain];
[_firstName realease];
_firstName= firstName;
}
2、自动释放池:
调用release会立刻递减该对象的保留计数,autorelease在稍后递减计数,通常是在下一次“事件循环”时递减。
autorelease能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间
3、保留环:
相互引用多个对象,不能释放,会产生内存泄漏,通常采用弱引用解决。
小结:
1、引用计数机制通过可以递增递减的计数器来管理内存,对象创建好后,计数至少为1,降为0时对象被销毁
2、对象生命周期中,其余对象通过引用来保留或者释放对象。
30 -> 以ARC简化引用计数
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong:默认语义,保留此值。
__unsafe_unretained:不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
__weak:不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
__autoreleasing:把对象“按引用传递”(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
用了ARC之后,就不需要再编写来释放强引用的dealloc方法了。因为ARC会借用Objective-C++的一项特性来生成清理例程(cleanup routine)。回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruce的方法。而ARC则借助此特性,在该方法中生成清理内存所需代码。
不过如果有非Objective-C的对象,比如CoreFoundation中的对象或是由malloc()分配在堆中的内存,那么仍然需要清理。然而不需要像原来马羊调用超类的dealloc方法。ARC会自动在.cxx_destruct方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的dealloc方法。ARC环境下,dealloc可以这样写:
- (void)dealloc
{
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemeoryBlob);
}
小结:
1、有ARC后,正确使用成员变量修饰符就好了,不用担心内存管理问题
2、ARC只负责Objective-C的内存, CoreFoundation对象不归ARC管理,必须适时使用CFRetain/CFRelease
31 -> 在dealloc方法中只释放引用并解除监听
- (void)dealloc{
CFRelease(coreFoundationObject);
[[NSNotificationCenterdefaultCenter]removeObserver:self];
}
在dealloc中释放掉所拥有的对象,dealloc中尽量不要写其他的代码,因为某些情况下时不会执行的dealloc的,例如:循环引用了对象,那这个类就不会执行的dealloc
小结:
1、在dealloc中,应该释放指向其他对象的引用,并取消原来订阅的KVO 或着NSNotificationCenter通知,不要做其他事
2、如果对象持有文件描述符等系统资源,那么应该专门写一个方法来释放资源,约定好:用完之后必须close掉
3、执行异步的方法不能在dealloc中调用,因为此时对象已处于正在回收的状态了
32 -> 编写“异常安全代码”时留意内存管理问题
@try{
JDSPerson*newPerson = [JDSPersonnew];
[newPersonsetAddres:@"sz"];
}@catch(NSException *exception) {
}@finally{
}
这样的异常会出现内存泄漏
在开启ARC之后正常情况下一切和内存有关的申请和释放操作皆不用你关心了,ARC全全帮你包办了。但是还有极少数的情况下,编译器无法为你生成合适的ARC额外代码,比如obj-c异常就是这么一个例子。
话句话说在ARC中异常可能会导致对象的内存泄露。因为ARC是颗粒化对象为一个文件:即可以在obj-c文件上启用ARC.所以我们可以选择性的在编译某个文件上加上-fobjc-arc-exceptions选项,如果开启了该选项,则ARC会额外为异常中的对象申请和释放操作添加代码,保证异常中ARC管理的对象也不会造成内存泄露。当然这样一来缺点就是可能会生成大量平常可能根本用不到的代码。(只有发生异常才会执行)
所以我们可以只在必要的obj-c文件上启用-fobjc-arc-exceptions标志,而其他文件禁用该标志,这样才可以做到万无一失。
小结:
1、捕获异常,一定要注意将try块内存所创立的对象清理干净
2、默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率
33 -> 以弱引用避免保留环
#import<Foundation/Foundation.h>
@class JDSPerson;
@class JDSEmployer;
@interface JDSPerson :NSObject
@property(nonatomic,strong)JDSEmployer*other;
@end
@interface JDSEmployer :NSObject
@property(nonatomic,unsafe_unretained)JDSPerson*other;
@end
JDSEmployer不拥有JDSPerson,避免了循环引用; unsafe_unretained跟 assign特质等价,assign通常用于“整体类型”(int、float结构体等),unsafe_unretained多用于对象类型
小结:
1、将某些引用设为weak,可避免出现“保留环”
2、weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC引入的新特性,由运行期系统来实现。在具备自动清空的功能等弱引用上,可以随意读去其数据,因为这种引用不会指向已经会收过 的对象
34 -> 以“自动释放池块”降低内存峰值
NSMutableArray *peoples= [[NSMutableArrayalloc]init];
for(inti=0; i
@autoreleasepool{
JDSPerson*person = [JDSPersonnew];
[peoples addObject:person];
}
}
循环时,降低内存峰值,不会在执行循环的时候,内存暴涨。
小结:
1、自动释放池排布在栈中,对象受到autoreleasepool 消息后,系统将其放入最顶端端池里;
2、合理运用自动释放池,可降低内存峰值;@autoreleasepool可以创建更轻便的自动释放池
35 -> 用“僵尸对象”调试内存管理问题
设置 zombie objects ,遇到僵尸对象会抛出异常,控制台会打印改对象
小结:
1、系统在回收对象时,可以不将其真大回收,而是把它转化为僵尸对象,通过环境变量 NSZombieEnable可以开启此功能
2、系统会修改对象的isa指针,令其指向特殊的僵尸类,使其变为僵尸对象。僵尸类能响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止程序。
37 -> 理解“块”这一概念
块与函数类似,只不过是定义在另一个函数里的,和定义它的那个函数共享同一范围类的东西
块的基本用法:
__blockintresult;
void(^block)(int,int) = ^(inta,intb){
result = a+b;
};
block(6,9);
全局块、栈块、堆块:
定义块的时候,内存区域是分布在栈中的,块只在定义的范围内有效,下面的写法,等离开了相应的范围之后,编译器可能会把分配给块的内存覆写掉,if 和 else 只有一个是有效的,可能会导致奔溃
void(^block)();
if(arr.count>8) {
block = ^{
NSLog(@"blockA");
};
}else{
block = ^{
NSLog(@"blockA");
};
}
block();
解决方法:copy到堆区
void(^block)();
if(arr.count>8) {
block = [^{
NSLog(@"blockA");
}copy];
}else{
block = [^{
NSLog(@"blockA");
}copy];
}
block();
全局block:不会被系统所回收,实际相当于单利
void(^block)() = ^{
NSLog(@"global block");
};
小结:
1、块是C、C++、Objective-C 中的词法闭包。
2、块可接受参数,也可返回值
3、块可分配在栈或者堆上,也可以是全局的。分配在栈上的块可以拷贝到堆里,这样就和标准的Objective-C对象一样,具备引用计数了。
38 -> 为常用的块类型创建typedef
typedef int(^Completion)(BOOL,NSError*);
- (void)requestwithCompletionHandle:(void(^)(BOOL,NSError*))completion;
- (void)requestwithCompletionHandle:(Completion)completion;
使用typedef“类型定义”关键字给块定义一个易读懂名字,使代码读起来更顺畅;
块中参数需要更改时也可以先修改“类型定义”内的参数,后面再编译就会报错,这样就可以根据报错一个不差的全部修改到位
typedefvoid(^ACAccountStoreSaveCompletionHandler)(BOOLsuccess,NSError*error);
typedefvoid(^ACAccountStoreRequsetAccessCompletionHandler)(BOOLsuccess,NSError*error);
使用同一个签名
小结:
1、用typedef重新定义块类型,可让块变量用起来更加简单
2、定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突
3、可以为同一个块签名定义多个类型别名,如果重构代码,使用了块类型的某个别名,只需要修改相应typedef中的块签名即可
39 - >用handle块降低代码分散程度
//成功和失败一起处理:
缺点: 全部逻辑写在了一起,块的代码会很长且比较复杂
优点:1、数据请求断开时,可以处理请求到的数据,可以根据数据判断问题做适当处理。
2、调用API的代码可能会在处理成功响应的过程中发现错误,例如数据太短、某些数据为空
JDNetworkingFetcher*fetcher = [[JDNetworkingFetcheralloc]initWithURL:url];
[fetcherstartWithCompletionHandler:^(NSData*data,NSError*error) {
if(error) {
//handle faile
}else{
//handle success
}
}];
//成功和失败分别处理:代码更容易读懂,可以把处理失败或成功所用的代码省略
JDNetworkingFetcher*fetcher = [[JDNetworkingFetcheralloc]initWithURL:url];
[fetcherstartWithCompletionHandlerSuucess:^(NSData*data) {
//handle success
}failureHandle:^(NSError*error) {
//handle failure
}];
小结:
1、在创建对象时,可以使用内联的handle块将相关业务逻辑一并申明
2、在有很多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handle块来实现,可直接将快与相关对象放在一起
3、设计API用到块时,可以增加一个参数,使调用者可以通过此参数来决定应该把块安排在哪个队列上执行(自己貌似很少用到)。
40 - >用块引用其所属对象时不要出现保留环
JDNetworkingFetcher*fetcher = [[JDNetworkingFetcheralloc]initWithURL:url];
[fetcherstartWithCompletionHandlerSuucess:^(NSData*data) {
//handle success
///!!!:保留环
self.data= data;
//FIXME:解决保留环
self.fetcher=nil;
}failureHandle:^(NSError*error) {
//handle failure
}];
另一种保留环,下载完毕,保留环接触,下载对象也会被回收
JDNetworkingFetcher*fetcher = [[JDNetworkingFetcheralloc]initWithURL:url];
[fetcherstartWithCompletionHandlerSuucess:^(NSData*data) {
//handle success
///!!!:保留环
self.data= data;
}failureHandle:^(NSError*error) {
//handle failure
}];
- (void)p_requsetCompetion{
if(self.completionHandle) {
self.completionHandle(self.downloadData);
}
self.completionHandle=nil;
}
小结:
1、如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题
2、一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。
41 -> 多用派发队列少用同步锁
加同步锁,确保线程安全
-(dispatch_queue_t)syncQueue{
if(!_syncQueue) {
_syncQueue=dispatch_queue_create("com.effectiveobjectivec.sycnQueue",NULL);
}
return_syncQueue;
}
-(NSString*)firstName{
__blockNSString*firstName;
dispatch_sync(self.syncQueue, ^{
firstName =_firstName;
});
returnfirstName;
}
-(void)setFirstName:(NSString*)firstName{
dispatch_sync(self.syncQueue, ^{
_firstName = firstName;
});
}
并发队列,但是这样是随意执行的,没有顺序
-(dispatch_queue_t)syncQueue{
if(!_syncQueue) {
_syncQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
}
return_syncQueue;
}
-(NSString*)firstName{
__blockNSString*firstName;
dispatch_sync(self.syncQueue, ^{
firstName =_firstName;
});
returnfirstName;
}
-(void)setFirstName:(NSString*)firstName{
dispatch_async(self.syncQueue, ^{
_firstName = firstName;
});
}
可通过栈栏解决顺序执行的问题dispatch_barrier_async :并发队列如果发现接下来要处理的块是个栈栏块,那么就要一直等到当前所有并发块都执行完毕,才会单独执行这个栈栏块。待栈栏块执行过后,再按正常方式继续向下处理。
-(void)setFirstName:(NSString*)firstName{
dispatch_barrier_async(self.syncQueue, ^{
_firstName = firstName;
});
}
小结:
1、派发队列可用来表述同步语义,这种做法比 @synchronized 块和NSLock 对象更简单
2、将同步和异步派发结合起来,可以实现与普通加锁机制一样的同步行为,这么做不会阻塞执行异步派发的线程
3、使用同步队列及栈栏块,可以令同步行为更加高效
42 -> 多用GCD,少用performSelect 系列方法
SELselector;
if(/* some condition*/) {
selector =@selector(greenBtn:);
}else{
selector =@selector(yellowBtn:);
}
编程灵活,可用来简化代码,编译器必须到了运行期才能确定选择子是哪个
SELselector;
if(/* DISABLES CODE */(1)) {
selector =@selector(newObject:);
}else{
selector =@selector(copy);
}
idret = [object performSelector:selector];
存在内存泄漏,这段代码即使在ARC环境下编译器也不会主动去释放它
小结:
1、 performSelector 系列方法内存管理方面容易疏失,它无法确定将要执行的选择子具体是什么,ARC编译器也就无法插入适当的内存管理方法
2、performSelector 系列方法所能处理的选择子太过局限,选择子的返回值类型及发送给方法的参数个数都受限制
3、如果想把任务放到另一个线程上,最好用GCD不要用这个。
43 -> 掌握GCD及操作队列的使用时机
GCD是纯C的API,而操作队列是Objective-C的对象
GCD中,任务用块来表示,而块是个轻量级数据结构;“操作”则是个更为重量级的Objective-C的对象;
NSBlockOperation;
[queue addOperationWithBlock:^{
}];
这两个结合起来使用,与GCD类似;
使用“操作”的好处:
1、取消某个操作:
GCD无法无法取消;不过,已经执行的任务就无法取消了。
NSInvocationOperation*operation = [[NSInvocationOperationalloc]initWithTarget:selfselector:@selector(yellowBtn:)object:nil];
NSOperationQueue*queue = [[NSOperationQueuealloc]init];
[queueaddOperation:operation];
[operation cancel];
2、指定操作间的依赖关系:
一个操作可以依赖多个操作,必须在其他依赖的操作结束以后才能执行该操作
NSInvocationOperation*operation3 = [[NSInvocationOperationalloc]initWithTarget:selfselector:@selector(log)object:nil];
[operation3addDependency:operation1];
[operation3addDependency:operation2];
[queue addOperation:operation3];
注意:不能循环依赖(不能A依赖于B,B又依赖于A)
3、通过键值观察机制监控 NSOperation对象的属性 :
1)、NSOperation 很多属性都可以通过KVO监听:可以通过 isCancelled属性来判断人物是否已经取消,通过 isFinished属性判断任务是否完成.
2)、 可以监听一个操作的执行完毕
//创建对象,封装操作
NSBlockOperation*operation=[NSBlockOperationblockOperationWithBlock:^{
for(inti=0; i<10; i++) {
NSLog(@"-operation-下载图片-%@",[NSThread currentThread]);
}
}];
//监听操作的执行完毕
operation.completionBlock=^{
//.....下载图片后继续进行的操作
NSLog(@"--接着下载第二张图片--");
};
//创建队列
NSOperationQueue*queue=[[NSOperationQueue alloc]init];
//把任务添加到队列中(自动执行,自动开线程)
[queue addOperation:operation];
说明:在上一个任务执行完后,会执行operation.completionBlock=^{}代码段,且是在当前线程执行(2)。
4、并发数
(1)并发数:同时执⾏行的任务数.比如,同时开3个线程执行3个任务,并发数就是3
(2)最大并发数:同一时间最多只能执行的任务的个数。
(3)最⼤并发数的相关⽅方法
- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
说明:如果没有设置最大并发数,那么并发的个数是由系统内存和CPU决定的,可能内存多久开多一点,内存少就开少一点。
注意:num的值并不代表线程的个数,仅仅代表线程的ID。
提示:最大并发数不要乱写(5以内),不要开太多,一般以2~3为宜,因为虽然任务是在子线程进行处理的,但是cpu处理这些过多的子线程可能会影响UI,让UI变卡。
5、指定操作的优先级:
1)设置NSOperation在queue中的优先级,可以改变操作的执⾏优先级
- (NSOperationQueuePriority)queuePriority;
- (void)setQueuePriority:(NSOperationQueuePriority)p;
(2)优先级的取值
typedefNS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal =0,
NSOperationQueuePriorityHigh =4,
NSOperationQueuePriorityVeryHigh =8
};
说明:优先级高的任务,调用的几率会更大。
小结:
1、在解决多线程任务管理问题时,GCD并非唯一方案
2、操作队列提供了一套高层的Objective-C API ,能实现纯GCD所具备的绝大部分功能,而且还能完成一些复杂的操作,这些操作如果该用GCD来实现,则需要另外编写代码
44 -> 通过 Dispatch Group 机制,根据系统资源状况来执行任务
任务编组的两组方式:
1》dispatch_group_async(dispatch_group_t _Nonnullgroup, dispatch_queue_t _Nonnullqueue, ^{
})
2》dispatch_group_enter(dispatch_group_t _Nonnullgroup)
dispatch_group_leave(dispatch_group_t _Nonnullgroup)
等待dispatch group 执行完毕的两种方式:
1》dispatch_group_wait(dispatch_group_t _Nonnullgroup, dispatch_time_t timeout)
2》dispatch_group_notify(dispatch_group_t _Nonnullgroup, dispatch_queue_t _Nonnullqueue, ^{
})
第二种方式:开发者可以向函数传入块,等dispatch group执行完毕之后,块会在特定的线程上执行;假如当前线程不应阻塞,而开发者又想在那些任务执行完毕时得到通知,那么久很有必要使用第二种方式了
遍历某个集合
dispatch_apply(10,dispatch_queue_create("com.jds.queue",NULL), ^(size_ti) {
[selflog];
});
for(inti=0; i<10; i++) {
[selflog];
}
也可以迸发执行,但是要考虑琐死的问题
dispatch_queue_tque =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_apply(10, que, ^(size_ti) {
[selflog];
});
小结:
1、一些列任务可归入一个 dispatch group 中,开发者可在这组任务执行完时获得通知
2、通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。
45 -> 使用dispatch_once来执行只需要运行一次的代码
单利可以这样写,可以简化代码并且保证线程的安全,无需担心同步和加锁
+(instancetype)shareInstance{
staticJDSEmployer*shareInstance =nil;
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[JDSEmployeralloc]init];
});
returnshareInstance;
}
小结:
1、一次性安全代码,通过GCD dispatch_once很容易实现此功能
2、标记应该申明在 static 个 global作用域中,这样的话,在把只需要执行一次的块传给 dispatch_once函数的时候。穿进去的标记也是相同
46 -> 不要使用dispatch_get_current_queue (注意: iOS 6.0 已经弃用这个方法了)
小结:
1、此方法已经废弃,仅可用于调试
2、由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念
47 -> 熟悉系统框架
Objective-C的重要特点是:经常使用底层的C语言API
在编写新的工具类之前,最好在系统框架里搜索一下,通常都有写好的类可供直接使用
1》CFNetwork 此框架提供了C语言级别的网络通信,它将“BSD 套接字” (BSD soket)抽象成易于使用的网络接口
2》CoreAudio 此框架提供的C语言API可用来操作设备上的音频硬件,比较难用
3》AVFundation 提供 Objective-C 对象可用来回放并录制音频及视频
4》CoreData 提供的接口可将对象放入数据库,便于持久保存
5》CoreText 此框架提供的C语言接口可以高效的执行文字排版及渲染
小结:
1、许多系统框架都可以直接使用,Fundation 和 CoreFundation 提供了核心的功能
2、很多常见任务都用框架来做,比如:音频与视频的处理、网络通信、数据管理
3、请记住:用纯C写成的框架与用 Objective-C 写成的一样重要,若想成为优秀的 Objective-C开发者,应该掌握C语言的核心概念
48 -> 多用枚举块,少用 for 循环
下面用枚举块的写法比较for的写法读起来更顺畅
数组:
NSArray*enumArray =@[@"1",@"2",@"33",@"23"];
NSEnumerator*enumerator = [enumArrayobjectEnumerator];
idobj;
while((obj =[enumeratornextObject])!=nil) {
NSLog(@"%@",obj);
}
字典:
NSDictionary*enumDic =@{@"11":@"11",@"22":@"22",@"33":@"33",@"44":@"44"};
NSEnumerator*enumeratorDic = [enumDickeyEnumerator];
idobjDic ;
while((objDic = [enumeratorDicnextObject]) !=nil) {
NSLog(@"%@",objDic);
}
集合:
NSSet*set = [NSSetsetWithArray:enumArray];
NSEnumerator*enumeratorSet = [setobjectEnumerator];
idobjSet;
while((objSet =[enumeratorSetnextObject])!=nil) {
NSLog(@"%@",obj);
}
快速查询:
for(id obj in enumArray) {
NSLog(@"%@",obj);
}
语法最简单效率最高的遍历方法:
for(id obj in [enumArray reverseObjectEnumerator]) {
NSLog(@"%@",obj);
}
基于块的遍历方式:遍历时可以直接从块里获取更多信息;
数组:
[enumArray enumerateObjectsUsingBlock:^(id _Nonnullobj, NSUInteger idx,BOOL*_Nonnullstop) {
/*do something*/
if(shouldStop) {
stop =YES;
}
}];
字典:
[enumDicenumerateKeysAndObjectsUsingBlock:^(id _Nonnullkey,id _Nonnullobj,BOOL*_Nonnullstop) {
/*do something*/
if(shouldStop) {
*stop=YES;
}
}];
小结:
1、遍历集合的有四种方式:for 、枚举、 快速、块最基本的方法是for,其次是NSEnumerator非遍历法及快速遍历,最新、最先进的方法是“块枚举法”
2、“块枚举法”本身就能通过GCD来并发执行遍历操作,无需另行编写代码,而采用其他遍历方式无法轻易实现这一点
3、如提前知道待遍历的集合含有何种对象,则应修改块签名,指出对象的具体类型
49 -> 对自定义其内存管理语义的collection 使用无缝桥接
使用C语言框架API需要桥接一下
NSArray*aNSArray =@[@1,@2,@3];
CFArrayRefaCFArrayRef = (__bridgeCFArrayRef)(aNSArray);
aNSArray = (__bridgeNSArray*)(aCFArrayRef);
__bridge:告诉ARC如果处理转换所涉及的Objective-C对象,ARC仍然具备这个Objective-C对象的所有权
__bridge_retained:与__bridge相反,意味着ARC交出对象的所有权,用完记得释放
__bridge_transfer:想把CFArrayRef非转换为NSArray*法,并且想令ARC获得所有权,就可以采取这种方式
这三种方式被称为桥式转换。
#import <CoreFoundation/CoreFoundation.h>
constvoid*JSRetainCallBack(CFAllocatorRefallcator,constvoid*value){
returnCFRetain(value);
}
voidJSReleaseCallBack(CFAllocatorRefallcator,constvoid*value){
returnCFRelease(value);
}
//CFIndex version;
//CFDictionaryRetainCallBack retain;
//CFDictionaryReleaseCallBack release;
//CFDictionaryCopyDescriptionCallBack copyDescription;
//CFDictionaryEqualCallBack equal;
//CFDictionaryHashCallBack hash;
CFDictionaryKeyCallBackskeyCallBacks = {
0,
JSRetainCallBack,
JSReleaseCallBack,
NULL,//null表示默认
CFEqual,
CFHash
};
CFDictionaryValueCallBacksvaluecallBacks = {
0,
JSRetainCallBack,
JSReleaseCallBack,
NULL,
CFEqual,
};
-(void)p_privateMethod{
CFMutableDictionaryRefaCFDictionary =CFDictionaryCreateMutable(NULL,0, &keyCallBacks, &valuecallBacks);
NSMutableDictionary*aNSDictionary = (__bridgeNSMutableDictionary*)(aCFDictionary);
}
小结:
1、通过无缝桥接技术,可以在Foudation框架中的Objective-C对象雨CoreFoundation框架中的C语言数据结构之间来回转换
2、在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素,然后,可用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C。
50 -> 构建缓存时选用NSCache而非NSDictionary
{
NSCache *_cache;
}
- (id)initWithURL:(NSURL*)url{
if([superinit]) {
_url= url;
_cache= [NSCachenew];
_cache.countLimit =100;//最大储存数
_cache.totalCostLimit=5*1024*1024;//不能超过的数据大小5MB
}
returnself;
}
- (void)downloadDataForUrl:(NSURL*)url{
NSData*cacheData = [_cacheobjectForKey:url];
if(cacheData) {
NSLog(@"缓存过的了直接使用缓存数据");
}else{
NSLog(@"没有缓存需要下载");
[selfstartWithCompletionHandler:^(NSData*data,NSError*error) {
[_cachesetObject:dataforKey:urlcost:data.length];
NSLog(@"保存并且使用数据");
}];
}
}
使用NSPurgeableData系统资源紧张时会把保存的缓存对象内存丢弃
- (void)downloadDataForUrl:(NSURL*)url{
NSPurgeableData*cacheData = [_cacheobjectForKey:url];
if(cacheData) {
[cacheDatabeginContentAccess];//停止正在清除该data的操作
NSLog(@"缓存过的了直接使用缓存数据");
[selfuseData:cacheData];
[cacheDataendContentAccess];
}else{
NSLog(@"没有缓存需要下载");
[selfstartWithCompletionHandler:^(NSData*data,NSError*error) {
//⚠️创建好的NSPurgeableData对象后,其“引用计数”会多1,所以无需再调用beginContentAccess,但是必须要调用endContentAccess抵消这个”1“
NSPurgeableData*purgeableData = [NSPurgeableDatadataWithData:data];
[_cachesetObject:purgeableDataforKey:urlcost:data.length];
NSLog(@"保存并且使用数据");
[selfuseData:data];
[purgeableDataendContentAccess];
}];
}
}
小结:
1、实现缓存时哟哦难过NSCache因为它是线程安全的
2、NSCache对象设置上限,不是“硬限制”,只是起指导作用
3、NSPurgeableData与NSCache搭配使用,可实现自动清除功能,被丢弃时,该对象自身也会从缓存清除
4、一般,从网络获取或者磁盘获取的数据草缓存,会提高程序效率
51 -> 精简 initialize 与 load的实现代码
+ (void)load :程序启动的时候分类或者类,会执行此方法
+(void)initialize :它是惰性调用的,程序首次用该类之前调用,且只调用一次
小结:
1、在加载阶段,如果类实现了load方法,那么系统就会调用它;类的load方法要比分类先调用,且不参与覆写机制
2、首次使用某个类之前,系统会向其发送initialize消息。因为该方法遵从覆写规则,所以通常应该在里面判断当前初始化的是哪个类
3、initialize 与 load尽量精简和避免使用
4、无法在编译器设定的全局变量,可以放在initialize方法里初始化
52 -> 别忘了NSTimer 会保留其目标对象
这段代码采用了一种很有效的写法,它先定义了一个弱引用,令其指向 self,然后使块捕获这个引用,而不直接去捕获普通的 self 变量,也就是说,self 不会为计时器所保留。当块开始执行时,立刻生成 strong 引用,以保证实例在执行期间持续存活。
@interface NSTimer (JSBlockSupport)
+ (NSTimer*)js_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementationNSTimer (JSBlockSupport)
+ (NSTimer*)js_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats {
return[selfscheduledTimerWithTimeInterval:intervaltarget:selfselector:@selector(js_blockInvoke:)userInfo: [blockcopy]repeats:repeats];
}
+ (void)js_blockInvoke:(NSTimer*)timer {
void(^block)() = timer.userInfo;
if(block) {
block();
}
}
@end
小结:
1、NSTimer 对象会保留其目标,直到计时器本身失效为止,调用 invalidate 方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。
2、反复执行任务的计时器(repeating timer),很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。
3、可以扩充 NSTimer 的功能,用 “块”来打破保留环。不过,除非 NSTimer 将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。