Retained Cycles, Yikes

How does memory management work in iOS?

ARC is a compile time feature that Apple uses to automate memory management, well it takes care of most of the BASICS.  ARC is short for Automatic Reference Count.  The idea of ARC is simple.  That means it will only frees up memory for objects when there are no strong pointers pointing to them.

class Vehicle{
   var speed = 0
   init(speed){
      self.speed = speed
   }
  deinit{
    print("deinit instance")
  }
}

// corolla instance has a strong reference to Vehicle Class
var corolla:Vehicle? = Vehicle() 

//setting corolla to nil means there are no strong reference left, 
//the instance Vehicle class gets deallocated.
corolla = nil

Deinit will be called once an instance is deallocated, we can check this with console log statement in deinit method.  Most of the time ARC works well in deallocating memory in iOS but it doesn’t work when there is a retained cycle.

Retained Cycle?

class Vehicle{ 
  var speed = 0  
  var vehicle:Vehicle? = nil

  init(speed){ self.speed = speed }

  deinit{
    print("deinit instance")
 } 
}

var accord:Vehicle? = Vehicle()
var mdx:Vehicle? = Vehicle()
accord.vehicle = mdx
mdx.vehicle = accord

Below illustrates relation of the references above.

accord —->  Vehicle ——> mdx

mdx   ——-> Vehicle ——> accord

//Now let sets accord and mdx to nil
accord = nil
mdx = nil

Below illustrates relation of the references above.

accord:         Vehicle ——> mdx

mdx:             Vehicle ——> accord

Instance accord and mdx reference to class Vehicle have been set to nil but there is still one strong reference left for each class.  Now this is a retained cycle, deinit method will not be called and accord and mdx will not be deallocated.  We have removed the references to accord and mdx, there is no way to deallocated the additional strong pointer.  This is a memory leak and often will cause your app to crash.  When your memory usage is too high, iOS will kill your app.

How to mitigate retained cycles

Use weak reference instead of strong

class Vehicle{ 
   var speed = 0 
   weak var vehicle:Vehicle? = nil  //weak reference

   init(speed){ 
      self.speed = speed 
   } 
   deinit{ print("deinit instance") } 
}

Weak reference is just a pointer to an object but does not protect it from being deallocated by ARC.  In ARC, strong reference increases retain count by one, weak reference do not.  Every weak reference has to be non-constant Optional because the instance can become nil when reference is deallocated.

Unowned Reference versus weak

Unowned is similar to weak reference except variable will not become nil and therefore reference can not be an Optional.  It is safer to use weak as opposed to unowned

Other Retained Cycles

  1.  One common scenario for retained cycle is the use of Delegates.  The parentViewController has a childViewController.  The parentViewController sets itself as the delegate of the childViewController so it can inform the childViewController of events or actions.
//This is the mainViewController
class classAViewController: UIViewController, classBViewControllerDelegate{
  override func viewDidLoad(){
     super.viewDidLoad()
  }

  override func prepare(for segue:UIStoryBoardSegue, sender:Any?){
   if let nav = segue.destination as? UINavigationController,
      let classBVC = nav.topViewController as? classBViewController
     classBVC.delegate = self
  }

  func changeBackgroundColor(_ color:UIColor?){
    view.backgroundColor = color
  }
}

//Protocol that has method to change background color
protocol classBViewControllerDelegate: class {
 func changeBackgroundColor(_ color: UIColor?)
}

class classBViewController: UIViewController {
  var delegate = classBViewControllerDelegate?

  //Assumme viewDidLoad and add gesture here

  func handleTap(_ tapGesture:UITapGestureRecognizer){
    view.backgroundColor = tapGesture.view?.backgroundColor
    //Add the delegate method call
    delegate?.changeBackgroundColor(tapGesture.view?.backgroundColor)
  }
}

The scenario above will cause a retained cycle after popping the classAViewController. In order to prevent this memory leak, we should always declare the delegate and datasource as weak references.

To fix this, just declare the the delegate as weak

class classBViewController: UIViewController {
 //Create the delegate here as weak to break retained cycle
 weak var delegate = classBViewControllerDelegate?

 func handleTap(_ tapGesture:UITapGestureRecognizer){
   view.backgroundColor = tapGesture.view?.backgroundColor
   //Add the delegate method call
   delegate?.changeBackgroundColor(tapGesture.view?.backgroundColor)
 }
}

2.  Another common scenario for retained cycle is the used of closures.  Closures in Swift is exactly like Blocks in Objective-C.  If any variable is declared outside of the closure’s scope, referencing that variable inside the closure’s scope creates another strong reference to that object.  The only exception to this are variables such as Ints, Array, Dictionary and String which are value-like semantics.

Consider this scenario

class VehicleRetainedCycle{
   var closure = (()->Void)!
   var speed = 65

   init(){
     closure = {
        self.speed = 80
     }
   }
}

//This will cause a retained cycle
let retainedCycleVehicle = VehicleRetainedCycle()
//Capturing self inside closure will not be nil
retainCycleVehicle.closure()

The retain cycle comes from the closure capturing strong self while self also has a strong reference to the closure. To safely address self in closure we need to add [unowned self] to the closure

closure = { [unowned self] in 
   self.speed = 80
}

 

Posted in: iOS

Leave a comment