1. 引用计数
这个 part 真的是老生常谈,但是这本书用开关灯来模拟引用计数还挺有意思的~ 也就是第一个上班的人要开灯先,对应创建对象;然后进来一个人,相当于retain;出去一个人就是release;当计数为0屋里就没人了可以destroy也就是关灯啦~

alloc / new / copy 之类的自己创建自己持有也就是说它在创建那一瞬间就持有了它的引用,也就类似于第一个开办公室灯的人,开灯的那个瞬间,屋内的人已经是1了,也就是对象创建的瞬间引用计数已经+1了。
retain 用于持有已经创建好的对象,不是自己创建的。例如创建一个array,虽然obj指向了这个创建好的array,但是需要retain以下让引用计数+1:
id obj = [NSMutableArray array]; // 这里因为不是new or alloc or copy出来的,所以不会直接持有,也就是不是自己创建的
[obj retain];
如果你自己定义一个创建对象的方法叫 allocObject
, 因为它是 alloc 开头的,那么这个方法会被认为是自己创建对象 & 持有。也就是返回值会被自动retain,不需要手动retain一次~
那么创建一个对象将它转交出去,如果不是 alloc 开头的要怎么做呢?
- (id) object {
id obj = [[NSObject alloc] init]; // 此时obj持有了新的对象,所以这个对象不会释放
return obj; // 这里是有问题的哈,如果是alloc开头的方法可以这么return,因为外面会直接持有,但是不是alloc开头的是不ok的
}
id objNotCarry = [self object];
[objNotCarry retain];
这里其实就是为啥要有 autorelease 了,当 allocObject 的 block 走完其实已经没有指针指向创建的对象了,但外面 objNotCarry 这个时候并不持有对象,到retain的时候才是真的持有。为了防止 allocObject 在 block 结束直接释放新创建的对象,才衍生了autorelease,也就是其实 allocObject 方法 return 的是 [obj autoRealse],这样的话就不会 block 结束立刻把 obj 的指针 release 掉,要等一会儿到 runloop 循环结束的时候再 release,就留给了外面 objNotCarry 去retain 持有对象的时间~
- 下面以 GNUstep 为例康康对象的创建哈~
对象创建的时候其实调用得是 allocWithZone:
,进行内存分配、置0并返回指针,那么为啥这里要有zone的概念呢?(其实和copy的zone一样)其实是为了防止内存碎片化,但因为当前系统的内存管理已经很好了所以zone已经废弃了几乎。

然后是引用计数怎么存的呢?

其实是内存分配了 对象大小+1 的空间,alloc以后会把这些空间全部置0,返回从 p+1
也就是空间第二位开始的位置,第一位留给了retain。然后我们看下 retainCount
的实现:

获取 referCount 就是从 对象指针地址-1 的位置读一个数,然后+1返回回去。
- 苹果的肿么做的呢?
苹果实际使用 hashtable 来做引用计数表以及弱引用表哒~

- 内存修饰符
ARC 里面的 strong 修饰其实相当于在出了作用域以后自动执行一遍 [obj release]。
OC对象释放以后,表示OC对象占用的空间可以分配给别人。但是再分配给别人之前,这个空间仍然存在,对象的数据仍然存在。这个被释放的对象,就叫做僵尸对象。
使用野指针访问僵尸对象,有的时候会出问题报错(EXC_BAD_ACCESS),有的时候不会出问题。当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候,这个时候其实是可以访问的。因为对象的数据还在。当野指针指向的对象所占用的空间分配给了别人的时候,这个时候访问就会出问题。
书中说 weak 修饰为了防止快速被 release,其实也是用 autorelease 修饰,但是其实我们日常都知道如果用 weak 修饰,下一行代码再访问这个对象都是nil了,所以我感觉可能是做了修改叭,现在的 weak 可能不再自动 autorelease。我查了下的确从LLVM8.0以后就不会对weak延迟释放了哈:https://stackoverflow.com/questions/47388778/why-weak-variable-not-registered-in-autorelease-pool
另一个非显式的 __autorelease 是对指针的声明,这个其实之前我记得也有一篇说过 NSError 的传递就是,id *obj 其实对应的是 id __autorelease *obj,NSObject **obj 对应 NSObject * __autorelease *obj
。
- (void)performOperationWithError(NSError **error);
等价于
- (void)performOperationWithError(NSError * __autorelease *error);
像下面酱紫给autorelease赋值一个strong指针会报错的,必须要显示声明让两边的指针类型一致,另外还有一个关于异步设置error指针的有趣问题可以参考https://www.cnblogs.com/tiantianbobo/p/11653843.html:
不可编译:
NSError *error = nil;
NSError **pError = &error;
OK的:
NSError *error = nil;
NSError * __strong *pError = &error;
为什么二级指针默认是autorelease,上文中说是习惯,我看书中解释是只有自己生成&持有(也就是alloc / new / copy / mutableCopy)不需要考虑,其他情况下用对象指针参数取得非自己生成对象的时候都要 autorelease,但如果这么说是不是所有参数都是autorelease的呢,但实际不是二级指针的应该不是啊,感觉还是怪怪的,这个问题如果有朋友有想法也可以留言哈~~
这个可以打印 autorelease pool 里面的对象,我觉得不是很好用,其实也没啥用,如果延迟释放有问题其实自己能看出来:
extern void _objc_autoreleasePoolPrint();
_objc_autoreleasePoolPrint();

