0x00 问题的引入

  • 前一阵子表哥给了我一道知乎的iOS开发岗位面试题,听说还是那种类似于“一票否决”的题目,考察应试者的编程能力。我仔细一看是关于MRC的一道题,也就是在考察Reference Counting。(代码为了方便运行测试,略有改动,但是核心思路无变化)
// 应用背景:MRC模式
// 请说出所有NSLog的输出值,并解释理由。

#import <Foundation/Foundation.h>

@interface Zhihu : NSObject

+ (int) setKanShanToZhihu;

@end

@implementation Zhihu

+ (int) setKanShanToZhihu {
    NSMutableArray *zhihu=[[[NSMutableArray alloc]init]retain];
    NSObject *kanshan=[[NSObject alloc]init];
    [kanshan retain];
    [zhihu addObject:kanshan];
    NSLog(@"%d",(int)[kanshan retainCount]);
    [kanshan retain];
    [kanshan release];
    [kanshan release];
    NSLog(@"%d",(int)[kanshan retainCount]);
    [zhihu removeAllObjects];
    NSLog(@"%d",(int)[kanshan retainCount]);
    [kanshan release];
    return (int)[kanshan retainCount]+(int)[zhihu retainCount];
}

@end

int main(int argc, const char * argv[]) {
    NSLog(@"%d",[Zhihu setKanShanToZhihu]);
    return 0;
}
  • 大家可以去编译一下这个题目,新建一个Xcode工程,在Compile Source中加入-fno-objc-arc,关闭ARC,运行结果是3 2 1 3

0x01 MRC和ARC

  • 最早的时候Objective-C和C++一样,也是手动管理内存的。不过OC使用的是Reference Counting(引用计数)的方式,也就是说,同一个内存空间,引用计数显示了目前有多少个指针正在指向这个内存空间。显然,当引用计数等于0的时候,这块内存就不再有用了,系统就会将其空间释放。这里面OC就提供了一些方法,允许程序员管理引用计数。
//对该对象的引用计数+1,返回一个新的指针指向该内存
- (instancetype) retain;
//对该对象的引用计数-1
- (oneway void) release;
//输出该对象的引用计数数值
- (NSUInteger) retainCount;
  • 大家也看出来了,这样管理也很麻烦,程序员需要关注大量的指针问题,还有可能出现强引用循环的问题。所以从iOS 5.0开始,苹果引入了ARC机制,ARC即自动引用计数(Automatic Reference Counting),这才把iOS开发者们从引用计数中解放出来,而原来的方式就称为MRC了,即手动引用计数(Manual Reference Counting)。
  • 既然iOS 5之后就可以用ARC了,现在App Store的最低支持版本已经是iOS 8.0了,为什么知乎的面试题还要考MRC呢?显然是为了考察iOS面试者对于引用计数机制的了解啦。

0x02 题目解答

  • 知乎这个面试题确实很考验iOS面试者的编程功底,将手动引用计数的管理应用到了极致。
  • 首先第1句创建了一个可变长度的数组,变量名字为zhihu
 NSMutableArray *zhihu=[[[NSMutableArray alloc]init]retain];
  • 这里我们发送alloc消息,就会分配一块内存,再发送init消息进行初始化,这样本身就会返回一个指针,引用计数变为1。但是偏偏又调用了一次retain,这时候引用计数又会加1,变为2。所以这一个语句使得引用计数+2。
  • 第2句和第3句正常创建了一个对象kanshan,引用计数为1,随后又进行了retain,引用计数为2。
NSObject *kanshan=[[NSObject alloc]init];
[kanshan retain];
  • 第4句通过向zhihu数组发送addObject消息,将kanshan对象加入数组。
[zhihu addObject:kanshan];
  • 这里因为向数组发送了addObject消息,数组中就也会保存一个指针指向这片内存,kanshan的引用计数再次加1。所以第一次输出的结果为3
  • 然后连续经过三句话,引用计数先加1后减2,结果当然是减1。
[kanshan retain];
[kanshan release];
[kanshan release];
  • 所以第二次输出的结果为2
  • 随后又向zhihu发送了removeAllObjects消息,清空了整个数组
[zhihu removeAllObjects];
  • 这时候kanshan的引用计数也会受到影响,因为它不再保存在数组中,所以引用计数减1,第三次输出的结果是1
  • 最后又调用了一次release
[kanshan release];
return (int)[kanshan retainCount]+(int)[zhihu retainCount];
  • 按理来说kanshan的引用计数应该降为0了,被释放。但是在发送retainCount消息的时候,为了避免对已经释放的内存发送消息,系统会自动持有一个指向该块内存的指针。而zhihu的引用计数还是为2,所以,最后返回值的总计数值是3

0x03 API文档中对retainCount消息的说明

  • 在API文档中,苹果直戳了当的表示:
Do not use this method.
  • 让我们不要使用这个方法,苹果的理由是这样表述的:
This method is of no value in debugging memory management issues. Because any number of framework objects may have retained an object in order to hold references to it, while at the same time autorelease pools may be holding any number of deferred releases on an object, it is very unlikely that you can get useful information from this method.
  • 这个函数对调试内存管理问题没有用处,因为所有的在framework中的对象都会保留了一个对象以便于持有对这个对象的引用。同时在autorelease pool中对象也可能被延迟释放。所以说我们不可能从这里获取有关内存管理的有用信息。
  • 所以,即使我们把kanshan的引用计数降为0,系统仍然会保留一个指针指向kanshan,以持有一个对kanshan的引用,这样我们在调用retainCount的时候才不会出现错误。

0x04 总结

  • 实际上苹果费尽心思让我们不要使用MRC,不要去考虑引用计数的问题。为了解决强引用循环的问题,苹果甚至设计了weak指针。在最新的Swift语言中,也必须进行一些unsafe的生命才允许你手动管理内存。但是作为一名合格的iOS开发人员,个人认为还是有必要了解一些关于引用计数的知识的,这也是知乎出这道题的意义。

雾花_小路
114 声望9 粉丝

Teresa love; Major in Information Security in CAUC; Favorite: Reverse Engineering / iOS Developing.