(原文出处:Objective-C Internals | Always Processing)

比较了Apple的相关引证完成和我为历史背景编写的一个完成,并附加了关于与符号指针方针的运用以及assign相关战略实际上是做什么的额定阐明。

我记得迫不及待地等待我们在即将成为Microsoft Office 2016 for Mac的项目中将最低部署方针更改为Mac OS X 10.6[1]。Snow Leopard引入了许多新的API,包含Grand Central Dispatch和blocks。但我最等待的是开始运用Objective-C的相关引证来替换一些糟糕的代码。

旧的办法

Objective-C最大的优势(也是缺点)是其动态办法绑定。简直一切首要的第三方应用程序都(乱用)运用此功用来填补功用距离或减轻应用程序/体系体系结构的不匹配之处。

将方针的生命周期与由第三方(即Apple)实例化和操控的方针的生命周期绑定在一起,就是一个此类功用的距离。在运转时供给此功用之前,应用程序能够经过部分地预修正完成-[NSObject dealloc]来完成此功用。以下代码示例显示了一个第三方或许运用这种办法完成相关引证的办法。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
static OSSpinLock s_lock;               // 用于首要侧表
static NSMapTable *s_associatedObjects; // 首要侧表
static IMP s_NSObject_dealloc;          // 原始完成
void APAssociatedObjectSet(id object, id association) {
  id previousAssociation = nil;
  // 在锁之外坚持,以最小化坚持锁的时刻
  [association retain];
  OSSpinLockLock(&s_lock);
  previousAssociation = [s_associatedObjects objectForKey:object];
  if (association != nil) {
    [s_associatedObjects setObject:association forKey:object];
  } else {
    [s_associatedObjects removeObjectForKey:object];
  }
  OSSpinLockUnlock(&s_lock);
  // 在锁之外开释,以防这是最后一个开释,
  // 由于dealloc完成会获取锁
  [previousAssociation release];
}
id APAssociatedObjectGet(id object) {
  OSSpinLockLock(&s_lock);
  id association = [s_associatedObjects objectForKey:object];
  // 坚持相关的方针,以确保在调用者运用期间它不会被开释,
  // 以防其他线程在此期间更改相关的方针
  [association retain];
  OSSpinLockUnlock(&s_lock);
  return [association autorelease];
}
static void APAssociatedObject_dealloc(id self, SEL _cmd) {
  // 开释任何相关的方针并删去侧表项
  APAssociatedObjectSet(self, nil);
  (*s_NSObject_dealloc)(self, _cmd);
}
void APAssociatedObjectInitialize(void) {
  s_lock = OS_SPINLOCK_INIT;
  // 该键运用弱引证,以避免方针变得永不开释。
  // 值运用弱引证,以显式地操控坚持计数,以避免dealloc重入死锁。
  s_associatedObjects=[NSMapTable mapTableWithWeakToWeakObjects];
  // 预先修正-[NSObject dealloc]以清理s_associatedObjects
  Method m = class_getInstanceMethod([NSObject class],
                                     @selector(dealloc));
  s_NSObject_dealloc = method_getImplementation(m);
  method_setImplementation(m, (IMP)&APAssociatedObject_dealloc);
}

尽管完成只要59行,包含空格和注释,但还有一些我想要指出的工作:

这个完成支持0个或1个方针相关,但能够经过轻微的修改来支持恣意数量的相关,就像objc_setAssociatedObject()相同。或者,客户端能够运用NSMutableDictionary来相关恣意数量的方针。

每个-dealloc都需求获取一个锁来履行记载(除了运转时和分配器锁定获取)。在从前的帖子中,我们看到,运转时对于没有相关引证(除其他条件外)的方针实例有一个快速的开释途径,使其能够避免大多数状况下的锁定开销。

删去相关或许会导致相关的方针被开释,反过来或许会导致其相关的方针被开释。因而,在坚持锁定的同时,完成有必要避免递归,由于OSSpinLock不行重入。

在取得一个相关的方针之前,坚持锁定并保存相关的方针,以确保在调用者运用期间不会开释方针,以防另一个线程在从映射表中检索方针之后将其开释。

预修正正在取得相关的方针的类上的-dealloc不是可行的办法,有两个原因:

类层次结构或许有多个补丁。例如,在NSObject上设置了一个相关的方针,然后在NSView上设置了另一个相关的方针,在dealloc期间,包含子类,一切NSView实例都会两次调用补丁。完成或许处理此状况,但价值是额定的杂乱性。

从补丁中调用正确的-dealloc变得更加具有挑战性。持续上面的比如,假如NSTableView正在开释,那么补丁怎么知道它应该调用-[NSView dealloc]完成仍是调用-[NSObject dealloc]完成?(self的类标识始终是NSTableView。)需求很多的记载才干盯梢方针在其dealloc链中的方位,并处理作为其dealloc的一部分发生的其他附加开释。

Objective-C的主动引证计数(ARC)直到OS X 10.7 Lion才初次露脸。因而,我想着重两点,这对现代Objective-C程序员来说已不再相关:

在APAssociatedObjectGet()中的retain和autorelease调用确保了回来的方针在当前的autorelease范围内存在。假如没有这个,另一个线程或许会在从映射表中检索方针和将其回来给调用者之间使方针开释。

在地图表的mapTableWithWeakToWeakObjects工厂办法中运用的弱引证不具有ARC的零化弱引证语义。相反,它相当于ARC的unsafe_unretained。

APAssociatedObjectInitialize()能够有一个__attribute__((constructor))来在调用main()之前初始化该功用。我把这个留出来,由于首要的应用程序一般有一个杂乱的初始化体系会调用这个函数。

接下来,让我们看看Apple的Objective-C运转时是怎么完成这个功用的。

