RT Blog
Programming

I don’t want this Class!

There are some very powerful features in the Objective-C runtime, so is this something we should be taking advantage of more?

During my time as a developer, I have seen my fair share of examples where people have abused the Objective-C runtime to solve a problem, when there was most likely a better solution.

One common example of this was when a developer uses objc_setClass() to change the class of an instance at runtime. I’ve seen this happen when developers want to instantiate a controller from a storyboard, but then subclass that class and go thorough the same initialiser.

// This is just a simple controller, that has a couple of outlets and also a class function which will initialise itself from a storyboard. 
class HorseViewController: UIViewController {
  
  @IBOutlet weak var header: UIView!
  @IBOutlet weak var body: UIView!
  
  class func instantiateViewController() -> HorseViewController {
    let storyboard = UIStoryboard(name: "main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "Identifier") as! HorseViewController
    return vc
  }
}

This is all very straightforward, but what happens if you now want to subclass this function?

class ZebraViewController: HorseViewController {
  
  var stripes: [Any]!
}

// What will this return?
ZebraViewController.instantiateViewController()

This will still return an instance of HorseViewController. We could then override the super method, and return our own instance. However, this would mean duplicating the controller in the storyboard for our subclass.

The problem

This is where I have seen the Objective-C runtime used incorrectly to solve the issue above.

// If we override the instantiateViewController in out subclass and then swap the class using object_setClass we can in theory change the class to our subclassed version.

…
  override class func instantiateViewController() -> HorseViewController {
    let vc = super.instantiateViewController()
    object_setClass(vc, ZebraViewController.self)
    return vc
  }
}

So why is this bad?

To really understand this we need to know a little about how the Objective-C runtime works, and how swift objects are converted to run inside this environment.

// If we look into objc.h we can see exactly we make up an object.

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

In essence, every object once compiled down to the Objective-C runtime, is a struct, which contains a single pointer called an isa, which is of type Class. That’s it… There isn’t really much to it. All I can say is that every object in the objc runtime has one of these pointers.

So what is an isa pointer?

The isa pointer is the first pointer-sized piece of memory that is allocated when initialising an object, and it contains information about the objects class. We can see this by looking at what makes up a Class.

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

Again a class is just a structure, and just like an object, it also has an isa pointer, which tells us that Class, is in-fact also an object. We also have a number of Objc2 only attributes.

super_class – this is a pointer to the object’s parent. This is what is returned when we call super on an object. Notice there is no pointer to retrieve the subclasses.

objc_ivar_list – this is the list of instance variables for the current class. Pretty self explanatory. Same with objc_protocol_list – which is again just a list of the protocols a class conforms to. As these are both pointers they can be both changed out at runtime.

objc_method_list – is slightly different to the two above, as it is a pointer to a pointer, which allows the objc runtime to modify the classes methods.

objc_cache – the object cache is very important, as it is this that tells the build system if there is a cached version of the class already stored. When the runtime searches for an implementation which matches a selector, it will first check the cache to see if a value exists, and if it doesn’t, it will query the rest of the hierarchy.

name, version and info are just some metadata set on the class.

instance_size – is the amount of memory that class needs to be allocated.

Now we know more about what happens behind the scenes we can finally look at why changing the class at runtime could be a problem.

Memory Allocation

All we are doing when we call object_setClass is changing the isa pointer to the one of our new class. Our original object has already been initialised, and the correct amount of memory has been allocated. When we change the pointer we now have a new class which may need more memory, if it has more methods, ivars etc. This is where the problem lies. New methods may not have enough memory to execute so we could start to see random crashes appearing.

Solution

In my opinion, if you want to share UI from a storyboard or xib, then I think the best approach would be to split it up into separate views or child view controllers. These can then easily be re-used in multiple view controllers.

Related posts

WWDC 19 – Swift UI, Combine

Rowan
5 years ago

Unit Testing with Viper

Rowan
6 years ago

Importing Data into HealthKit

Rowan
6 years ago
Exit mobile version