017-深入学习 ARC 及内存管理

自动引用计数(Automatic Reference Counting)是指在内存管理中对引用计数进行自动计数的技术,适用条件为:

  • Xcode 4.2 以上
  • LLVM 编译器 3.0 以上
  • 设置 ARC 有效

iOS 内存管理的实现

内存管理的思考方式

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也可以持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

这四条规则在 MRC 和 ARC 中都适用,但略有差别,要注意这里的自己,可以理解为对象的使用环境,也可以理解为编程人员自身。

对象操作与 OC 方法的对应

对象操作 OC 方法
生成并持有对象 alloc/new/copy/mutableCopy系列方法
持有对象 retain
释放对象 release
废弃对象 dealloc

方法实现

由于包含 NSObject 的 Foundation 框架没有公开,NSObject 包含的 alloc/new/copy 等方法也就无法看到源码,但是可以参考 Cocoa 框架的互换框架 GNUstep。

alloc

id obj = [NSObject alloc];

实现如下

+ (id) alloc
{
  return [self allocWithZone: NSDefaultMallocZone()];
}

+ (id) allocWithZone: (NSZone*)z
{
  return NSAllocateObject (self, 0, z);
}

struct obj_layout {
  char  padding[__BIGGEST_ALIGNMENT__ - ((UNP % __BIGGEST_ALIGNMENT__)
    ? (UNP % __BIGGEST_ALIGNMENT__) : __BIGGEST_ALIGNMENT__)];
  gsrefcount_t  retained;
};

NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
  id    new;
  int   size;

  NSCAssert((!class_isMetaClass(aClass)), @"Bad class for new object");
  size = class_getInstanceSize(aClass) + extraBytes + sizeof(struct obj_layout);
  if (zone == 0)
    {
      zone = NSDefaultMallocZone();
    }
  new = NSZoneMalloc(zone, size);
  if (new != nil)
    {
      memset (new, 0, size);
      new = (id)&((obj)new)[1];
      object_setClass(new, aClass);
      AADD(aClass, new);
    }
  if (0 == cxx_construct)
    {
      cxx_construct = sel_registerName(".cxx_construct");
      cxx_destruct = sel_registerName(".cxx_destruct");
    }
  callCXXConstructors(aClass, new);

  return new;
}

简单来说,NSAllocteObject 通过 NSZoneMalloc 函数来分配存放对象的内存空间,置 0 后返回。

简化版代码如下

struct obj_layout {
   NSUInteger retained;
}

+ (id)alloc
{
    int size = sizeOf(struct obj_layout) + 对象大小;
    struct obj_layout *p = (struct obj_layout *)calloc(1, size);
    return (id)(p + 1);
}

obj_layout 结构体用于存放 retained 引用计数,最后返回的指针不包含它。

获得一个对象的引用计数可以用 retainCount 方法。

- (NSUInteger) retainCount
{
  return NSExtraRefCount(self) + 1;
}

inline NSUInteger
NSExtraRefCount(id anObject)
{
  return ((obj)anObject)[-1].retained;
}

在 C 语言中对于一个指针 p 调用 p[-1] 相当于 p - 1,获取指针所指地址前一位的地址。

这里定义每一个对象都有一个隐式的为 1 的引用计数,同时有一个外部引用计数,所以实际上 alloc 之后的外部引用计数为 0,所以调用 retainCount 方法的返回值为 1。

retain

[obj retain];

实现如下

- (id) retain
{
  NSIncrementExtraRefCount(self);
  return self;
}

inline void
NSIncrementExtraRefCount(id anObject)
{
  BOOL  tooFar = NO;

#if defined(GSATOMICREAD)
  /* I've seen comments saying that some platforms only support up to
   * 24 bits in atomic locking, so raise an exception if we try to
   * go beyond 0xfffffe.
   */
  if (GSAtomicIncrement((gsatomic_t)&(((obj)anObject)[-1].retained))
    > 0xfffffe)
    {
      tooFar = YES;
    }
#else   /* GSATOMICREAD */
  NSLock *theLock = GSAllocationLockForObject(anObject);

  [theLock lock];
  if (((obj)anObject)[-1].retained > 0xfffffe)
    {
      tooFar = YES;
    }
  else
    {
      ((obj)anObject)[-1].retained++;
    }
  [theLock unlock];
#endif  /* GSATOMICREAD */
  if (YES == tooFar)
    {
      static NSHashTable        *overrun = nil;
      [gnustep_global_lock lock];
      if (nil == overrun)
        {
          overrun = NSCreateHashTable(NSNonRetainedObjectHashCallBacks, 0);
        }
      if (0 == NSHashGet(overrun, anObject))
        {
          NSHashInsert(overrun, anObject);
        }
      else
        {
          tooFar = NO;
        }
      [gnustep_global_lock lock];
      if (YES == tooFar)
        {
          NSString      *base;

          base = [NSString stringWithFormat: @"<%s: %p>",
            class_getName([anObject class]), anObject];
          [NSException raise: NSInternalInconsistencyException
            format: @"NSIncrementExtraRefCount() asked to increment too far"
            @" for %@ - %@", base, anObject];
        }
    }
}