苹果的完成办法

上述第三方完成和注释与Apple的完成惊人地相符。(我之前编写了这个完成,然后才查阅了Apple的完成[2]。)

首先,让我们看看objc_setAssociatedObject(),它仅仅调用了_object_set_associative_reference()。

runtime/objc-references.mm lines 170-219
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// 在锁之外保存新值(假如有的话)。
association.acquireValue();
bool isFirstAssociation = false;
{
  AssociationsManager manager;
  AssociationsHashMap &associations(manager.get());
  if (value) {
    auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
    if (refs_result.second) {
      /* 这是我们做的第一个相关 */
      isFirstAssociation = true;
    }
    /* 树立或替换相关 */
    auto &refs = refs_result.first->second;
    auto result = refs.try_emplace(key, std::move(association));
    if (!result.second) {
      association.swap(result.first->second);
    }
  } else {
    auto refs_it = associations.find(disguised);
    if (refs_it != associations.end()) {
      auto &refs = refs_it->second;
      auto it = refs.find(key);
      if (it != refs.end()) {
        association.swap(it->second);
        refs.erase(it);
        if (refs.size() == 0) {
          associations.erase(refs_it);
        }
      }
    }
  }
}
if (isFirstAssociation)
  object->setHasAssociatedObjects();
// 在锁之外开释旧值。
association.releaseHeldValue();

鉴于与上一节的对应联系,我将扼要概述与我的完成相似之处和差异。

  • DisguisedPtr用于阻挠在诸如走漏之类的工具中对堆进行追寻。
  • ObjcAssociation辅助方针完成相关战略(运用assign、retain或copy语义,以及读取是原子的仍是非原子的)。
  • AssociationsManager是RAII便利方针,用于锁定和解锁相关自旋锁(现在是一个不公平的锁)。
  • 运用哈希映射(具体为LLVM的DenseMap)存储方针相关。顶级哈希映射将方针指针映射到一个相关哈希映射,将键映射到ObjcAssociations(方针及其保存战略)。
  • 相关nil值会删去任何从前相关的方针。
  • 当方针取得其第一个相关时,运转时会更新其状况,以关闭快速的开释途径。
  • 开释任何从前相关的方针发生在锁之外。
  • 像getter和setter函数相同,objc_getAssociatedObject()仅仅调用_object_get_associative_reference()。获取途径很简单,所以对此我没有什么可评论的!

苹果的完成供给了一个奇特的函数,objc_removeAssociatedObjects()。老实说,我真不知道为什么这是一个公共API,runtime.h中的注释主张不要运用它(并且有很好的原因):

这个函数的首要意图是使方针回来“原始状况”变得简单。您不应该运用此函数来从方针中一般删去相关,由于它还会删去其他客户端或许现已添加到方针的相关。一般状况下,您应该运用objc_setAssociatedObject来运用nil值铲除相关。

与getter和setter函数相同,objc_removeAssociatedObjects()调用了_object_remove_associations()。可是,这个内部函数多了一个参数:bool deallocating,在调用objc_removeAssociatedObjects()时为false。这个内部函数只要一个其他的调用者,objc_destructInstance(),毫不奇怪,将deallocating设置为true。

那么,deallocating标志是做什么的呢?函数中的一条注释解说了它的意图:

假如我们没有正在开释,则保存SYSTEM_OBJECT相关。

苹果有一个内部的战略标志,OBJC_ASSOCIATION_SYSTEM_OBJECT,避免其相关方针被objc_removeAssociatedObjects()删去。您能够经过这个函数自己给自己设置一个圈套,可是苹果将阻挠您违反他们的假定。

我置疑这就是为什么相关键的类型为void *:指针键在Apple的框架中很难被识别并在第三方应用程序中(乱用)运用,例如,相对于简单被找到和运用的字符串键(例如NSNotificationName)。

符号指针方针

在符号指针方针上设置相关方针会发生什么?效果与将方针赋值给具有相同存储战略的全局变量相同:方针坚持不变,直到分配新值。因而,在符号指针方针上设置相关方针将有效地走漏相关方针。

相关方针完成没有处理符号指针的代码途径(甚至没有在操控台中记载警告)。因而,运转时将符号指针存储在相关哈希映射中,其中它会永远存在,由于符号指针方针永远不会开释。

符号指针方针的另一个副作用是,它们有效地将一切值合并为一个。虽然某些类型,如NSNumber,被知道完成了某种方式的合并,但NSString没有这样的行为。可是,NSString符号指针的代码途径满足急进,以至于从磁盘加载的本地化字符串或许会发生符号指针方针!因而,设置与NSString类型的任何方针相关方针的代码或许会发现相关方针不是放在彼此之间而是放在符号指针方针上。

尽管运用符号指针方针被视为内部完成细节,但请检查运用符号指针的类,并避免在具有这些类型的任何方针上运用相关方针。

assign存储的更近距离看

在撰写本文时,我认识到我在过去十多年中一直在错误地运用这个API ‍♂️。在OBJC_ASSOCIATION_ASSIGN战略旁边的注释说:

指定与相关方针的弱引证。

如上面所述,ARC之前,弱等价于ARC的unsafe_unretained;此标志不运用ARC零化弱引证语义。检查一下运用弱的完成。没有任何!

这个星期我有很多地方需求进行查找和代码审查…​

结论

第三方完成的相关引证简直能够与Apple的一方完成相媲美,首要的一方优势是对于没有相关方针的方针供给了快速的开释途径的可用性。新的运转时优化(即符号指针方针)或许会导致意外行为,使代码相关到随操作体系版别改变而改变的方针的唯一性和生命周期。历史背景是必要的,文档的假定或许随着时刻的改变而改变,扭曲其意义。