声明:这个笔记的系列是我每天早上打开电脑第一件做的事情,当然使用的时间也不是很多因为还有其他的事情去做,虽然吧自己买了纸质的书但是做笔记和看的时候基本都是看的电子版本,一共52个Tip,每一个Tip的要点我是完全誊写下来的,害怕自己说的不明白所以就誊写也算是加强记忆,我会持续修改把自己未来遇到的所有相关的点都加进去,最后希望读者尊重原著,购买正版书籍。PS:不要打赏要喜欢~
GitHub代码网址,大大们给个鼓励Star啊。
整个系列笔记目录
《Effective Objective-C 2.0》第一份读书笔记
《Effective Objective-C 2.0》第二份读书笔记
《Effective Objective-C 2.0》第三份读书笔记
第六章 block和GCD (block和Grand Central Dispatch)
“块”是一种可在C,C++以及Objective-C代码中使用的“语法闭包(lexical closure)”,借用此机制,开发者可讲代码像对象一样传递,令其在不同的情况下运行。
GCD是一种和块有关的技术,它提供了对线程的抽象,而这种抽象则基于“派发队列(dispatch queue)”。开发者可将块排入队列中,由GCD负责处理所有调用事宜。GCD会根据系统资源情况,适时的创建,复用,摧毁后台线程。
37.理解“块”这一概念
块的基本知识
块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块使用 “^” 符号来表示,后面跟着花括号。
int additional = 5;
__ block int iSu = 8;
int (^ addBlock) (int a, int b) = ^(int a , int b){
return a + b + additional;
iSu++;
}
int add = addBlock(2,5) // add = 12
声明变量的时候可以加上_ _block修饰符,变量的地址就可以从stack转移到heap上,从而持有这个变量,而如果只是局部变量,block会copy变量数值,但是不会再受到变量改变的影响。
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,那会将其一并释放。就是引出一个和块有关的重要问题。块本身可视为对象。有引用计数。当最后一个指向块的引用移走之后,块就回收了。
如果将块定义在OC的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无须加__block。不过,如果实例变量与self所指代的实例关联在一起的。例如,下面这个块声明在EOCClass类的方法中。
@interface EOCClass
- (void)anInstancemethod{
void (^someBlock)()= ^{
_anInstanceVariable = “Something”;
NSLog(@“_anInstanceVariable = %@”,_anInstanceVariable);
}
}
如果某个EOCClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。由于块里没有明确使用self变量,所以很容易就会忘记self变量其实也为块所捕获了。直接访问实例变量和通过self来访问是等效的。
self -> _anInstanceVariable = @“Something” ;
那么这种情况下就会导致“保留环”。
块的内存布局:
首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种消息。
- invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void * 型的参数,此参数代表块。
- descriptor 变量是指向结构体的指针,每个块里都包含此结构体,其中声明块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行。
块还会把捕捉到的所有变量都拷贝一份。放在descriptor后面。捕捉多少个变量,就会占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。用于在执行块的时候,从内存中吧这些捕捉到的变量读出来。
全局block,栈block以及堆block
定义块的时候,其所占的内存区域是分配在栈中的,这就是说,块只在定义它的那个范围内有效。
void(^block)();
if ( some condition){
block = ^ {
NSLog(@“Block A”);
}
} else {
block = ^ {
NSLog(@“Block B”);
}
}
block();
解决此问题的办法就是发送copy消息以拷贝。
if ( some condition ){
block = [^{
NSLog(@“Block A”);
} copy];
}else {
block = [^{
NSLog(@“Block B”);
} copy];
}
那么其他文章里面有相关的block类型分类:
全局block(_NSConcreteGlobalBlock)的block要么是空block,要么是不访问任何外部变量的block。它既不在栈中,也不再堆中。
栈block(_NSConcreteStackBlock)的block有闭包行为,也就是有访问外部变量,并且block只且只有有一次执行,因栈中的空间是可重复使用的,所以当栈中的block执行一次之后就会被清空出栈,所以无法多次使用。
堆block(_NSConcreteMallocBlock)的block有闭包行为,并且该block需要被多次执行。当需要多次执行时,就会把该block从栈中复制到堆中。
要点:
- 块是C,C++,Objective-C中的词语闭包。
- 块可接受参数,也可返回值。
- 块可以分配在栈或堆上,也可以是全局的。分配在站上的block可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。
38.为常用的块类型创建typedef
要点:
- 以typedef重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应准从现有的命名习惯,勿使其名称与别的类型相冲突。
- 不妨为同一块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名。那么只需要修改相应typedef中块签名即可,无须改动其他的tyoedef。
39.用handler块降低代码分散程度
iOS 上有一个叫”系统监控器 ”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。
异步方法在执行任务之后,需要以某种手段通知相关代码。实现此功能有很多办法。常用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。
这里面呢我需要再重新规划一下委托模式的规范:
第一如果我们要一个类监视另一类的某个属性,那么。
在监视的那个类里面
@protocol EOCNetworkFecherDelegate<NSObject>
(void)newworkFetcher:(EOCNetworkFetcher *)networkFether didFinishWithData:(NSData *)data;
@end
然后给一个需要赋值给监视类的delegate属性
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate>delegate;
因为是要监视一个data属性的 ,所以在被监视的类的.m文件中我们需要:
[_delegate newworkFether:networkFether didFinishWithData:data];
然后再监视的类里面我们首先是
EOCNetworkFecher对象 newEOC newEOC.delegate = self;
然后实现newworkFecher didFinishWithData:
(void)newworkFetcher:(EOCNetworkFetcher *)networkFether didFinishWithData:(NSData *)data{
做对于data数据收取情况的相关对策。
}
该Tip的主题:
那么现在如果我们想要通过block来传递监听的话:
被监听类:
typedef void (^ iSuNetWorkFecherCompletionHandler)(NSData data,NSError * error);
@interface iSuNetWorkFetcher :NSObject
(void)iSuStartWithCompletionHandler:(iSuNetworkFecherCompletionHandler)completion;
然后再.m里面传递block里面的数值
(void)iSuStartWithCompletionHandler:(iSuNetworkFecherCompletionHandler) completion{
int a = 5;
NSError * error;
completion(a,error);
}
然后监听的类里面 。
首先是对象iSuTest。
[iSuTest iSuStartWithCompletionHandler:^(int data, NSError * error){
NSLog(@“监听过来的数值为:%@”,data);
}];
要点:
- 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
- 在有多个实例需要监控的时候,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块和相关对象放在一起。
- 设计API时如果用到handler块,那么可以增加一个参数,使调用者可以通过此参数来决定应该把块安排在哪个队列上执行。
40.用块引用其所属对象时不要出现保留环
//EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData * data)
@interface EOCNetworkFether:NSObject
@property (nonatomic, strong, readonly) NSURL * url;
(id) initWithURL:(NSURL *)url;
(void)startWithCompletionHandle:(EOCNetworkFetcherCompletionHandler)completion
@end
//EOCNetworkFetcher.m
#import “EOCNetworkFetcher.h”
@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property(nonatomic, strong) NSData * downloadedData;
@implementation EOCNetworkFetcher
(id)initWithURL:(NSURL *)url{
if (slef = [super init]){
_url = url;
}
return self;
}
(void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
self.completionHandler = completion;
//Start the request;
//Request sets downloadedData property
// When request is finished , p_requestCompleted is called;
}
(void)p_requestCompleted{
if(_completionHandler){
_completionHadnler(_downloadedData);
}
}
而另一个类可能会创建这种网络数据获取器对象
@implementation EOCClass {
EOCNetworkFetcher * _networkFethcer;
NSData * _fetchedData;
}
- (void)downLoadData{
NSURL * url =[nsurl alloc]initWithString : @“www.baidu.com”;
_networkFethcer =[EOCNetworkFetcher alloc]initwithURL:url];
[_networkFethcer startWithCompletionHandler:^(NSData * data){
NSLog:(@“url = %@’,_networkFethcer.url);
_fetchedData = data;
}];
}
这段代码看上去是没有问题的 。 但是存在循环引用。
但是因为handler 里面要设置_fechedData = data ,所以handler是要持有self(EOCClass)的。而实例EOCClass又通过属性持有着网络获取器。
解决办法就是:要么令_networkFetcher实例变量不再引用获取器,要么令获取器的completionHandler属性不再持有handler块。在这个例子中,应该等待comletion handler块执行完毕之后,再去打破保留环,比如:
[_networkFecher startWithCompletionHandler:^(NSData * data){
NSLog(@“Request for URL:%@ finished ” ,_networkFetcher.url );
_fetchedData = data;
_networkFetcher = nil;
}];
但是这种情况很自由在执行handler的时候才会调用,如果completion handler一直不运行,那么保留环就无法被打破,内存就会泄露。
我们可以尝试 不持有属性_fetchedData
(void)setcondDownload{
NSURL * url =[[NSURL alloc]initWithString:@"www.baidu.com"];
SecondYearNetworkFetcher * networkFetcher =[[SecondYearNetworkFtcher alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"%@",networkFetcher.url);
_fetchedData = data;
}];
}
//也就是一个局部变量networkFetcher;
但是这样也是含有保留环的;
因为因为handler获取网址是通过networkFecher的 那么也就持有了这个类。
但是这个networkFecher又通过block 来持有handler;
那么解决办法就是在block运行触发的时候,将completionHandler属性置为nil
(void)p_requestCompleted{
if (_completionHandler){
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}
这样一来,只要下载请求执行完成,保留环就被解除了。
要点:
- 如果块所捕获的对象直接或间接地保留了块本身,那么就要当心保留环问题。
- 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。
41.多用派发队列,少用同步锁
有两种添加锁头的方法:
(void)synchronizedMehod{
@synchronized(self){
}
}
或者是:
_lock =[NSLock alloc] init];
(void)synchronizedMethod{
[_lock lock];
//safe
[_lock unlock];
}
这里面也确定了一下设置属性的时候为什么是非原子性的,而不是线程安全的原子性的。
因为滥用也就是每一个属性都用原子性的话会所有的同步快都会彼此抢夺同一个锁。要是有很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完毕才能执行。而且属性的这种知识提供了某种程度的“线程安全(thread safety)”,但是无法保证访问该对象时绝对是线程安全的。
有个简单并且高效的办法可以替代同步块或者锁对象,那就是“串行同步队列”。
_syncQueue = dispatch_queue_create(“com.effectiveobjective.syncQueue”,NULL)
这个最后的一个参数"0"的时候是并行,NULL是串行应该都知道吧。
- (NSString*)someString{
__ block NSString * localSomeString;
dispatch_sync (_syncQueue,^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue,^{
_someString = someString;
});
}
这样就变成了同步,优化一下设置数值的时候是不用同步的,
- (void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue,^{
_someString = someString;
});
}
从调用者的角度来看,这个小改动可以提升设置方法的执行速度,但是如果你测一下程序的性能,那么可能会发现这种写法比原来慢,因为执行异步派发的时候,需要拷贝块。如果块里面的运行操作比较简单,那么运行的时间就会变慢,但是如果繁琐的话就会快一点。
那么第二个问题,我们想要读写一个属性。读的时候可以写,但是写的时候停止读的操作。
这个时候我们可以同步并发队列的栅栏块来试下你这个效果。
_syncQueue = dispatch_get_global_queue(DISPATHC_QUEUE_PRIOPRTY_DEFAULT,0);
- (NSString *)someString{
_ _ block NSString * localSomeString;
dispathch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
})
}
这样的并发栅栏队列要比串行要快。这个意思是必须等set完事才能进行get方法。
要点:
- 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized块或NSLock对象更简单。
- 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
- 使用同步队列以及栅栏块,可以令同步行为更加高效。
42.多用GCD,少用performSelector系列方法
关于perform方法:
object调用perfome方法返回的类型是id,虽然可以是void,如果想返回整数型或者是浮点型那就需要复杂的转换操作了。而这种转换很容易出错。performSelector还有几个版本,可以在发消息时顺便传递参数。
所以对比GCD和perform方法我们经常选择GCD。
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
//Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time,dispatch_get_main_queue(),^{
[self doSomething]
})
对比任务放主线程的两种形式:
[self performSelectorOnMainThread:withObject:waitUntilDone:];
//Using dispathc_async
dispathc_async(dispathc_get_main_queue(),^{
[self doSomething];
});
要点:
- performSelector系列方法在内存管理方法容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
- performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
- 如果想要把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用GCD的相关方法来实现。
43.掌握GCD及操作队列的使用时机
NSOperationi以及NSOperationQueue的好处如下:
- 取消某个操作。如果使用派发队列 ,那么取消某个操作是麻烦的,他只存在fire and forget。而操作队列只要调用cancel方法就可以了。
- 指定操作间的依赖关系。GCD也能完成响应的操作,只不过多任务的要好好算算逻辑。而操作队列调用addDependency就可以了。
- 通过键值观测机制监控NSOperation对象的属性。NSOperation有好多属性能够被监听。比如isCancelled等属性。
- 指定操作的优先级。
- 重用NSOperation对象
比如NSBlockOperation对象,使用方法是:
第一种:
NSBlockOperation * block1 =[NSBlockOperation blockOperationWithBlock:^{
NSLog(@“调用子类方法的block”);
}];
[block1 setCompletionBlock:^{
NSLog(@“结束调用”);
}];
[block1 start];
第二种
详细讲解
NSBlockOperation * op1 =[NSBlockOperation alloc] init];
[op1 addExecutionBlock:^{
sleep(10)
NSLog(@“第一个线程第一个任务”);
}];
[op1 addExecutionBlock:^{
sleep(4)
NSLog(@“第二个线程第一个任务”);
}];
[op1 addExecutionBlock:^{
sleep(6)
NSLog(@“第一个线程第二个任务”);
}];
结论:这样方法加载上去的线程 只会是两个 为啥不是三个或者更多呢?
要点:
- 在解决多线程与任务管理问题时,派发队列并非唯一方案。
- 操作队列提供了一套高层的Objective-C API,能实现纯GCD所就被的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。
44.通过Disptch Group机制,根据系统资源状态来执行任务
这里面dispatch group主要的还是并行的队列,因为串行的就没有意义了。
创建通过 dispatch_group_dispatch_group_create();
把任务加入到组里面:
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
dispatch_group_wait (dispatch_group_t group, dispatch_time_t time);
这个方法同在group结束的时候。算是一种堵塞吧
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue ,dispatch_block_t block);
这个方法算是一种结束调度组之后调用的block。
要点:
- 一系列任务可归入一个dispatch group之中。开发者可以在这组任务完毕时获得通知。
- 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量大妈。
45.使用dispatch_once来执行只需运行一次的线程安全代码
单例模式我们之前使用的方法是:
- (id)sharedInstance{
static EOCClass * shared = nil;
@synchrnized(self){
if(!sharedInstance){
sharedInstance = [[self alloc] init];
}
}
return shared;
}
使用@synchrnized同步锁的原因是因为线程安全问题。
关于线程安全问题;
基本就是同一个资源被不同线程同时设置的时候产生的冲突。
而后来我们经常使用的是dispatch_once方法进行单例方法编写:
- (id)iSuSharedInstance{
static EOCClass * shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken ,^{
shared = [[self alloc] init];
})
return shared;
}
事实证明,GCD方法的运行速度要比同步锁块的运行速度高了两倍。
要点:
- 经常需要编写“只需执行一次的线程安全代码(thread-safe single-code execution)”。通过GCD所提供的dispathc_once函数,很容易就能实现此功能。
- 标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传递给dispatch_once函数时,传进去的标记也是相同的。
46.不要使用dispatch_get_current_queue
析构函数:在对象销毁的时候调用的函数就是析构函数。
我们看下下面的这个两个函数
- (NSString *)someString{
__block NSString * localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _ someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
获取方法的时候可能会死锁(EXC_BAD_INSTRUCTION)
死锁是啥子意思呢:
我看哈 就是说单个一个人 要做一个任务 ,然后这个任务的任务是巴拉拉巴拉的任务,所以这个人想要继续做的话就要先把这个巴拉巴拉的任务做完,然而这个巴拉巴拉德任务又是一个加到了同步队列里面的任务。因为这个任务列表不是一个栈类型的(LIFO)而是一个先进去先出来的(FIFO)的队列所以,那么这个人想要完成所有任务就必须要完成外表的队列任务,然后再完成巴拉巴拉的内部队列任务。所以任务停在第一个外表任务,而内部巴拉巴拉任务也需要执行,因为这个人已经被第一个任务给拖住了,所以不可能跑去干第二个事情,第二个任务完不成导致第一个任务也返回完成不了。嗨呀,好气啊,我写了些啥。
死锁存在时机就是get和set方法一同被调用。
这里面就带出了为什么不用dispatch_get_current_queue。
解决方法中含有一个判断是个否是同步队列,如果是就直接block() 实现,如果不是就正常的加入队列运行。
- (NSString *)someString{
_ _block NSString * localSomeString ;
dispatch_block_t accessorBlock = ^{
localSomeString = _someString;
};
if (dispathc_get_current_queue() == _syncQueue){
accessorBlock();
}else {
dispatch_sync(_syncQueue,accessorBlock);
}
}
这个例子这么做当然没有问题,但是如果是嵌套的队列
dispatch_queue_t queueA =dispatch_queue_create("com.effective.queueA", NULL);
dispatch_queue_t queueB =dispatch_queue_create("com.effective.queueB", NULL);
dispatch_sync(queueA, ^{
NSLog(@“第一个A");
dispatch_sync(queueB,^{
NSLog(@“第二个B");
dispatch_sync(queueA, ^{
NSLog(@"第三个C");
});
});
});
这个时候就算用getCurrent方法获取queue的队列获取的也是queueB,同样会产生死锁。
要解决这个问题,最好的办法就是通过GCD提供的功能来设定“队列特有数据”(queue-specific data)
dispatch_queue_t queueC = dispatch_queue_create("com.effectiveobjectivec.queueC", NULL);
dispatch_queue_t queueD =dispatch_queue_create("com.effectiveobjectivec.queueD", NULL);
//将D的运行优先级赋予给C
dispatch_set_target_queue(queueD, queueC);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueC");
dispatch_queue_set_specific(queueC, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueD, ^{
dispatch_block_t block = ^{
NSLog(@"No deadlock!");
CFStringRef retrievalue = dispatch_get_specific(&kQueueSpecific);
if (retrievalue) {
block();
}else{
dispatch_sync(queueC, block);
}
};
});
那么这边确认一下。dispatch_set_target_queue(A,B)的意思是讲A的优先级变成和B同级别,那么就会变成直线向下运行。
要点:
- dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应该在调试的时候运用。
- 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
- dispatch_get_current_queue函数用于解决由不可重入的代码所引起的死锁。然而能用次函数解决的问题,通常也能改变“队列特定数据”来解决。
第七章 系统框架
47.熟悉系统框架
框架的意义:将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。iOS凭条构建的三方框架使用的是静态库(static library),这是因为iOS应用程序不允许在其中包含动态库。这些东西严格讲不是真正的框架,不过,所有iOS平台的系统框架仍然使用动态库。
在iOS上开发“带图形界面的应用程序”时,会用到名为Cocoa的框架,也就是Cocoa Touch,其实Cocoa本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。
开发者会碰到的主要框架就是Foundation 像NSObject,NSArray,NSDictionary等类都在其中。
还有一个Foundation相伴的框架,叫做CoreFoundation。从技术上讲,CoreFoundation框架不算是Objective-C框架,但是他编写了OC应用程序所应熟悉的重要框架,Foundation框架中的许多功能,都可以在CoreFoundation上找到对应的C语言API。CoreFoundation和Foundation框架不仅名字相似,而且还有更加紧密的联系,叫做“无缝桥接”(toll-free bridging)。
无缝桥接技术是用某些相当复杂的代码实现出来的。这些代码可以使运行期系统把CoreFoundation框架中的对象视为普通的Objective-C对象。
- CFNetwork:此框架提供了C语言级别的网络通信能力。
- CoreAudio:C语言API来才做设备上的音频硬件。
- CoreData:可将对象放入数据库,以便持久保存。
- AVFoundation:回放并录制视频音频。
- CoreText:执行文字排版以及渲染操作。
用C语言来实现API的好处是,可以绕过OC的运行期系统,从而提高执行速度。当然由于ARC只负责Objective-C的对象,所以使用这些API的时候需要注意内存管理问题。如果是UI框架的话,主要就是UIKit。
CoreAnimation是OC写成的,CoreAnimation本身并不是框架,它是QuartzCore框架的一部分。然而在框架的国度里,CoreAnimation仍算是“一等公民”。
CoreGraphics框架是以C语言写成的。提供了2D渲染所必备的数据结构和函数。例如CGPoint,CGSize,CGRect等。
要点:
- 许多系统框架都可以直接使用,其中最重要的是Foundation和CoreFoundation,这两个框架构建应用程序所需的许多核心功能。
- 很多常见任务都能用框架来做,例如音频处理,网络通讯,数据管理等。
- 请记住:用纯C写成的框架与同OC写成的一样重要,若要成为优秀的OC开发者,应该掌握C语言的核心概念。
48.多用块枚举,少用for循环
相对于For循环对应的 NSArray,NSDictionary,NSSet,NSEnumerator更加便捷。
Dic : [aDictionary allKeys] 赋值给NSArray;
Set : [aSet allObjects] 赋值给NSArray;
NSEnumerator是个抽象类,其中提供了两个方法- nextObject
和 - allObjects
NSArray:
NSArray * anArray = @[@"iSu",@"Abner",@"iSuAbner"];
NSEnumerator * enumerator = [anArray objectEnumerator];
id iSuobject;
while ((iSuobject = [enumerator nextObject]) != nil) {
NSLog(@"NSEnumerator保存的数据位%@",iSuobject);
}
NSDictionary:
NSDictionary * aDictionary = @{@"iSu":@"1",@"Abner":@"2",@"KoreaHappend":@"3"};
NSEnumerator * DicEn = [aDictionary keyEnumerator];
id key;
while ((key = [DicEn nextObject])!= nil) {
id value = aDictionary[key];
NSLog(@"NSDictionary的%@对应的是%@",key,value);
}
NSSet:
NSSet * aSet = [NSSet setWithObjects:@"iSu",@"Abner",@"iSuAbner", nil];
NSEnumerator * SetEn = [aSet objectEnumerator];
id Setobject;
while ((Setobject = [SetEn nextObject]) != nil) {
NSLog(@"NSSet里面出来的东西:%@",Setobject);
}
快速遍历:
也就是forin遍历:
NSDictionary:
id key;
for (id key in aDictionary) {
id value = aDictionary[key];
//NSLog(@"ForinNSDictionary的%@对应的是%@",key,value);
}
NSSet:
id key;
for (id key in aDictionary) {
//NSLog(@“ForinNSSet:%@",key);
}
基于块的遍历方式:
(void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx ,BOOL * stop)) block
block遍历的优势在于有一个stop的指针可以选择性的结束遍历。
//块遍历
[anArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([anArray[idx] isEqualToString:@"Abner"]) {
*stop = YES;
}else{
NSLog(@"block遍历:%@",anArray[idx]);
}
}];
要点:
- 遍历collection有四种方式,最基本的办法是for循环,其次是NSEnumerator遍历法以及快速遍历法,最新,最先进的方式则是“块枚举法”。
- “块枚举法”本身就能通过GCD来并发执行遍历操作,无需另行编写代码。而采用其他遍历方法则无法轻易实现这一点。
- 若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。
49.对自定义内存管理语义的collection使用无缝连接
什么时候能够用到无缝连接?
那我看了原文基本就是一个改变某些基类的内存管理语义。
比如说我们需要一个NSDictionary的键是一个不能被copy的对象,我们知道正常情况下key的数值是被拷贝而不是保存的,而如果这个key不是一个遵守NSCopy对象,那么我们就需要他保留下来,而不是copy。
首先说一下OC的NSArray —> CFArrayRef
NSArray * anNSArray = @[@1,@2,@3,@4,@5];
// __bridge本意是:ARC仍然具备这个OC对象的所有权。而__bridge_retained则与之相反,意味着ARC交出对象的所有权。
// NSArray --> CFArrayRef 用__bridge
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"size of array = %li",CFArrayGetCount(aCFArray));
而反向的CFArrayRef --> NSArray 用 __bridge_transfer
这里面提醒一下。如果是从C的不可变数组或者是字典转化成OC的,那么需要的参数会比可变的要多:
// CFArrayCreateMutable(<#CFAllocatorRef allocator#>, <#CFIndex capacity#>, <#const CFArrayCallBacks *callBacks#>)
CFArrayCreate(<#CFAllocatorRef allocator#>, <#const void **values#>, <#CFIndex numValues#>, <#const CFArrayCallBacks *callBacks#>)
我们用到改变内存管理语义的话基本都是可变的,因为你都不可变了你变化管理语义有什么语义....
这里面我们用CFMutableDictionaryRef为例子需要四个参数:
CFDictionaryCreateMutable(<#CFAllocatorRef allocator#>, <#CFIndex capacity#>, <#const CFDictionaryKeyCallBacks *keyCallBacks#>, <#const CFDictionaryValueCallBacks *valueCallBacks#>)
第一个是内存,第二个是个数,第三个是key属性的对象内存管理,第四个参数是value属性的对象内存管理。仔细的demo请看FortyNine。
最后代码:
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
NSMutableDictionary * anNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFDictionary;
这样生成的字典就能满足健可以是不能copy的对象。
要点:
- 通过无缝桥接计数,可以在Foundation框架中的Objective-C对象与CoreFoundation框架中的C语言结构之间来回转换。
- 在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存语义的Objective-C collection。
50.构建缓存时选用NSCache而非NSDictionary
对比NSCache相对于NSDictionary的优势:
- 当系统资源将要耗尽的时候,它可以自动删除缓存,字典的话会比较麻烦,而且NSCache会先行删除“最久未使用的对象”。
NSCache并不会“拷贝”键,而是“保留”他,这样就可以使得健值可以使不能被copy的对象。 - 开发者可以操控缓存删减其内容的时机。可以调整的有两个数值,一是缓存中的”对象总数“,其二是对象的“总开销”,开发者在将对象加入缓存的时候,可以为其指定“开销值”。当对象总数或开销超过上限时,缓存就可能会删减其中的对象了。想缓存中添加对象时候,只能在很快计算出“开销值”的情况下,才应该考虑采用这个尺度。如果计算的过程长的就不适合这样了。具体的例子可以看Fifty.demo。
- 还有一个类NSPurgeableData,此类是NSMutableData的子类,这个对象在内存能够根据需求随时丢弃。我们可以通过他遵守的NSDiscardableContent协议里面的isContentDiscarded方法来查询相关内存是否已经释放。
- 如果想要访问某个NSPurgeableData对象,可以调用其beginContentAccess方法,告诉它现在不应该被抛弃,使用完之后调用endContentAccess方法来告诉必要的时候可以丢弃。这就和引用计数相似。
要点:
- 实现缓存时应选用NSCache而非NSDictionary对象。因为NSCache可以提供优雅的自动删除功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝健。
- 可以给NSCache对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删除其中对象对象的时机,但是绝对不要把这些尺寸当成可靠的“硬限制(hard limit)”,它们仅对NSCache其指导作用。
- 将NSPurgeableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
- 如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种“重新计算起来很费事”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。
51.精简initialize和load的实现代码
load方法和initialize方法的区别:
- load方法只会调用一次。虽然initalize也是只调用一次。。
- load方法必须要所有的load运行才能继续下去,而init是惰性的。不会直接启动。
- 如果子类没有实现load方法,那么就不会调用父类的load方法。
而initialize 就是正常的,如果子类没有实现就实现超类的。如果有,也要实现超类的。 - 在一个类的load方法里面放入了其他的类的方法是不安全的,应为不清楚那个类是否已经load完毕了。
要点:
- 在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。和其他类方法不同,load方法不参与覆写机制。
- 首次使用某个类之前,系统会给其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在判断当前要初始化的是哪个类。
- load和init方法都应该实现的精简一点,这有助于保持应用程序的响应能力,也- - 能减少引入“依赖环”的几率
- 无法在编译器设定的全局常量,可以放在initialize方法里初始化。
随便看到的一个句子:
朋友问我暗恋是什么感觉,下意识的回答:好像在商店看到喜欢的玩具,想买,钱不够,努力存钱,回头去看的时候发现涨价了,更加拼命的存钱,等我觉得差不多的时候,再回去发现已经被买走了。希望不会在垃圾堆看到这玩具,不然我依然会把它捡起来。
52.别忘了NSTimer会保留其目标对象
计时器是一个很方便很有用的对象,Foundation框架中有个类叫做NSTimer,开发者可以指定绝对的日期和时间,以便到时执行任务。
由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出问题。例如:
- (void)startPolling{
//因为target的对象是self 所以定时器是持有self的,而timer有是self的成员变量,所以就相互保留
_pollTimer =[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}
想要打破保留环:
只能改变实例变量或者令计时器无效。
也就说要不就调用[_pollTimer invalidate]; _pollTime = nil;
要不就改变实例变量。
除非所有的代码在你的手上,否则我们不确定这个定时器失效方法一定会被调用,而且即使满足没这种条件,这种通过调用某个方法来避免内存泄露的方法,不是一个好主意。那我们就选择通过改变实例变量的方法。
创造一个NSTimer的分类,然后新加入一个类方法
- (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
这段代码将计时器所执行的任务封装成了“块”,在调用计时器函数的时候,把它作为userInfo参数传进去。这个参数可以用来存放“不透明值(也就是全能数值id)”,只要计时器还有效,就会一直保留它。传入参数时要通过copy方法将block拷贝到“堆”上,否则等到稍后要执行它的时候,该块可能已经无效了。计时器现在的target是NSTimer类对象,是个单例,因此计时器是否保留他都无所谓。
然后block还是有个强引用的循环,这个简单就_ _weak就行了。
要点:
- NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计数器失效,另外,一次性的计时器在触发完任务之后也会失效。
- 反复执行任务的计时器(repeating timer),很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。
- 可以扩充NSTimer的功能,用”块“来打破保留环。不过,除非NSTimer将来在公共接口里提供此功能。否则必须创建分类,将相关代码加到其中。
结尾
自己写的笔记首先是用Pages写的,写完之后放到简书里面以为也就剩下个排版了,结果发现基本上每一个点的总结都不让自己满意,但是又想早点放上去,总感觉自己被什么追赶着,哈哈,本来写完笔记的时候是2W字的,结果到第二次发表的时候发现就成了2.5W了,需要改进的东西还是太多,希望朋友们有什么改进的提议都可以告诉我,我会一直补充这个笔记,然后抓紧改GitHub上的代码~