6

Is it true that ARC keeps a count of unowned references to an object?

So, if the strong reference count of an object reaches 0 and the unowned reference count of that object is > 0 the object is de-initialized but not de-allocated? And only when the strong and unowned reference count reaches 0 does it get de-allocated?

I read that in an article, on Medium I think) but I'm not sure it's correct.

8
  • 2
    I’d suggest you include a link to the article and include the relevant excerpt in the question. But in answer to your question, when the last strong reference is removed, the object is deallocated, regardless of what dangling unowned references might be out there. Commented Feb 22, 2019 at 23:58
  • At the minimum, you should link to the article you're referring to, so we can address the claims as they're stated, not the claims as you understood/represented them. Commented Feb 23, 2019 at 0:29
  • @Rob Actually, dangling references don't remain. Weak references actaully point to an entry in a "side table", which itself contains a reference to the "main" object. The side table entry remains so long as there are outstanding weak refs, to keep the pointers from dangling. Upon accessing a weak ref with a deallocated main object, the weak ref is deincremented in the side table, and that particular weak ref is niled out. mikeash.com/pyblog/… Commented Feb 23, 2019 at 0:31
  • @Alexander - Those are weak references. He asked about unowned references. Perhaps he just misremembered the article, which may have been about weak. Commented Feb 23, 2019 at 0:45
  • 1
    @Alexander - For debug builds you’ll see that behavior, but for release builds, you’ll see something more akin to true dangling pointer behavior. Commented Feb 23, 2019 at 1:00

1 Answer 1

12

First of all, let's be aware that the answers to these questions are all implementation details that we should generally avoid relying on. Now, on to the answers:

Is it true that ARC keeps a count of unowned references to an object?

Yes, it is true. Each object has three reference counts: the strong count, the unowned count, and the weak count.

  • The strong count is always stored (but is stored with an adjustment of -1, so a stored 0 means a strong reference count of 1, and a stored 1 means a strong reference count of 2, and so on).

  • The unowned count is also always stored, with an adjustment of +1 that represents all strong references and is removed at the end of deinitialization.

  • The weak reference count is only stored after the first weak reference to the object is created. The weak reference count, if it is stored, is stored with a +1 adjustment, which represents all unowned references and is removed after the object is deallocated.

So, if the strong reference count of an object reaches 0 and the unowned reference count of that object is > 0 the object is de-initialized but not de-allocated?

Correct. The object is deinitialized: the deinits of the object's class and all superclasses are run, and any of the object's properties that are themselves references are set to nil. However, the object's memory is not deallocated, because the object's header has to remain valid until the last unowned reference to the object is destroyed.

And only when the strong and unowned reference count reaches 0 does it get de-allocated?

Correct. The object is deallocated when both the strong and unowned reference counts reach zero. Since most objects are never referenced by unowned references, this is usually when the last strong reference is destroyed.

You didn't ask about weak references, but for the sake of completeness, I'll explain them also. When an object is (or has ever been) referenced weakly, Swift allocates what it calls a “side table entry” (or sometimes just “side table”) for the object.

  • If an object has no side table, then the strong and unowned counts are stored directly in the object, and the weak count (which must be zero) is not stored.

  • If an object has a side table, then a pointer to the side table is stored in the object. The strong, unowned, and weak counts, and a pointer back to the object, are stored in the side table.

A weak reference to an object is stored as a pointer to the side table, not to the object. This means that an object can be deallocated (not just deinitialized) even if there are still weak references to it.

The side table is deallocated when the object is deallocated if there are no weak references to the object. If there are still weak references, the object is deallocated but the side table remains allocated. When the last weak reference to a deallocated object is destroyed, the side table is deallocated.

Note that weak references are not set to nil (destroyed) immediately when a Swift object is deinitialized or deallocated! A weak reference to a deinitialized object is only set to nil when the program tries to load the reference, or when the container of the weak reference is deinitialized. (What I mean by “the container” is, for example, when an object has a weak var property. The object is the container of the weak var reference.)


A large comment at the top of RefCount.h in the Swift source code explains all of these details and more.


P.S. There is one more kind of reference, unowned(unsafe), which does not adjust any reference counts. You should avoid this kind of reference if at all possible (and avoidance is almost always possible).

Sign up to request clarification or add additional context in comments.

6 Comments

unowned yields unowned(unsafe) behavior with optimized builds, e.g. -Ofast, no?
In my testing just now using Apple Swift version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1), there is no -Ofast. In both -O and -Ounchecked builds, a program that tries to use an invalid unowned reference reports “Fatal error: Attempted to read an unowned reference but object 0x7fb358e06a10 was already deallocated”
I just tested in both Xcode 10.1 and 10.2 beta 3 and when choosing “optimize for speed” for the "Swift Compiler - Code Generation” (the default setting for release builds), I’m seeing this unsafe behavior. It’s a bit academic, but that’s what I’m seeing. Regardless, great answer.
Here's my test program. For me, it aborts “safely” with -Onone, -O, and -Osize in Xcode 10.1 on macOS 10.13.6.
While this answer does cover implementation details, one important thing to take away from it is the fact that unowned is guaranteed to be memory safe (which conforms to Swift's philosophy of being safe by default). It is guaranteed to produce a runtime error if you use a reference after it has been deallocated. As you say, you can opt-out of this memory safety using unowned(unsafe), which yields undefined behaviour instead.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.