主要是检查是否数值过大而溢出,未溢出则会把 retained 的变量自增一。

release

[obj release];

实现如下

- (oneway void) release
{
  if (NSDecrementExtraRefCountWasZero(self))
    {
#  ifdef OBJC_CAP_ARC
      objc_delete_weak_refs(self);
#  endif
      [self dealloc];
    }
}

inline BOOL

NSDecrementExtraRefCountWasZero(id anObject)
{
    if (((struct obj_layout *)anObject)[-1].retained == 0) {
        return YES;
    } else {
        ((struct obj_layout *)anObject)[-1].retained == 0) {
            return NO;
        }
    }
}

这里可以理解下,当 retained 值为 0 的时候,调用 retainCount 方法返回 1,此时调用 release 方法就会执行 dealloc 方法,因为没有变量再持有这个对象。

dealloc

- (void) dealloc
{
  NSDeallocateObject (self);
}

inline void
NSDeallocateObject(id anObject)
{
  Class aClass = object_getClass(anObject);

  if ((anObject != nil) && !class_isMetaClass(aClass))
    {
      obj   o = &((obj)anObject)[-1];
      NSZone    *z = NSZoneFromPointer(o);

      /* Call the default finalizer to handle C++ destructors.
       */
      (*finalize_imp)(anObject, finalize_sel);

      AREM(aClass, (id)anObject);
      if (NSZombieEnabled == YES)
    {
      GSMakeZombie(anObject, aClass);
      if (NSDeallocateZombies == YES)
        {
          NSZoneFree(z, o);
        }
    }
      else
    {
      object_setClass((id)anObject, (Class)(void*)0xdeadface);
      NSZoneFree(z, o);
    }
    }
  return;
}

NSDeallocateObject 会释放掉最开始 alloc 开辟的内存空间。

苹果的实现略有不同,没有用结构体而是用散列表(引用计数表)来管理,后面对于 __weak 变量也用到了散列表。散列表中键值为内存块地址的散列值,这样可以方便地从各个记录追溯到各对象的内存块,也有助于检测各对象的持有者是否存在。

autorelease 与实现

autorelease 在 MRC 和 ARC 中使用都比较广泛,在 MRC 中使用方法如下:

  1. 生成并持有 NSAutoreleasePool 对象
  2. 调用已分配对象的 autorelease 实例方法
  3. 废弃 NSAutoreleasePool 对象

而在 ARC 中用 @autoreleasingpool 和 __autoreleasing 修饰符来实现。

同时在 Cocoa 框架中,相当于程序主循环的 NSRunLoop 或者在其他程序可运行的地方,对 NSAutoreleasePool 对象进行生成、持有和废弃处理,所以不一定非要手动创建 NSAutoreleasePool 对象。但是如果是大量产生 autorelease 对象的场景,只要不废弃 NSAutoreleasePool 对象,生成的对象就不会被释放,可能产生内存不够的现象。

在 GNUstep 中

[obj autorelease]

实现如下

- (id) autorelease
{
  if (double_release_check_enabled)
    {
      NSUInteger release_count;
      NSUInteger retain_count = [self retainCount];
      release_count = [autorelease_class autoreleaseCountForObject:self];
      if (release_count > retain_count)
        [NSException
      raise: NSGenericException
      format: @"Autorelease would release object too many times.\n"
      @"%"PRIuPTR" release(s) versus %"PRIuPTR" retain(s)",
      release_count, retain_count];
    }

  (*autorelease_imp)(autorelease_class, autorelease_sel, self);
  return self;
}

关键的语句是最后一句(*autorelease_imp)(autorelease_class, autorelease_sel, self);,这里用到了 "IMP Caching" 技术来缓存经常被调用的方法的结果值,实际的方法调用就是使用缓存的结果值

    autorelease_class = [NSAutoreleasePool class];
    autorelease_sel = @selector(addObject:);
    autorelease_imp = [autorelease_class methodForSelector: autorelease_sel];

所以实际上它等同于下面的语句

[NSAutoreleasePool addObject:self];

而 NSAutoreleasePool 实际上是维护一个列表,在列表内存储各个 autorelease 对象。

苹果封装了一些方法,用动态数组来存储 authorelease 对象,下面是简化代码

class AutoreleasePoolPage
{
    static inline void *push() 
    {
        生成或持有 NSAutoreleasePool 对象
    }
    
