preface

Recently, I encountered an online Crash accident caused by objc_setAssociatedObject and objc_getAssociatedObject. I felt very sad and interesting at the same time, so I hereby share with you.

The body of the

The problem background

A Catagory already exists in the project, which mounts a property to a third-party library class, represented by the ssShowTime property in TestCatagory in the following code.

@interface ViewController(TestCategory)

@property (nonatomic, assign) long ssShowTime;

@end
Copy the code

The implementation is done using the objc_setAssociatedObject and objc_getAssociatedObject methods.


@implementation ViewController (TestCategory)

- (void)setSsShowTime:(long)ssShowTime {
    NSNumber *number = @(ssShowTime);
    objc_setAssociatedObject(self, @selector(ssShowTime), number, OBJC_ASSOCIATION_ASSIGN);
}

- (long)ssShowTime {
    NSNumber *number = objc_getAssociatedObject(self, @selector(ssShowTime));
    return [number longValue];
}

@end
Copy the code

This method has run through several versions without any problems. We will add a mount attribute to this base, we use ssLocalDesc to represent.

@property (nonatomic, strong) NSString *ssLocalDesc;

- (void)setSsLocalDesc:(NSString *)ssLocalDesc {
    objc_setAssociatedObject(self, @selector(ssLocalDesc), ssLocalDesc, OBJC_ASSOCIATION_ASSIGN);
}

- (NSString *)ssLocalDesc {
    NSString *ret = objc_getAssociatedObject(self, @selector(ssLocalDesc));
    return ret;
}
Copy the code

The ssLocalDesc attribute is used to store some description, such as a constant or concatenated string, as follows:

    self.ssLocalDesc = @"123"; Int index = 1; self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index];
Copy the code

Everything is fine until the following code appears:

    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)];
Copy the code

After this assignment, accessing the self.ssLocalDesc attribute will result in Crash!

The problem back

The ssShowTime attribute is long, but it is implemented internally through the NSNumber class. Therefore, OBJC_ASSOCIATION_ASSIGN should not be used.

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
Copy the code

It is more appropriate to use OBJC_ASSOCIATION_RETAIN or OBJC_ASSOCIATION_RETAIN_NONATOMIC.

The ssLocalDesc attribute is a string, and strings are usually strong or copy, so using OBJC_ASSOCIATION_ASSIGN is itself an error. The OBJC_ASSOCIATION_ASSIGN is usually added to avoid circular references and does not change the reference count.

Problem extension

After solving this problem, we find that there are several extended problems before crash: Question 1: Why does ssShowTime not crash during running? We know that Crash is caused by the fact that OBJC_ASSOCIATION_ASSIGN does not increase the reference count by 1, causing the object to be released and causing wild Pointers. So let’s look at the reference count of the number object before we mount it.

- (void)setSsShowTime:(long)ssShowTime {
    NSNumber *number = @(ssShowTime);
    objc_setAssociatedObject(self, @selector(ssShowTime), number, OBJC_ASSOCIATION_ASSIGN);
}
Copy the code

Surprisingly, the reference count is very large.

(lldb) p CFGetRetainCount(number)
(CFIndex) $0 = 9223372036854775807
Copy the code

If we rule out the possibility of reference counting errors, we can understand why the Number object is not freed.

Question 2: why does ssLocalDesc Crash when running online? For the ssLocalDesc attribute, I constructed three cases:

  • Case 1, a plain constant string;
    self.ssLocalDesc = @"123";
Copy the code

The result is shown below, with a large reference count; The string type is a constant string that is created as the App runs and destroyed when the App exits.

  • Case 2, short strings when tested;
    int index = 1;
    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", index];
Copy the code

The result is shown below, where the reference count is still large; The string type is TaggedPointerString, which is a string of label pointer type, and we use the pointer as a string object;

  • Case 3, longer string after line;
    self.ssLocalDesc = [NSString stringWithFormat:@"Tag_%d", (int)time(NULL)];
Copy the code

The result is shown below, and the reference count is normal. The string type is plain string, which is our most common string type. This type of string crashes when accessing the ssLocalDesc property below.

Going back to question 1, we know that NSNumber uses a similar Tagged Pointer. When the number is small, the NSNumber is not really an object, but a label pointer, and does not go through the same destruction and release process as an object. Validation method: Use a large number to initialize. For example, if ssShowTime is set to NSIntegerMax, the reference count returns to the normal range.

Tagged Pointer

Tagged Pointer is a technique used to improve performance and reduce memory usage. The principle is to use memory alignment in memory storage, where the address of an object is usually a multiple of the size of a pointer. Most iOS devices are 64-bit machines, so Pointers are usually stored as 64-bit integers. Because of memory alignment, some bits in the pointer will always be zero. To efficiently utilize this space, iOS considers an object pointer to be tagged pointer when its least significant bit is 1. The first three bits in the lowest tagged pointer are no longer regarded as the address of the ISA pointer. Instead, they represent the index value of a special tagged class table. This index is used to find the class that corresponds to Tagged Pointer, and the remaining 60 bits are used directly.

conclusion

The specific concept of the label pointer is clearly described in the two appendixes and will not be repeated here. The accident has many hidden factors, such as test environment do not agree with the online environment, such as on-line process not perform in accordance with the specification, such as failure to observe the code specification, such as review process found no problems, etc., on so many factors, including the two step is very important: 1, ensure that the test environment and online environment are consistent; 2. Standardize the operation according to the on-line process;

In order to detect problems during the test phase, it is better to set the test environment and the online environment to be exactly the same. From a technical point of view, as long as the engineering Settings are completely consistent, you can achieve the client test environment = online environment.

The appendix

tagged pointer

Take the Tagged Pointer string