总结了Effective Objective-C以后,还想读一本进阶的iOS书,绝不犹豫选中了《Objective-C 高级编程》。html
这本书有三个章节,我针对每一章节进行总结并加上适当的扩展分享给你们。能够从下面这张图来看一下这三篇的总体结构:ios
注意,这个结构并不和书中的结构一致,而是以书中的结构为参考,稍做了调整。git
本篇是第一篇:引用计数,简单说两句: Objective-C经过 retainCount 的机制来决定对象是否须要释放。 每次runloop迭代结束后,都会检查对象的 retainCount,若是retainCount等于0,就说明该对象没有地方须要继续使用它,能够被释放掉了。不管是手动管理内存,仍是ARC机制,都是经过对retainCount来进行内存管理的。程序员
先看一下手动内存管理:github
我我的以为,学习一项新的技术以前,须要先了解一下它的核心思想。理解了核心思想以后,对技术点的把握就会更快一些:面试
从上面的思想来看,咱们对对象的操做能够分为三种:生成,持有,释放,再加上废弃,一共有四种。它们所对应的Objective-C的方法和引用计数的变化是:编程
对象操做 | Objecctive-C方法 | 引用计数的变化 |
---|---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 | +1 |
持有对象 | retain方法 | +1 |
释放对象 | release方法 | -1 |
废弃对象 | dealloc方法 | 无 |
用书中的图来直观感觉一下这四种操做:数组
下面开始逐一解释上面的四条思想:多线程
在生成对象时,使用如下面名称开头的方法生成对象之后,就会持有该对象:app
举个🌰:
id obj = [[NSObject alloc] init];//持有新生成的对象
复制代码
这行代码事后,指向生成并持有[[NSObject alloc] init]的指针被赋给了obj,也就是说obj这个指针强引用[[NSObject alloc] init]这个对象。
一样适用于new方法:
id obj = [NSObject new];//持有新生成的对象
复制代码
注意: 这种将持有对象的指针赋给指针变量的状况不仅局限于上面这四种方法名称,还包括以他们开头的全部方法名称:
举个🌰:
id obj1 = [obj0 allocObject];//符合上述命名规则,生成并持有对象
复制代码
它的内部实现:
- (id)allocObject
{
id obj = [[NSObject alloc] init];//持有新生成的对象
return obj;
}
复制代码
反过来,若是不符合上述的命名规则,那么就不会持有生成的对象, 看一个不符合上述命名规则的返回对象的createObject方法的内部实现🌰:
- (id)createObject
{
id obj = [[NSObject alloc] init];//持有新生成的对象
[obj autorelease];//取得对象,但本身不持有
return obj;
}
复制代码
经由这个方法返回之后,没法持有这个返回的对象。由于这里使用了autorelease。autorelease提供了这样一个功能:在对象超出其指定的生存范围时可以自动并正确地释放(详细会在后面介绍)。
也就是说,生成一个调用方不持有的对象是能够经过autorelease来实现的(例如NSMutableArray的array类方法)。
个人我的理解是:经过autorelease方法,使对象的持有权转移给了自动释放池。因此实现了:调用方拿到了对象,但这个对象还不被调用方所持有。
由这个不符合命名规则的例子来引出思想二:
咱们如今知道,仅仅经过上面那个不符合命名规则的返回对象实例的方法是没法持有对象的。可是咱们能够经过某个操做来持有这个返回的对象:这个方法就是经过retain方法来让指针变量持有这个新生成的对象:
id obj = [NSMutableArray array];//非本身生成并持有的对象
[obj retain];//持有新生成的对象
复制代码
注意,这里[NSMutableArray array]返回的非本身持有的对象正是经过上文介绍过的autorelease方法实现的。因此若是想持有这个对象,须要执行retain方法才能够。
对象的持有者有义务在再也不须要这个对象的时候主动将这个对象释放。注意,是有义务,而不是有权利,注意两个词的不一样。
来看一下释放对象的例子:
id obj = [[NSObject alloc] init];//持有新生成的对象
[obj doSomething];//使用该对象作一些事情
[obj release];//事情作完了,释放该对象
复制代码
一样适用于非本身生成并持有的对象(参考思想二):
id obj = [NSMutableArray array];//非本身生成并持有的对象
[obj retain];//持有新生成的对象
[obj soSomething];//使用该对象作一些事情
[obj release];//事情作完了,释放该对象
复制代码
可能遇到的面试题:调用对象的release方法会销毁对象吗? 答案是不会:调用对象的release方法只是将对象的引用计数器-1,当对象的引用计数器为0的时候会调用了对象的dealloc 方法才能进行释放对象的内存。
在释放对象的时候,咱们只能释放已经持有的对象,非本身持有的对象是不能被本身释放的。这很符合常识:就比如你本身才能从你本身的银行卡里取钱,取别人的卡里的钱是不对的(除非他的钱归你管。。。只是随便举个例子)。
id obj = [[NSObject alloc] init];//持有新生成的对象
[obj doSomething];//使用该对象
[obj release];//释放该对象,再也不持有了
[obj release];//释放已经废弃了的对象,崩溃
复制代码
id obj = [NSMutableArray array];//非本身生成并持有的对象
[obj release];//释放了非本身持有的对象
复制代码
思考:哪些状况会使对象失去拥有者呢?
如今知道了引用计数式内存管理的四个思想,咱们再来看一下四个操做引用计数的方法:
某种意义上,GNUstep 和 Foundation 框架的实现是类似的。因此这本书的做者经过GNUstep的源码来推测了苹果Cocoa框架的实现。
下面开始针对每个方法,同时用GNUstep和苹果的实现方式(追踪程序的执行和做者的猜想)来对比一下各自的实现。
//GNUstep/modules/core/base/Source/NSObject.m alloc:
+ (id) alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+ (id) allocWithZone: (NSZone*)z
{
return NSAllocateObject(self, 0, z);
}
复制代码
这里NSAllocateObject方法分配了对象,看一下它的内部实现:
//GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:
struct obj_layout {
NSUInteger retained;
};
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
{
int size = 计算容纳对象所需内存大小;
id new = NSZoneMalloc(zone, 1, size);//返回新的实例
memset (new, 0, size);
new = (id)&((obj)new)[1];
}
复制代码
- NSAllocateObject函数经过NSZoneMalloc函数来分配存放对象所须要的内存空间。
- obj_layout是用来保存引用计数,并将其写入对象内存头部。
对象的引用计数能够经过retainCount方法来取得:
GNUstep/modules/core/base/Source/NSObject.m retainCount:
- (NSUInteger) retainCount
{
return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject)
{
return ((obj_layout)anObject)[-1].retained;
}
复制代码
咱们能够看到,给NSExtraRefCount传入anObject之后,经过访问对象内存头部的.retained变量,来获取引用计数。
//GNUstep/modules/core/base/Source/NSObject.m retain:
- (id)retain
{
NSIncrementExtraRefCount(self);
return self;
}
inline void NSIncrementExtraRefCount(id anObject)
{
//retained变量超出最大值,抛出异常
if (((obj)anObject)[-1].retained == UINT_MAX - 1){
[NSException raise: NSInternalInconsistencyException
format: @"NSIncrementExtraRefCount() asked to increment too far”];
}
((obj_layout)anObject)[-1].retained++;//retained变量+1
}
复制代码
//GNUstep/modules/core/base/Source/NSObject.m release
- (void)release
{
//若是当前的引用计数 = 0,调用dealloc函数
if (NSDecrementExtraRefCountWasZero(self))
{
[self dealloc];
}
}
BOOL NSDecrementExtraRefCountWasZero(id anObject)
{
//若是当前的retained值 = 0.则返回yes
if (((obj)anObject)[-1].retained == 0){
return YES;
}
//若是大于0,则-1,并返回NO
((obj)anObject)[-1].retained--;
return NO;
}
复制代码
//GNUstep/modules/core/base/Source/NSObject.m dealloc
- (void) dealloc
{
NSDeallocateObject (self);
}
inline void NSDeallocateObject(id anObject)
{
obj_layout o = &((obj_layout)anObject)[-1];
free(o);//释放
}
复制代码
总结一下上面的几个方法:
下面看一下苹果的实现:
经过在NSObject类的alloc类方法上设置断点,咱们能够看到执行所调用的函数:
retainCount:
咱们能够看到他们都调用了一个共同的 __CFdoExternRefOperation 方法。
看一下它的实现:
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对象的散列表(obj);
int count;
switch (op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
break;
case OPERATION_retain:
count = CFBasicHashAddValue(table, obj);
return obj;
case OPERATION_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}
复制代码
能够看出,__CFDoExternRefOperation经过switch语句 针对不一样的操做来进行具体的方法调用,若是 op 是 OPERATION_retain,就去掉用具体实现 retain 的方法,以此类推。
能够猜测上层的retainCount,retain,release方法的实现:
- (NSUInteger)retainCount
{
return (NSUInteger)____CFDoExternRefOperation(OPERATION_retainCount,self);
}
- (id)retain
{
return (id)____CFDoExternRefOperation(OPERATION_retain,self);
}
//这里返回值应该是id,原书这里应该是错了
- (id)release
{
return (id)____CFDoExternRefOperation(OPERATION_release,self);
}
复制代码
咱们观察一下switch里面每一个语句里的执行函数名称,彷佛和散列表(Hash)有关,这说明苹果对引用计数的管理应该是经过散列表来执行的。
在这张表里,key为内存块地址,而对应的值为引用计数。也就是说,它保存了这样的信息:一些被引用的内存块各自对应的引用计数。
那么使用散列表来管理内存有什么好处呢?
由于计数表保存内存块地址,咱们就能够经过这张表来:
当对象超出其做用域时,对象实例的release方法就会被调用,autorelease的具体使用方法以下:
全部调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release方法(引用计数-1):
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];//至关于obj调用release方法
复制代码
NSRunLoop在每次循环过程当中,NSAutoreleasePool对象都会被生成或废弃。 也就是说,若是有大量的autorelease变量,在NSAutoreleasePool对象废弃以前(一旦监听到RunLoop即将进入睡眠等待状态,就释放NSAutoreleasePool),都不会被销毁,容易致使内存激增的问题:
for (int i = 0; i < imageArray.count; i++)
{
UIImage *image = imageArray[i];
[image doSomething];
}
复制代码
所以,咱们有必要在适当的时候再嵌套一个自动释放池来管理临时生成的autorelease变量:
for (int i = 0; i < imageArray.count; i++)
{
//临时pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *image = imageArray[i];
[image doSomething];
[pool drain];
}
复制代码
可能会出的面试题:何时会建立自动释放池? 答:运行循环检测到事件并启动后,就会建立自动释放池,并且子线程的 runloop 默认是不工做的,没法主动建立,必须手动建立。 举个🌰: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。不然在出了做用域之后,自动释放对象会由于没有自动释放池去处理本身而形成内存泄露。
和上文同样,咱们仍是经过GNUstep和苹果的实现来分别看一下。
//GNUstep/modules/core/base/Source/NSObject.m autorelease
- (id)autorelease
{
[NSAutoreleasePool addObject:self];
}
复制代码
若是调用NSObject类的autorelease方法,则该对象就会被追加到正在使用的NSAutoreleasePool对象中的数组里(做者假想了一个简化的源代码):
//GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject
+ (void)addObject:(id)anObj
{
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象
if (pool != nil){
[pool addObject:anObj];
}else{
NSLog(@"NSAutoreleasePool对象不存在");
}
}
- (void)addObject:(id)anObj
{
[pool.array addObject:anObj];
}
复制代码
也就是说,autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法,而后这个对象就被追加到正在使用的NSAutoreleasePool对象中的数组里。
再来看一下NSAutoreleasePool的drain方法:
- (void)drain
{
[self dealloc];
}
- (void)dealloc
{
[self emptyPool];
[array release];
}
- (void)emptyPool
{
for(id obj in array){
[obj release];
}
}
复制代码
咱们能够看到,在emptyPool方法里,确实是对数组里每个对象进行了release操做。
咱们能够经过objc4/NSObject.mm来确认苹果中autorelease的实现:
objc4/NSObject.mm AutoreleasePoolPage
class AutoreleasePoolPage
{
static inline void *push()
{
//生成或者持有 NSAutoreleasePool 类对象
}
static inline void pop(void *token)
{
//废弃 NSAutoreleasePool 类对象
releaseAll();
}
static inline id autorelease(id obj)
{
//至关于 NSAutoreleasePool 类的 addObject 类方法
AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;
autoreleaesPoolPage->add(obj)
}
id *add(id obj)
{
//将对象追加到内部数组中
}
void releaseAll()
{
//调用内部数组中对象的 release 方法
}
};
//压栈
void *objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
//出栈
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}
复制代码
来看一下外部的调用:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于 objc_autoreleasePoolPush
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于 objc_autorelease(obj)
[NSAutoreleasePool showPools];
// 查看 NSAutoreleasePool 情况
[pool drain];
// 等同于 objc_autoreleasePoolPop(pool)
复制代码
看函数名就能够知道,对autorelease分别执行push、pop操做。销毁对象时执行release操做。
可能出现的面试题:苹果是如何实现autoreleasepool的? autoreleasepool以一个队列数组的形式实现,主要经过下列三个函数完成. • objc_autoreleasepoolPush(压入) • objc_autoreleasepoolPop(弹出) • objc_autorelease(释放内部)
上面学习了非ARC机制下的手动管理内存思想,针对引用计数的操做和自动释放池的相关内容。如今学习一下在ARC机制下的相关知识。
ARC和非ARC机制下的内存管理思想是一致的:
在ARC机制下,编译器就能够自动进行内存管理,减小了开发的工做量。但咱们有时仍须要四种全部权修饰符来配合ARC来进行内存管理
可是,在ARC机制下咱们有的时候须要追加全部权声明(如下内容摘自官方文档):
下面分别讲解一下这几个修饰符:
__strong修饰符 是id类型和对象类型默认的全部权修饰符:
id obj = [NSObject alloc] init];
复制代码
等同于:
id __strong obj = [NSObject alloc] init];
复制代码
看一下内存管理的过程:
{
id __strong obj = [NSObject alloc] init];//obj持有对象
}
//obj超出其做用域,强引用失效
复制代码
__strong修饰符表示对对象的强引用。持有强引用的变量在超出其做用域时被废弃。
在__strong修饰符修饰的变量之间相互赋值的状况:
id __strong obj0 = [[NSObject alloc] init];//obj0 持有对象A
id __strong obj1 = [[NSObject alloc] init];//obj1 持有对象B
id __strong obj2 = nil;//ojb2不持有任何对象
obj0 = obj1;//obj0强引用对象B;而对象A再也不被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(如今obj0,ojb1,obj2都强引用对象B)
obj1 = nil;//obj1再也不强引用对象B
obj0 = nil;//obj0再也不强引用对象B
obj2 = nil;//obj2再也不强引用对象B,再也不有任何强引用引用对象B,对象B被废弃
复制代码
并且,__strong可使一个变量初始化为nil:id __strong obj0; 一样适用于:id __weak obj1; id __autoreleasing obj2;
作个总结:被__strong修饰后,至关于强引用某个对象。对象一旦有一个强引用引用本身,引用计数就会+1,就不会被系统废弃。而这个对象若是再也不被强引用的话,就会被系统废弃。
生成并持有对象:
{
id __strong obj = [NSObject alloc] init];//obj持有对象
}
复制代码
编译器的模拟代码:
id obj = objc_mesgSend(NSObject, @selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);//超出做用域,释放对象
复制代码
再看一下使用命名规则之外的构造方法:
{
id __strong obj = [NSMutableArray array];
}
复制代码
编译器的模拟代码:
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
复制代码
objc_retainAutoreleasedReturnValue的做用:持有对象,将对象注册到autoreleasepool并返回。
一样也有objc_autoreleaseReturnValue,来看一下它的使用:
+ (id)array
{
return [[NSMutableArray alloc] init];
}
复制代码
编译器的模拟代码:
+ (id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj,, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
复制代码
objc_autoreleaseReturnValue:返回注册到autoreleasepool的对象。
__weak修饰符大多解决的是循环引用的问题:若是两个对象都互相强引用对方,同时都失去了外部对本身的引用,那么就会造成“孤岛”,这个孤岛将永远没法被释放,举个🌰:
@interface Test:NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (id)init
{
self = [super init];
return self;
}
- (void)setObject:(id __strong)obj
{
obj_ = obj;
}
@end
复制代码
{
id test0 = [[Test alloc] init];//test0强引用对象A
id test1 = [[Test alloc] init];//test1强引用对象B
[test0 setObject:test1];//test0强引用对象B
[test1 setObject:test0];//test1强引用对象A
}
复制代码
由于生成对象(第一,第二行)和set方法(第三,第四行)都是强引用,因此会形成两个对象互相强引用对方的状况:
因此,咱们须要打破其中一种强引用:
@interface Test:NSObject
{
id __weak obj_;//由__strong变成了__weak
}
- (void)setObject:(id __strong)obj;
@end
复制代码
这样一来,两者就只是弱引用对方了:
{
id __weak obj1 = obj;
}
复制代码
编译器的模拟代码:
id obj1;
objc_initWeak(&obj1,obj);//初始化附有__weak的变量
id tmp = objc_loadWeakRetained(&obj1);//取出附有__weak修饰符变量所引用的对象并retain
objc_autorelease(tmp);//将对象注册到autoreleasepool中
objc_destroyWeak(&obj1);//释放附有__weak的变量
复制代码
这确认了__weak的一个功能:使用附有__weak修饰符的变量,便是使用注册到autoreleasepool中的对象。
这里须要着重讲解一下objc_initWeak方法和objc_destroyWeak方法:
注意:由于同一个对象能够赋值给多个附有__weak的变量中,因此对于同一个键值,能够注册多个变量的地址。
当一个对象再也不被任何人持有,则须要释放它,过程为:
ARC下,能够用@autoreleasepool来替代NSAutoreleasePool类对象,用__autoreleasing修饰符修饰变量来替代ARC无效时调用对象的autorelease方法(对象被注册到autoreleasepool)。
说到__autoreleasing修饰符,就不得不提__weak:
id __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);
复制代码
等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@",[tmp class]);//实际访问的是注册到自动个释放池的对象
复制代码
注意一下两段等效的代码里,NSLog语句里面访问的对象是不同的,它说明:在访问__weak修饰符的变量(obj1)时必须访问注册到autoreleasepool的对象(tmp)。为何呢?
由于__weak修饰符只持有对象的弱引用,也就是说在未来访问这个对象的时候,没法保证它是否尚未被废弃。所以,若是把这个对象注册到autoreleasepool中,那么在@autoreleasepool块结束以前都能确保该对象存在。
将对象赋值给附有__autoreleasing修饰符的变量等同于ARC无效时调用对象的autorelease方法。
@autoreleasepool{
id __autoreleasing obj = [[NSObject alloc] init];
}
复制代码
编译器的模拟代码:
id pool = objc_autoreleasePoolPush();//pool入栈
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);//pool出栈
复制代码
在这里咱们能够看到pool入栈,执行autorelease,出栈的三个方法。
咱们知道了在ARC机制下编译器会帮助咱们管理内存,可是在编译期,咱们仍是要遵照一些规则,做者为咱们列出了如下的规则:
在ARC机制下使用retain/release/retainCount/autorelease方法,会致使编译器报错。
在ARC机制下使用NSAllocateObject/NSDeallocateObject方法,会致使编译器报错。
对象的生成/持有的方法必须遵循如下命名规则:
前四种方法已经介绍完。而关于init方法的要求则更为严格:
对象被废弃时,不管ARC是否有效,系统都会调用对象的dealloc方法。
咱们只能在dealloc方法里写一些对象被废弃时须要进行的操做(例如移除已经注册的观察者对象)可是不能手动调用dealloc方法。
注意在ARC无效的时候,还须要调用[super dealloc]:
- (void)dealloc
{
//该对象的处理
[super dealloc];
}
复制代码
ARC下须使用使用@autorelease块代替NSAutoreleasePool。
NSZone已经在目前的运行时系统(__OBC2__被设定的环境)被忽略了。
C语言的结构体若是存在Objective-C对象型变量,便会引发错误,由于C语言在规约上没有方法来管理结构体成员的生存周期 。
非ARC下,这两个类型是能够直接赋值的
id obj = [NSObject alloc] init];
void *p = obj;
id o = p;
复制代码
可是在ARC下就会引发编译错误。为了不错误,咱们须要经过__bridege来转换。
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//显式转换
id o = (__bridge id)p;//显式转换
复制代码
来看一下属性的声明与全部权修饰符的关系
属性关键字 | 全部权 修饰符 |
---|---|
assign | __unsafe_unretained |
copy | __strong |
retain | __strong |
strong | __strong |
__unsafe_unretained | __unsafe_unretained |
weak | __weak |
说一下__unsafe_unretained: __unsafe_unretained表示存取方法会直接为实例变量赋值。
这里的“unsafe”是相对于weak而言的。咱们知道weak指向的对象被销毁时,指针会自动设置为nil。而__unsafe_unretained却不会,而是成为空指针。须要注意的是:当处理非对象属性的时候就不会出现空指针的问题。
这样第一章就介绍完了,第二篇会在下周一发布^^
本文已经同步到我的博客:传送门
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。
由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。
并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~