    static inline void pop(void *token) 
    {
        废弃 NSAutoreleasePool 对象
        releaseAll();
    }
    
    static inline id autorelease(id obj)
    {
        相当于 addObject
    }
    
    void releaseAll()
    {
        依次调用数组中对象的 release 实例方法
    }
}

有两个非公开类方法可以确认并打印出 autoreleasePool 的状态

  • [NSAutoreleasePool showPools] 这个方法在 ARC 下不可用

  • _objc_autoreleasePoolPrint()

    这个方法使用前要先声明,extern void _objc_autoreleasePoolPrint();

要注意一点,对于 Foundation 框架中的对象调用 autorelease 方法时,实际是调用 NSObject 的 autorelease 方法,但是 NSAutoreleasePool 类的 autorelease 方法被重栽了所以调用会发生异常。

ARC 规则

iOS 内存管理的思考方式本质在 ARC 中并未改变,但是 ARC 将实现方式做了改动,能便捷可靠地管理对象的生命周期。

ARC 权限可以设置为单个文件的属性,ARC 和 非 ARC 文件可以共存于一个项目中。

所有权修饰符

ARC 有效时,id 类型和 OC 对象类型与 C 语言其他类型不同,必须加上所有权修饰符

  • __strong 默认修饰符
  • __weak
  • __unsafe_unretained
  • _autoreleasing

__strong 修饰符可以用于方法参数上,这样传入的参数就会被使用环境强持有。

- (void)setObject: (id __strong) target

__strong__weak__autoreleasing 可以保证将附有这些修饰符的自动变亮初始化为 nil。

__weak 修饰符在持有某个对象弱引用时,当对象被废弃,此弱引用会自动失效且置为 nil,不会造成野指针问题。

__unsafe_unretained 修饰符主要用于兼容 iOS4 以下版本,它修饰的变量不属于编译器的内存管理对象,同时在对象被废弃时不会被自动置为 nil,因此可能造成野指针问题。

由于 ARC 下不能手动使用 autorelease 方法和 NSAutoreleasePool 对象,因此用 @autoreleasingpool 和 __autoreleasing 修饰符来代替。

虽然 __strong 修饰符是对于 id 类型和 OC 对象类型的默认修饰符,但不适用于 id 指针和对象指针,id 指针和对象指针的默认修饰符是 autoreleasing。

    NSError *realError = nil;
    NSError **errorPointer = &realError;

由于对象指针赋值时所有权必须一致,因此会报编译错误

    NSError *realError = nil;
    NSError * __strong *errorPointer = &realError;

《Pro multithreading and memory management for iOS and OSX》 在 autoreleasing 修饰符这部分写的很混乱,也可能是翻译的原因,英文版也有很多困惑的地方。

规则

  • 不能使用 retain/release/retainCount/autorelease

  • 不能使用 NSAllocateObject/NSDealloateObject

    NSAllocateObject 和 NSDeallocateObject 是 alloc 等方法的内部实现,所以也不能使用。

  • 遵守内存管理的方法命名规则

    alloc/new/copy/mutableCopy 等驼峰式前缀的方法必须返回给调用方所应当持有的对象,init 驼峰式前缀方法必须是实例方法,返回对象为 id 类型或该方法声明类的对象类型,返回对象不会注册到 autoreleasePool 中。

  • 不显式调用 dealloc 方法

  • 使用 @autoreleasingpool 和 __autoreleasing 修饰符代替 NSAutoreleasePool

  • 不能使用 NSZone

  • OC 对象不能作为 C 语言结构体成员

    C 语言无法管理 OC 对象的生命周期,可以用 void * 或 __unsafe_unretained 修饰符

桥接转换

如上所述,OC 对象生命周期由编译器管理,C 语言无法管理,如果想把 OC 对象转换为 C 变量,在 MRC 中可以简单转换,但是在 ARC 中必须进行桥接转换,这一点在使用 Core Foundation 框架时很常见,因为 Core Foundation 框架包含的是 C 语言接口。

    id obj = [[NSObject alloc] init];
    void *p = (__bridge void *)obj;
    id o = (__bridge id)p;

__bridge 转换的安全性与 __unsafe_unretained 相近,因此可能造成野指针问题。

__bridge_retained 将 OC 对象转换为 CF 对象,CF 对象需要负责用 CFRelease 等方法释放对象

    void *p = (__bridge_retained void *)obj;

__bridge__transfer 将 Core Foundation 对象转换为 OC 对象,同时 CF 对象放弃持有。

    id o = (__bridge_transfer id)p;

在 CF 框架中有一组对应 API

  • CFBridgingRelease -- __bridge__transfer
  • CFBridgingRetain -- __bridge_retained

属性

ARC 中属性具有的特性与所有权修饰符的对应关系