ARC 不能管理 C 对象内存,所以 struct 里面不用持有 OC 对象,如果想持有可以把它用 __unsafe_unretain 修饰OC对象,这样 arc 就不会管理它的内存了。
书中说 id
和 void *
在 ARC 下不能隐式转换,因为 void * 是 c指针,ARC不知道如何管理内存。但其实现在已经可以了,大概是自动加了 bridge 转换:
可以编译:
id obj = [[NSObject alloc] init];
void *p = obj;
bridge:不改变持有,源对象仍旧持有,如果转给 OC strong指针,那么 OC 也会持有
bridge_retain:转到哪里,哪里也持有对象。
bridge_transfer:源对象交出所有权
Blocks
这部分其实主要还是如何捕获外部变量,可以参考我之前的实验叭:https://www.jianshu.com/p/191553c37803
关于class isa指针可以参考这里的:https://www.jianshu.com/p/872ca1aa24e0

MRC 的时候 block 在栈上,ARC 的时候如果有 strong 指针指向block,这个block就会被拷贝到堆上,所以我们现在的 property 都是会给 block 设置copy,就是遗留习惯。以及如果 block 作为函数返回值也是会自动拷贝到堆的,但是如果block作为函数参数,是不会自动拷贝的,毕竟其实 block 的拷贝很耗CPU,所以不会无差别全部拷贝,于是就会出现下面的crash:
- (NSArray*) getBlockArray
{
int num = 916;
return [[NSArray alloc] initWithObjects:
^{ NSLog(@"this is block 0:%i", num); },
^{ NSLog(@"this is block 1:%i", num); },
^{ NSLog(@"this is block 2:%i", num); },
nil];
}
- (void)test
{
NSArray* obj = [self getBlockArray];
void (^blockObject)(void);
blockObject = [obj objectAtIndex:2];
blockObject();
}
调用test会发生crash 因为 EXC_BAD_ACCESS 野指针错误,如果想改好其实也很简单,让[NSArray alloc] initWithObjects:
的时候传入block的copy即可~

__block 修饰的对象在 block 被拷贝的时候也会一起被拷贝到堆上,其实也就是forwarding存在的意义,如果所有引用它的 block 都销毁了,他也会被销毁:

我们再来看一个对象捕获的例子:
- (void)viewDidLoad {
[super viewDidLoad];
[self captureObject];
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
typedef void (^blk_t)(id obj);
blk_t blk;
- (void)captureObject
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
输出:
2021-01-09 21:42:51.150534+0800 Example1[95705:1692026] array count = 1
2021-01-09 21:42:51.150646+0800 Example1[95705:1692026] array count = 2
2021-01-09 21:42:51.150741+0800 Example1[95705:1692026] array count = 3
这里的 array 作用域应该是在 captureObject 里面吖,为啥会 count = 3 呢?其实因为这个array被block捕获,而block被拷贝到堆上,所以它的作用域就超过了他本来的作用域。

即使加了 __block 的 weak 也是会释放的哈,因为weak没有强持有。我们通常会通过 weak 来解决循环引用的问题,但是其实也可以通过 __block 修饰,然后执行后置空指针来解决,这样解决的问题是如果block不执行就不能释放哈,但也是一种办法。

当block在堆上的时候,你对它 retain 才管用,如果在栈上无论怎么 retain 也没啥用,毕竟栈是出了作用域就会销毁的。
3. GCD
这个part我之前有好几篇一起写过其实,感兴趣的朋友往前翻一翻哈~
需要注意的一点可能是别用太多多线程,毕竟切换线程表也很费内存的,上下文切换也很麻烦。
附加一下用 GCD 多线程并行读取文件的操作(dispatch source 其实也有 read type):

原理:

网友评论