Can’t believe we’re still dealing with this in 2017? Well, that makes two of us. While retain cycles are easy to fix, they’re also hard to spot while eyeballing a codebase. Recently, I’ve found that a single unit test can provide solace. With just a few lines of code, it runs continuously as you make changes, all the while verifying you haven’t introduced any new memory leaks.

It’s a simple snippet, but I’ll walk you through it.

The test function makes use of XCTest’s asynchronous expectation system to pick up whether your class’s deinit function gets called. The subclassing trick you see there is needed because Swift doesn’t support reflection yet. The subclass tracks the deinit call by adding that deinitCalled closure.

The test works by allocating the instance on the main queue, while immediately deallocating it on the background queue. This triggers the deinit call, and the test succeeds.

If the test fails due to the 5-second timeout, this is your cue. You have just found a retain cycle! I’ve found it helps to run this test throughout the development process. The delta between code changes remains small enough to pick up where you forgot a [weak self] or [unowned self].

P.S. I was unable to figure out a way to make the test function generic, so I’ve been copy-pasting it around. I realize this is not ideal. However, I find it’s not a big deal because we’re dealing with test code, not actual app code.

Previous ArticleNext Article

This post has 3 Comments

3
  1. I really like this way of testing for reference cycles, automating the process is always good!
    Can you give me an example of where this test will fail i.e catches a retain cycle, thanks

    1. Hi Izzy,

      Sure! Consider the following (contrived) example. The most common retain cycles can be triggered by accessing some property on self from within a closure, which itself is stored on self. The test below does exactly this and will fail due to a timeout when run on causesRetainCycle() because it implicitly captures self strongly (by omitting the capture list).

      doesntCauseRetainCycle() succeeds because it explicitly captures self as weak (see [weak self] in statement).

      “`
      class ClassWithRetainCycle {
      var someProperty = false
      var someAction: (()->Void)?

      init() {}
      
      func causesRetainCycle() {
          someAction = {
              self.someProperty = true
          }
      }
      
      func doesntCausesRetainCycle() {
          someAction = { [weak self] in
              self?.someProperty = true
          }
      }
      

      }

      class RetainCycleTestTests: XCTestCase {
      func testCleanup() {
      // Extend your class inline in order to add closure property deinitCalled,
      // which indicates when/if your class’s deinit() gets called
      class ClassUnderTest: ClassWithRetainCycle {
      var deinitCalled: (() -> Void)?
      deinit { deinitCalled?() }
      }

          // Set up async expectation, which causes the test to wait for `deinitCalled`
          // to be called
          let exp = expectation(description: "exp")
      
          // Initialize the class
          var instance: ClassUnderTest? = ClassUnderTest()
          instance?.causesRetainCycle()
          instance?.doesntCausesRetainCycle()
      
          // Set up up the `deinitCalled` closure, making the test succeed
          instance?.deinitCalled = {
              exp.fulfill()
          }
      
          // On a different queue, remove the instance from memory,
          // which should call `deinit`, in order to clean up resources.
          // If this doesn't cause `deinit` to be called, you probably have a
          // retain cycle
          DispatchQueue.global(qos: .background).async {
              instance = nil
          }
      
          // Wait for max. five seconds for the test to succeed, if not,
          // you may have a memory leak due to a retain cycle
          waitForExpectations(timeout: 5)
      }
      

      }
      “`

Leave a Reply

Your email address will not be published. Required fields are marked *

A.

Android device debugging on Linux Mint: “error: insufficient permissions for device: udev requires plugdev group membership”

As I rejoiced in the achievement of finally finding a Linux distro which plays well with my Dell XPS 13 (2019 edition, model 9380), I plugged in my Moto test device, intending to continue working on an Android app. Pressing the Run button promptly made Android Studio (3.4) do its compilation magic, but got stopped in its tracks rather quickly. An angry-looking error message awaited me:

error: insufficient permissions for device: udev requires plugdev group membership

Oops, no device debugging for you.

Android Studio also left a note pointing me to their developer page on the subject. As I suspected, it seemed I had some configuration work left to get device debugging working on Linux. However, following Google’s instructions on setting up adb didn’t do much to resolve my problem. The part about adding yourself to the udev group is important, though, as it’s linked to the actual solution described in the next paragraph.

sudo usermod -aG plugdev $LOGNAME

Adds your user to the plugdev group.

Update: Someone mentioned recently that the below steps may not actually be required; just logging out and back in again at this point, should also do the trick. YMMV of course :-).

An ill-timed dog walk (let’s just say I hadn’t expected to be caught in the middle of a downpour), and a few mildly frustrated Google searches later, I ran across a blog post from 2013 (!) on the subject of “Adding udev rules for USB debugging Android devices“, by Janos Gyerik.

I will not pretend to know why this extra configuration is required, but it describes looking up the device’s identifier and adding it to the aforementioned plugdev access group, so Android Studio can properly access the USB device.

And there you go, a working USB debugging connection to my Android device, thanks to some great advice from 2013!