The original address

In a recent development, I accidentally overwrote the dealloc method in the custom UIViewController category, causing the project to crash with many wild Pointers. Although overwriting the dealloc method caused some uncertain behavior, But why did the crash happen? With doubt and reviewed the source code of the next category.

The underlying implementation of a Category

The objc-runtime-new.m file can be found in objC4 source code as follows:

typedef struct category_t *Category; struct category_t { const char *name; // The name of the Category classref_t CLS; Struct method_list_t *instanceMethods; Struct method_list_t *classMethods; Struct protocol_t *protocols; Struct property_list_t *instanceProperties; // Category attribute list method_list_t *methodsForMeta(bool isMeta) {if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta) { if (isMeta) return nil; // classProperties; else return instanceProperties; }};Copy the code

As you can see from the source code, a category is a category_T structure that maintains information about the classes and categories to be extended.

Extension: It can be seen from the source code that there is no way to store member variables in the structure of the classification, which explains why member variables cannot be added to the classification. It also needs to be noted that although the classification provides a way to store property lists, it does not automatically generate member variables. It only generates declarations of setter and getter methods, which we need to implement ourselves.Copy the code

How are categories loaded

The OC runtime is dynamically loaded via dyld, and the _objc_init() method is the first method executed after the runtime is loaded. Let’s trace the category loading process starting with _objc_init().

Let’s look at the implementation of _objc_init() as follows:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
        
    // Register for unmap first, in case some +load unmaps something
    _dyld_register_func_for_remove_image(&unmap_image);
    dyld_register_image_state_change_handler(dyld_image_state_bound,
                                             1/*batch*/, &map_2_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
Copy the code

Ld_register_image_state_change_handler (DYLD_IMAGe_state_bound, 1/* Batch */, &map_2_images) When dyLD_IMAGE is dyLD_IMAGe_STATE_bound, map_2_images is triggered to Map the image to memory.

const char *
map_2_images(enum dyld_image_states state, uint32_t infoCount,
             const struct dyld_image_info infoList[])
{
    rwlock_writer_t lock(runtimeLock);
    return map_images_nolock(state, infoCount, infoList);
}
Copy the code

This will call map_images_NOLock, map_images_NOLock, map_images_NOLock, map_images_NOLock, map_images_NOLock. In the implementation of map_images_NOLock, we’ll find an important function, _read_images, that initializes the image after the Map.

In the Discover Categories section, a key function, remethodizeClass, is called as follows:

static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertWriting(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : ""); } attachCategories(cls, cats, true /*flush caches*/); free(cats); }}Copy the code

The attachCategories function is called to attach methods, attributes, and protocols to the class.

static void attachCategories(Class cls, category_list *cats, bool flush_caches) { if (! cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); bool isMeta = cls->isMetaClass(); // fixme rearrange to remove these intermediate allocations method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists)); property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists)); protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists)); // Count backwards through cats to get newest categories first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) { auto& entry = cats->list[i]; method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); } property_list_t *proplist = entry.cat->propertiesForMeta(isMeta); if (proplist) { proplists[propcount++] = proplist; } protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; } } auto rw = cls->data(); prepareMethodLists(cls, mlists, mcount, NO, fromBundle); rw->methods.attachLists(mlists, mcount); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls); rw->properties.attachLists(proplists, propcount); free(proplists); rw->protocols.attachLists(protolists, protocount); free(protolists); }Copy the code

In the attachCategories function, some memory allocation is carried out at first, then the methods, attributes and protocols of the classification are acquired and put into the specified array. Finally, the attachLists method is called to merge the methods, attributes and protocols of the classification and the original class. Take a look at the attachLists function implementation:

void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount; setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount; memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (! list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1:0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); }}Copy the code

In this function, the memmove and memcpy operations place the list of classified methods, attributes, and protocols before the list of methods, attributes, and protocols stored in the class object.

Why crash

We use an example to duplicate the crash mentioned at the beginning of the article. The code is as follows:

// Override dealloc in UIViewController+ test. m - (void)dealloc {} // testaViewController.m@interface TestAViewController () @property (nonatomic, strong) TestBViewController *bViewController; @end @implementation TestAViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.view.backgroundColor = UIColor.whiteColor; _bViewController = [TestBViewController new]; __weak typeof (self) weakSelf = self; _bViewController.willDismiss = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf.navigationController popViewControllerAnimated:YES]; }; [self setDefinesPresentationContext:YES]; [self.bViewController setModalPresentationStyle:UIModalPresentationCurrentContext]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (! self.isFirst) { [self presentViewController:self.bViewController animated:YES completion:nil]; } self.isFirst = YES; }; - (void)onDismiss {if (self.willdismiss) {self.willdismiss (); } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ if (self.presentingViewController) { [self dismissViewControllerAnimated:YES completion:^{ }]; }}); }Copy the code

Run TestBViewController and click the Close button.

*** -[TestAViewController retain]: message sent to deallocated instance 0x7fba53d161d0
Copy the code

From this log, it can be seen that the crash was caused by accessing the address of the released object.

Analysis: Because overwriting the dealloc method in the category causes UIViewController’s own Dealloc method not to execute, it does not release member variables in dealloc, and Pointers to those member variables become wild Pointers, If you accidentally access the address of a freed UIViewController object via a wild pointer, the above crash will occur.

conclusion

While categories provide a lot of convenience, we have to be careful not to fall into the trap of using them. Here’s a quote from Apple’s website:

Avoid Category Method Name Clashes
Because the methods declared in a category are added to an existing class, 
you need to be very careful about method names.

If the name of a method declared in a category is the same as a method in the 
original class, or a method in another category on the same class (or even a 
superclass), the behavior is undefined as to which method implementation is used 
at runtime. This is less likely to be an issue if you’re using categories with 
your own classes, but can cause problems when using categories to add methods to 
standard Cocoa or Cocoa Touch classes.
Copy the code

—- Scan the following public account to get more technical good articles —–