声明:这个笔记的系列是我每天早上打开电脑第一件做的事情,当然使用的时间也不是很多因为还有其他的事情去做,虽然吧自己买了纸质的书但是做笔记和看的时候基本都是看的电子版本,一共52个Tip每一个Tip的要点我是完全誊写下来的,害怕自己说的不明白所以就誊写也算是加强记忆,我会持续修改把自己未来遇到的所有相关的点都加进去,最后希望读者尊重原著,购买正版书籍。PS:不要打赏要喜欢~
GitHub代码网址,大大们给个鼓励Star啊。
整个系列笔记目录
《Effective Objective-C 2.0》第一份读书笔记
《Effective Objective-C 2.0》第二份读书笔记
《Effective Objective-C 2.0》第三份读书笔记
第四章 协议与分类
23.通过委托与数据源协议进行对象间通信
我举一个~ 获取网络数据的类含有一个“委托对象”,在获取完数据之后,它会回调这个委托对象。
EOCDataModel对象就是EOCNetworkFetcher的委托对象。EOCDataModel请求EOCNetworkFetcher“以异步方式执行一项任务”,而EOCNetworkFetcher在执行完这项任务之后,就会通知其委托对象,也就是EOCDataModel。
这里面代理要用weak修饰。通常情况下,因为代理delegate要对比TableView做相应的操作,所以代理delegate要持有TableView这个对象,而如果我们用Strong修饰TableView的delegate属性,就会引入保留环(retain cycle)。
如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法判断这个委托对象是否响应相关选择子。
NSData * data ;
if([_delegate respondsToSelector:@selector(networkFetcher: didReceiveData:)]){
[_delegate networkFetcher: didReceiveData:];
}
委托模式: 对象把应该对某个行为的责任委托给另一个类。
以TableView为例子
委托模式是信息从类流向受委托者也就是让这个责任流向了受委托者。
数据源模式(Data Source Pattern)是数据流向TableView,决定TableView的布局。
写一个现实中的例子吧。(通过协议代理哈,我知道这个方案更好的方案,只不过想写出来下协议代理的步骤)。(详细见第二十三条Demo)。
加入我想通过一个类开启定时器,然后另一个类来监控这个类的定时器,当这个定时器开启5S之后,相应的做一些事情,首先是被监视定时器的那个类:
//.h
#import <Foundation/Foundation.h>
@class Thirtyeight;
//首先是协议代理方式
typedef void(^iSuCompletionHandle)(int five);
@protocol iSuNetworkFetcherDelegate <NSObject>
- (void)netwrokFecher:(Thirtyeight *)networkFetcher didFinishWithData:(int)five;
@end
@interface Thirtyeight : NSObject
@property (nonatomic,weak) id<iSuNetworkFetcherDelegate> delegate;
@property (nonatomic,strong) NSTimer * iSuTimer;
@property (nonatomic,assign) int iSuNumber;
@property (nonatomic,copy) iSuCompletionHandle iSuCompletion;
- (void)TimerTest;
@end
//.m
@implementation Thirtyeight
- (void)TimerTest{
self.iSuNumber = 0;
//这个Block我写着玩的,实际作用不大,只不过下面有一个关于block的问题。
__weak typeof(self) weakSelf = self;
self.iSuCompletion = ^(int five) {
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.iSuNumber = five;
NSLog(@"赋值给self.iSuNumber为%i",strongSelf.iSuNumber);
};
__block int num = 0;
_iSuTimer =[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
num++;
NSLog(@"定时器里面的变化%d",num);
/*
这边就是最重要的代码了,也就是传递给监听对象数据。
*/
[strongSelf.delegate netwrokFecher:strongSelf didFinishWithData:num];
if (num == 5) {
strongSelf.iSuNumber = num;
NSLog(@"现在的数值为:%d",strongSelf.iSuNumber);
//这边调用block的时候,是不允许我们使用strong.iSuCompletion这样的操作。
_iSuCompletion(num);
[_iSuTimer invalidate];
};
}];
}
主动监听类:
//.m
@implementation ThirtyeightViewController
interface ThirtyeightViewController ()<iSuNetworkFetcherDelegate>
- (void)netwrokFecher:(Thirtyeight *)networkFetcher didFinishWithData:(int)five{
//这样就可以监听了。
NSLog(@"现在行走的时间是多少:%d",five);
}
@end
要点:
- 委托模式为对象提供一套接口,使其可由此将相关事件告知其他对象。
- 将委托对象应该支持的接口定义为协议,在协议中把可能需要处理的事件定义成方法。
- 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情况下,该模式也成为“数据源协议(data source protocal)”
- 若有必要,可实现含有位段的结构体,将委托对象是否响应相关协议方法这一信息缓存到其中。
24.将类的实现代码分散到便于管理的数个分类之中
分类功能是对类对相应功能的整理,使得整个类的条理更加清晰,防止更多不需要的方法在头文件中导入,影响系统性能。
例如:
//.h
#import <Foundation/Foundation.h>
@interface EOCBengi : NSObject
@property (nonatomic, copy, readonly) NSString * firstName;
@property (nonatomic, copy, readonly) NSString * lastName;
@property (nonatomic, strong, readonly) NSArray * friends;
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
@end
@interface EOCBengi(Friendship)
- (void)addFriend:(EOCBengi *)person;
- (void)removeFirend:(EOCBengi *)person;
- (BOOL)isFriendWith:(EOCBengi *)person;
@end
@interface EOCBengi(Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
@interface EOCBengi(Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
//.m
#import "EOCBengi.h"
@implementation EOCBengi
@end
@implementation EOCBengi(Friendship)
@end
@implementation EOCBengi(Work)
@end
@implementation EOCBengi(Play)
@end
要点:
- 使用分类机制把类的实现代码划为易于管理的小块。
- 将应该视为“私有”的方法归为Private的分类中,来隐藏实现细节。
25.总是为第三方类的分类名称加前缀
分类机制通常用于向无源码的既有类中新增功能。
要点:
- 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
- 向第三方磊中添加分类时,总应给其中的方法加上你专用的前缀。
26.勿在分类中声明属性
尽管在技术上讲,分类中也是可以声明属性的,但这种做法还是要尽量避免的。原因在于,除了"class-continuation分类"之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。
如果在分类中声明了属性,我们可以通过运行期关联对象的方式get set数值,但是这样容易引起内存问题。因为我们在为属性实现存取方法时,经常会忘记遵守从内存管理语义,那么有可能在不经意之间就造成了内存出现错误。(当然这个错误是自己可以解决的)。
#import <objc/runtime.h>
static const char * kFriendsPropertyKey = “kFriendPropertyKey”
@implemetation EOCPerson(Friendship)
(NSArray*)friends(
return objc_getAssociatedObject(self, kFriendsPropertyKey);
)
(void)setFriends:(NSArray*)friends{
objc_setAssociatedObject(self , kFriendsPropertyKey , friends,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
要点
- 把封装数据所用的全部属性都定义在主接口里。
- 在”class - continuation分类” 之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
27.使用“class- continuation分类“ 隐藏实现细节
编写的准则是
@interface EOCPerson ()
//Methods here
@end
这个就是正常的我们创建的.m上面的东西,就是class- continuation
类的延续。
要点:
- 通过“ class-continuation分类”向类中新增实例变量。
- 如果某属性在主接口声明为“只读”,而类的内部又要设置方法修改此属性,那么就在“calss-continuation分类’中将其扩展为“可读写”。
- 把私有方法的原型声明在“calss-continuation分类”里面。
- 若要使类所遵循的协议不被人知道,则可用“class-continuation分类”中声明。
28.通过协议提供匿名对象
匿名对象:以内联形式所创建出来的无名类。
这个匿名对象具体表象就是:
@property (nonatomic ,weak) id <EOCDelegate> delegate;
由于该属性类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性,即使这个类不集成与NSObject也是可以的,只要遵守协议<EOCDelegate>就好了。相同例子还有字典的存储,我们知道字典对于key是copy而对于值为保留的:
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
要点:
- 协议可在某个程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所实现的方法。
- 使用匿名对象来隐藏类型名称(或类名)
- 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
第五章 内存管理
29.理解引用计数
在引用计数的架构下,每个对象都一个计数器,NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值。
Retain 递增保留计数
release 递减保留计数
autorelease 待稍后清理“自动释放池(autorelease pool)”时,再递减保留计数。
NSMutableArray * array = [NSMutableArray alloc] init];
NSNumber * number =[NSNumber alloc] initWithInt:1223];
[array addObject:number];
[number release];
[array release];
如果我们不走[array release]方法,那么我们知道number对象还是会存在的,因为数组还在持有他,但是绝对不应该假设对象一定存在,也就是说,不要这样写代码:
NSNumber * number = [NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
NSLog(@“ number = %@”,number);
如果我们调用了release之后,基于某些原因,其保留计数可能降至0.
为了避免在不经意间使用了无效对象,一般调用玩realse之后都会清空指针。这就保证了不会出现可能指向无效对象的指针。这种指针通常称为悬挂指针
。比如:
NSNumber * number = [NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;
不管是数组,其他的对象也可以保留别的对象,这一般都是用过“属性”来实现。会用到相关实例变量的获取方法及设置方法。若属性为“strong关系(string relationship)”,那么设置的属性就会保留,比方说,有一个属性为foo
- (void)setFoo:(id ) foo {
[foo retain];
[_foo release];
_foo = foo;
}
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。加入还未保留新值就先将旧值释放了,而且两个值又指向同一个对象,那么,先执行的release操作就可能导致喜用将对象永久回收。而后续的retain操作则无法令这个已经彻底回收的对象复生,于是实例变量就变成了悬挂指针。
自动释放池
调用release会立刻递减对象的保留计数,然而有些时候不能可以不调用release,改调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环(event loop)时递减,不过也可能执行的更早些”。
此特性很有用,尤其在方法中返回对象时更应该用它,在这种情况下:
(NSString*)stringValue{
NSString * str =[NSString alloc] initWithFormat:@“I am this:%@,self”];
return str;
}
这个时候返回的str对象比期望的要多1 因为调用 alloc 会加1,但是不会有对应的释放操作,但是不能在方法内部释放 str,否则没等方法返回系统就把该方法回收了。这里应该用autorelease,它会稍后释放对象,从而给调用者留下足够长的时间,使其可以在需要时先保留返回数值,换句话就是保证对象在跨越“方法调用边界(method call boundary)”后一定存活。
(NSString *)stringValue{
NSString * str =[NSString alloc] initWithFormat:@“I am this %@”,self];
return [str autorelease];
}
保留环(retain cycle)
就是相互持有
要点:
- 引用计数机制通过可以递增递减的计数器来管理内存,对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0的时,对象就被销毁了。
- 在对象声明周期中,其余对象通过引用来保留或释放此对象,保留与释放操作分别会递增或者递减保留计数。
30. 以ARC简化引用计数
Clang编译器自带一个“静态分析器(static analyzer)” 。用于指明程序里引用计数出问题的地方。
由于ARC会自动执行retain,release,autorelease等操作,所以不能直接调用retain,release,autorelease,dealloc 。
将内存管理语义在方法命中表示出来早就成了OC的惯例,而ARC则将之确立为硬性规定。这些规则简单的体现在方法名上。若方法名以下列语句开头,则其返回的对象鬼调用者所有
alloc ,new ,copy ,mutableCopy
对于ARC和MRC的转换:
ARC:
_myPerson = [EOCPerson personWithName:@“Bob Smith”];
MRC:
EOCPerson * tmp = [EOCPerson personWithName:@“Bob Smith”];
_myPerson = [tmp retain];
ARC可以在运行期检测到这一对多余的操作,也就是autorelease及紧跟其后的retain。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的autorelease方法,而是改用调用objc_autoreleaseReturnValue。此函数会检视当前方法之后即将要执行的那段代码。若发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构中的一个标志位,而不执行autorelease操作。
如果返回一个自动释放的对象,而调用方法的代码中保留此对象,那么此时不执行retain,而改成objc_retainAutoreleasedReturnValue函数。此函数要检测刚才提到的那个标志位,若已经执行retain操作。设置并检测标志位。要比调用autorelease和retain更快。
要点:
- 有ARC之后,程序员就无须担心内存管理问题。使用ARC来变成,可省去类中很多的“样板代码”。
- ARC管理对象生命期的办法基本上就是:在合适的地方插入”保留“及”释放“操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手动执行“保留”及“释放”操作。
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。
- ARC只负责管理Object-C对象的内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
31.在dealloc方法中释放引用并解除监听
如果手动管理引用计数的话 dealloc中需要调用 [super dealloc] 而ARC中则不需要调用[super dealloc]。
要点:
- 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观察“(KVO)或NSNotificationCenter等通知 ,不要做其他的事情。
- 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
- 执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因此此时对象已处于正在回收的状态下。
32.编写“异常安全代码”时留意内存管理问题
OC的错误模型表示,异常只有在发生了严重错误(21条详解)的时候才会被抛出。
这个Tip里面我们研究MRC情况下怎么实现异常处理内存,就像C++那样的。
我们使用try块实现这个功能:
@finally的作用在于无论是否抛出异常都会走这一步,因为我们不清楚程序会不会在dosomeThing中抛出异常,如果抛出异常可能会影响下面的动作。
@try {
ThirtyTwo * object =[[ThirtyTwo alloc]init];
[object dosomeThing];
// [object release];
} @catch (NSException *exception) {
NSLog(@"抛出异常");
} @finally {
// [object release];
}
要点:
- 捕捉异常时,一定要注意try块内创立的对象清理干净。
- 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不会导致应用程序变大,而且降低运行效率。
33.以弱引用避免保留环
强引用:
#import <Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA :NSObject
@property (nonatomic, strong) EOCClassB * other;
@end
@interface EOCClassB :NSObject
@porperty (nonatomic, strong) EOCClassA * other;
@end
避免保留环的最佳方式就是弱引用,用weak或者是unsafe_unretained即可。
要点:
- 将某些引用设置为weak,可避免出现“保留环”。
- weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
34.以“自动释放池块”降低内存峰值
创建自动释放池的方法如下:
@autoreleasepool {
}
当数据量过大的时候我们可以通过嵌套自动释放池的方法来降低内存峰值。
NSArray * databaseRecorde = /******/;
NSMutableArray * people = [NSMutableArray new];
for (NSDictionary * record in databaseRecords){
@autoreleasepool {
EOCPerson * person = [[EOCPerson alloc] initWithRecord:record];
[person addObject: person];
}
}
自动释放池机制就像“栈(stack)”一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
之前MRC的时候有一个创建释放池的类NSAutoreleasePool 此对象更加重量级(heavyweight) 通常用来创建偶尔需要清空的池,比方说:
NSArray * databasRecode = /****/
NSMutableArray * people = [NSMutableArray new];
int i = 0 ;
NSAutoreleasePool * pool = [NSAutoreleasePool alloc] init ];
for (NSDictionary * record in databaseRecode ){
EOCPerson * person = [EOCPerson alloc] initWithRecord:record];
[people addobject:person];
if (++i == 10){
[pool drain];
i = 0;
}
}
[pool drain];
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
要点:
- 自动释放池排布在栈中,对象受到autorelease消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
35.用“僵尸对象”调用内存管理问题
僵尸对象:启动这项调试成功之后,运行期系统会把所有已经回收的实例转化为特殊的“僵尸对象(Zombie Object)”,而不会真正回收他们。这样就使得我们能够及时定位到错误位置。
打开全局断点和僵尸对象调试功能有两种方法:
第一种方法:
点击"+"选择Exception Breakpoint。
第二种方法:
点击上方调试开关右边的程序管理台,选择Edit Scheme。
然后选择Run - > Diagnostics - > Zombie Objects。
要点:
- 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
- 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。
36.不要使用retainCount
要点:
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数(absolute retain count)”都无法反应对象生命期的全貌。
- 引入ARC之后,retainCount方法就正式废止,在ARC下调用该方法会导致编译器报错。
结尾
自己写的笔记首先是用Pages写的,写完之后放到简书里面以为也就剩下个排版了,结果发现基本上每一个点的总结都不让自己满意,但是又想早点放上去,总感觉自己被什么追赶着,哈哈,本来写完笔记的时候是2W字的,结果到第二次发表的时候发现就成了2.5W了,需要改进的东西还是太多,希望朋友们有什么改进的提议都可以告诉我,我会一直补充这个笔记,然后抓紧改GitHub上的代码~