属性声明的属性 所有权修饰符
assign __unsafe_unretained
copy __strong
retain __strong
strong _strong
unsafe_unretained __unsafe_unretained
weak __weak

其中 copy 属性的赋值是通过 NSCopying 接口的 copyWithZone: 方法。

ARC 实现

__strong 实现

对于 alloc/new/copy/mutableCopy 方法等,其模拟源代码如下

    id obj = objc_msgSend(NSObject, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    objc_release(obj);

而对于非 alloc 等方法,实现源码略有不同

    id obj = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);

这里用到一个新的方法 objc_retainAutoreleasedReturnValue,而在返回对象的方法,例如这里的 array 方法等,有一个配对的方法

+ (id) array
{
    id obj = objc_mgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

这里这两个方法其实是一种优化策略,如果 objc_autoreleaseReturnValue() 检测使用该函数的方法或函数调用方的执行命令列表,如果方法或调用方在调用了该方法后会紧接着调用 objc_retainAutoreleasedReturnValue,则该函数会取消注册到 autoreleasePool 的操作,并且将结果保存到 thread-local storage 中,也就是一个线程局部存储,这一部分存储空间只作为某个线程专有的存储。然后被调用函数直接返回这个 object,同时外部接收到返回值后去检查 TLS 中刚好有这个对象,就直接返回,不进行retain 操作。相当于节约了一次 release 和 retain 的操作。

但是这样做的前提是被调用方要能得知外部调用方的环境是 ARC 还是 非 ARC,这里会用到 __builtin_return_address 方法,作用是得到函数的返回地址,参数表示层数,于是内部的被调用方加入偏移值就可以查看外部的调用方的汇编指令,从而检测出外部是否调用了 objc_retainAutoreleasedReturnValue 这个方法。

__weak 实现

前面提过引用计数的存储是用引用计数表来实现的,这里对于 __weak 对象也是如此。以对象地址作为主键,存储多个 __weak 修饰符的变量,对它们进行统一管理。

{
    id __weak obj1 = obj;
}

这句声明赋值语句的实现如下

id obj1;
obj1_initWeak(&obj1, obj);
obj1_destroyWeak(&obj1);

实际上也就是

id obj1;
obj1 = 0;
objc_storeWeak(obj1, obj);
objc_storeWeak(&obj1, 0);

这里 objc_storeWeak 函数会把第二餐素的赋值对象的地址作为键值,将第一参数变量地址注册到 weak 表中。如第二参数为 0,则把变量地址从 weak 表中删除。

当废弃对象时,将进行以下操作:

  • 从 weak 表中获取废弃对象的地址为键值的记录
  • 将包含在记录中的所有附有 __weak 修饰符变量的地址置为 nil
  • 从 weak 表中删除该记录
  • 从引用计数表中删除废弃对象的地址为键值的记录

因此如果大量使用 __weak 变量就会消耗相应的 CPU 资源。

另一方面,在 使用 附有 _weak 修饰符的变量时变量会被注册到 autoreleasePool 中。

{
    id __weak boj1 = obj;
    NSLog(@"%@", obj1);
}

其实现如下

id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

这里强调在使用该变量时才会注册到 autoreleasePool 中。由于每次使用时都会注册一遍,所以当多次使用时会注册大量变量到 autoreleasePool 中,因此最好暂时用 __strong 变量暂存一下,因为 __strong 变量只会被注册一次。

关于注册到 autoreleasePool 的操作无法被验证

    id obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
    NSLog(@"pre %lu", _objc_rootRetainCount(obj));
    NSLog(@"%@", [obj1 class]);
    NSLog(@"after %lu", _objc_rootRetainCount(obj));

按照 《Pro multithreading and memory management for iOS and OSX》 的说法打印出来应该是使用前为 1,使用后变成 2,实际打印结果

pre 1
NSObject
pre 1
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容

  • 1.1 什么是自动引用计数 概念:在 LLVM 编译器中设置 ARC(Automaitc Reference Co...
    __silhouette阅读 5,070评论 1 17
  • 自动引用计数 自动引用计数:指内存管理中对引用采取自动计数的技术。 内存管理/引用计数 持有对象引起引用计数加...
    南京小伙阅读 1,295评论 2 3
  • 29.理解引用计数 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数...
    Code_Ninja阅读 1,464评论 1 3
  • 貌似每个iOS开发者都有一篇属于自己的内存管理,记录了自己对内存管理理解的深度以及广度,所以我也来记录一下我的理解...
    Bugfix阅读 2,248评论 0 3
  • 你像天边最美的那朵云 而我在遥远的这里 望着 我和你 终究天各一方 你是学校里最优秀的学生 而我离开那个地方已经...
    迷人的hero阅读 236评论 0 1