2

I was running into a problem in my project and I realized that an object is not being deallocated as needed. I decided to test the ARC of the object and just after initialization it is 2. In this trivial example below the same is true. Why is the ARC 2 and not 1?

import SpriteKit

class LevelBuilder:SKNode{
    var testNode:SKSpriteNode?
    init(with color:SKColor){
        super.init()
        self.testNode = SKSpriteNode(color: color, size: CGSize(width: 2, height: 2))
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

let test = LevelBuilder(with: .red)
print("ARC: \(CFGetRetainCount(test))")

It prints ARC: 2

Aleksandr
  • 533
  • 1
  • 4
  • 12

2 Answers2

3

There is no such thing as "the ARC of the object." What you're thinking about is the retain count. It is hard to imagine a number more meaningless than the retain count. It is either zero (in which case the object is gone, so you'll never see that), or it is "not zero."

The retain count is the number of ownership claims that have been put on an object. Any part of the system is free to make an ownership claim at any time. Any part of the system can remove their ownership claim at any time. There's a whole thing called the autorelease pool that holds ownership claims and will automatically release those claims "at some point in the future." It is completely normal for an object to have several autorelease retains on it at any given time. That will increase the retain count, but the retain count will drop later.

If retain counts were meaningless under MRC (and they were), they're completely bonkers under ARC, where the compiler is free to optimize them out anytime it can prove it doesn't matter, and often injects extra retains when it can't prove they're not needed (particularly related to function calls). So the actual value is even more meaningless. For example, under ARC, it is completely appropriate for test to have an extra retain attached to it before calling CFGetRetainCount just to make sure that test isn't released too quickly.

If you have a memory management problem, you want to use tools like the memory graph debugger (and just looking for strong references and especially strong loops). Checking the retain count will only lie to you.

In your particular case, we can explore it a bit with swiftc -emit-sil, starting at the point that we do string interpolation (i.e. the "" in the last line):

// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%34 = function_ref @$SSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %35
%35 = apply %34(%30, %31, %32, %33) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %37
%36 = alloc_stack $String                       // users: %39, %37, %41
store %35 to %36 : $*String                     // id: %37
// function_ref specialized String.init<A>(stringInterpolationSegment:)
%38 = function_ref @$SSS26stringInterpolationSegmentSSx_tcs23CustomStringConvertibleRzs20TextOutputStreamableRzlufCSS_Tg5 : $@convention(method) (@owned String, @thin String.Type) -> @owned String // user: %40
%39 = load %36 : $*String                       // user: %40
%40 = apply %38(%39, %29) : $@convention(method) (@owned String, @thin String.Type) -> @owned String // user: %42
dealloc_stack %36 : $*String                    // id: %41
store %40 to %28 : $*String                     // id: %42
%43 = integer_literal $Builtin.Word, 1          // user: %44
%44 = index_addr %28 : $*String, %43 : $Builtin.Word // user: %58
%45 = metatype $@thin String.Type               // user: %56
%46 = load %3 : $*LevelBuilder                  // users: %48, %47

=========
strong_retain %46 : $LevelBuilder               // id: %47
%48 = init_existential_ref %46 : $LevelBuilder : $LevelBuilder, $AnyObject // user: %49
%49 = enum $Optional<AnyObject>, #Optional.some!enumelt.1, %48 : $AnyObject // users: %52, %51
// function_ref CFGetRetainCount
%50 = function_ref @CFGetRetainCount : $@convention(c) (Optional<AnyObject>) -> Int // user: %51
%51 = apply %50(%49) : $@convention(c) (Optional<AnyObject>) -> Int // user: %54
release_value %49 : $Optional<AnyObject>        // id: %52
=========

I've marked the important part with === lines. A strong retain is put on test. It's then wrapped up into a AnyObject? wrapper to pass to the C function (GetRetainCount). The function is called. And then the value of the Optional (i.e. test) is released. So you should expect one extra retain when you call GetRetainCount.

But if you re-compile this with -O, you'll notice that there is no strong_retain instruction. ARC sees that the extra retain isn't actually necessary and removes it. So that suggests that with optimization the retain count will be 1. I wonder if that's true:

$ swiftc main.swift
$ ./main
ARC: 2
$ swiftc -O main.swift
$ ./main
ARC: 1

Sure enough.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Major props to you for this answer. Very detailed and crystal clear. Since I never used swift CLI to compile, what is `swiftc -emit-sil` ? Thanks! – Aleksandr Oct 02 '18 at 00:04
  • 1
    `swiftc` is the Swift compiler. SIL is the Swift Intermediate Language, which is a language that Swift is compiled into to simplify compiling into LLVM's IR (which is then compiled into platform-specific machine languages). https://llvm.org/devmtg/2015-10/slides/GroffLattner-SILHighLevelIR.pdf – Rob Napier Oct 02 '18 at 00:06
0

Maybe because you initialize your testNode which is a SKSpriteNode which might as well be referencing to SKNode under the hood. So you have first reference from your LevelBuilder class and the second one comes from the testNode.

emrepun
  • 2,496
  • 2
  • 15
  • 33