Demo portal: AssociatedObjectDemo

Abstract: Programming, only understand the principle of not, must be actual combat to know the application scenario. This article is the related object of this series, which attempts to explain the runtime theory as well as some practical scenarios. In this article, section 1 introduces associative objects and how to associate them. Section 2 introduces one of the most common scenarios for associative objects: adding attributes to classes. Section 3 introduces another important scenario for associative objects: associating event Block bodies with UI controls (e.g., UIAlertView, UIButton, etc.).

1. What are associated objects

1.1 Associated Objects

Categories and Associated Objects are two features of objective-C’s extension mechanism: categories, through which methods can be extended; Associated Object, through which attributes can be extended;

<objc/ Runtime. h> <objc/runtime.h> <objc/runtime.h> <objc/runtime.h> <objc/runtime.h> <objc/runtime.h> <objc/runtime.h>

1.2 How do I Associate Objects

The Runtime provides us with three apis to manage associated objects (store, retrieve, remove) :

Void objc_setAssociatedObject(id object, const void *key, id value, Objc_getAssociatedObject (id object, Const void *key) // Remove the associated object void objc_removeAssociatedObjects(id object)Copy the code

The parameters

  • id object: Indicates the associated object
  • const void *key: The associated key must be unique
  • id value: Associated object
  • objc_AssociationPolicy policy: Memory management policy

2. Associate objects: Add “attributes” to categories

2.1 Classification limitations

So let’s look at an example of @property

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end
Copy the code

There are three things you do when you use @property above:

  • Generate instance variables_property
  • generategettermethods- property
  • generatesettermethods- setProperty:
@implementation DKObject {
    NSString *_property;
}

- (NSString *)property {
    return _property;
}

- (void)setProperty:(NSString *)property {
    _property = property;
}

@end
Copy the code

This code is generated by the compiler for us, and you can’t see it, but it’s here. However, if we write an attribute in the class, we get a warning that the @property in the class does not generate instance variables and access methods for us, and we need to implement them manually.

Because @property does not automatically generate instance variables and access methods in a classification, associated objects are commonly used to add “properties” to existing classes. The solution: You can use two methods, objc_getAssociatedObject and objc_setAssociatedObject, to simulate the access methods of properties, and use associated objects to simulate instance variables.

2.2 Usage Analysis

  • NSObject+AssociatedObject.m
#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>

@implementation NSObject (AssociatedObject)

- (void)setAssociatedObject:(id)associatedObject
{
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject
{
    return objc_getAssociatedObject(self, _cmd);
}

@end
Copy the code
  • ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSObject *objc = [[NSObject alloc] init];
    objc.associatedObject = @"Extend Category";
    
    NSLog(@"associatedObject is = %@", objc.associatedObject);
}
Copy the code

Where the _cmd generation refers to the selector child of the current method, that is, @selector(categoryProperty). _cmd represents the selector of the current method in objective-C methods, just as self represents the object instance of the current method call. So now, the _cmd scope is only in the current method, which is the name of the current method, @selector.

Therefore, it can also be written as follows:

- (id)associatedObject
{
    return objc_getAssociatedObject(self, @selector(associatedObject));
}
Copy the code

Also, if you look at OBJC_ASSOCIATION_RETAIN_NONATOMIC, you can see that it is an enumeration type, with the complete enumeration items as follows:

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

We can see a lot from the comments here, that is, the different objc_associationPolicies correspond to different property modifiers, as shown in the following table:

objc_AssociationPolicy modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN atomic, strong
OBJC_ASSOCIATION_COPY atomic, copy

The attribute associatedObject we implement in our code is equivalent to using nonatomic and strong modifiers.

2.3 Actual Combat Scenarios

Requirement: For example, if you add an event to a UIView, you can add a UITapGestureRecognizer to it, but the click event can’t carry an NSString (although it can carry an int tag), so subsequent methods that respond to the event can’t tell where the event was activated. So, can you carry additional information in this way of adding events?

Add an attribute to the UITapGestureRecognizer and create a new class of UITapGestureRecognizer.

Classification:

  • UITapGestureRecognizer+NSString.h
#import <UIKit/UIKit.h>@interface UITapGestureRecognizer (NSString) @Property (nonatomic, strong) NSString *dataStr; @endCopy the code
  • UITapGestureRecognizer+NSString.m
#import "UITapGestureRecognizer+NSString.h"
#import <objc/runtime.h>Static char *PersonNameKey ="PersonNameKey";

@implementation UITapGestureRecognizer (NSString)

- (void)setDataStr:(NSString *)dataStr{
    objc_setAssociatedObject(self, PersonNameKey, dataStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)dataStr{
    return objc_getAssociatedObject(self, PersonNameKey);
}

@end
Copy the code

Call:

  • VCtableView:cellForRowAtIndexPath:The event is fired by the cell in the proxy method
UITapGestureRecognizer *signViewSingle0 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
            //partnercode
signViewSingle0.dataStr = [cell.cellMdl.partnercode copy];
[cell.contractView addGestureRecognizer:signViewSingle0];
Copy the code
  • VC writes a separate response method
- (void)tapAction:(UITapGestureRecognizer *)sender
{
    UITapGestureRecognizer *tap = (UITapGestureRecognizer *)sender;
    //partnercode
    [self requestCallConSetWithPartnerCode:tap.dataStr];
}
Copy the code

In this way, the method that responds to the event can proceed to the next step based on the information brought in by the event activator, such as making a network request based on a parameter it brought in, and so on.

2.4 Third-party frameworks applied to this knowledge point

  • Masonry

  • MJRefresh

2.5 This will generate the _ variable?

Although it is possible to add “attributes” to a classification simulatively, it is only a simulation. @property does not generate _ variables in a classification, nor does it implement getters and setters. We implement getters and setters only. We don’t automatically generate variables that start with an underscore!

3. Associated object: Associates the event Block body with the UI control

3.1 UIAlertView

The UIAlertView class is often used in iOS development, which provides a standard view to display warning messages to the user. When a user presses a button to close the view, this action needs to be handled using the Delegate Protocol, but to set up this delegate mechanism, you need to separate the code that creates the warning view from the code that handles the button action. Because the code is split into two pieces, it is a bit messy to read.

Plan 1: Traditional plan

So, for example, when we’re using UIAlertView, we usually say:

  • Test2ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test2ViewController";
    
    [self popAlertViews1];
}

#pragma mark - way1
- (void)popAlertViews1{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 0) {
        [self doCancel];
    } else {
        [self doContinue]; }}Copy the code

If we want to handle multiple warning views in the same class, the code becomes more complex, and we must check the passed alertView parameter in the Delegate method and select the appropriate logic accordingly.

It would be much easier if we could just write the logic for each button when we create UIAlertView. This can be done by associating objects. Once the warning view is created, set an associated “block” and wait until the Delegate method is executed to read it out. The following are improvements to this scheme.

Scheme 2: Associate the Block body

In addition to the traditional approach in the previous scenario, we can associate a Block with UIAlertView using an associative object: First set the associated callback (objc_setAssociatedObject) when UIAlertView is created, and then fetch the associated callback (objc_getAssociatedObject) in UIAlertView’s proxy method.

  • Test2ViewController.m
#pragma mark - way2
- (void)popAlertViews2 {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    void (^clickBlock)(NSInteger) = ^(NSInteger buttonIndex){
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doContinue]; }}; objc_setAssociatedObject(alert,CMAlertViewKey,clickBlock,OBJC_ASSOCIATION_COPY); [alert show]; } // UIAlertViewDelegate protocol method - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ void (^clickBlock)(NSInteger) = objc_getAssociatedObject(alertView, CMAlertViewKey); clickBlock(buttonIndex); }Copy the code
Option 3: Continue to improve: encapsulate the associated Block body as an attribute

In the above scenario, if more locations are needed, the same code will be more redundant, so we can wrap the code that sets the Block into a UIAlertView category.

  • UIAlertView+Handle.h
#import <UIKit/UIKit.h>// Declare a callback to a button click event. Block typedef void (^ClickBlock)(NSInteger buttonIndex); @interface UIAlertView (Handle) @property (copy, nonatomic) ClickBlock callBlock; @endCopy the code
  • UIAlertView+Handle.m
#import "UIAlertView+Handle.h"
#import <objc/runtime.h>

@implementation UIAlertView (Handle)

- (void)setCallBlock:(ClickBlock)callBlock
{
    objc_setAssociatedObject(self, @selector(callBlock), callBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (ClickBlock )callBlock
{
    return objc_getAssociatedObject(self, _cmd);
    //    return objc_getAssociatedObject(self, @selector(callBlock));
}

@end
Copy the code
  • Test2ViewController.m
#pragma mark - way3
- (void)popAlertViews3 {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    [alert setCallBlock:^(NSInteger buttonIndex) {
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doContinue]; }}]; [alert show]; } // UIAlertViewDelegate protocol method - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ void (^block)(NSInteger) = alertView.callBlock; block(buttonIndex); }Copy the code
Option 4: Further refinement: Encapsulate the associated Block body and bind it to the initialization method

Exercise: This classification can be further improved by writing the method that sets the Block property together with the initialization method.

3.2 a UIButton

In addition to the UIAlertView mentioned above, this section uses UIButton as an example to implement a function function using the associated object: add a class to UIButton, define a method, and use block to implement button click callback.

  • UIButton+Handle.h
#import <UIKit/UIKit.h>
#import // Declare a button click callback block typedef void(^ButtonClickCallBack)(UIButton *button); @interface UIButton (Handle) // add a callBack method to UIButton - (void)handleClickCallBack:(ButtonClickCallBack)callBack; @endCopy the code
  • UIButton+Handle.m
#import "UIButton+Handle.h"// Declare a static index key for the associated object. Static char *buttonClickKey; @implementation UIButton (Handle) - (void)handleClickCallBack:(ButtonClickCallBack)callBack { // Associate a button instance with a callback block by index key:  objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self addTarget:self action:@selector(buttonClicked).forControlEvents:UIControlEventTouchUpInside]; } - (void)buttonClicked {// With the static index key, CallBack = objc_getAssociatedObject(self, &buttonClickKey);if (callBack) {
        callBack(self);
    }
}

@end
Copy the code

In Test3ViewController, import the UIButton class header we wrote, define a button object, and call this method in the class:

  • Test3ViewController.m
    [self.testButton handleClickCallBack:^(UIButton *button) {
        NSLog(@"block --- click UIButton+Handle");
    }];
Copy the code

4. Associated object: Associated observer object

Sometimes we use NSNotificationCenter or KVO in categorization. It is recommended to use the associated object as an observer, and try to avoid the object observing itself.

For example, the well-known AFNetworking listens to the NSURLSessionTask for the chrysanthemum control to get the classification of network progress:

  • UIActivityIndicatorView+AFNetworking.m
@implementation UIActivityIndicatorView (AFNetworking)

- (AFActivityIndicatorViewNotificationObserver *)af_notificationObserver {
    
    AFActivityIndicatorViewNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver));
    if (notificationObserver == nil) {
        notificationObserver = [[AFActivityIndicatorViewNotificationObserver alloc] initWithActivityIndicatorView:self];
        objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return notificationObserver;
}

- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
    [[self af_notificationObserver] setAnimatingWithStateOfTask:task];
}

@end
Copy the code
@implementation AFActivityIndicatorViewNotificationObserver

- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

    [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil];
    [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil];
    [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil];
    
    if (task) {
        if(task.state ! = NSURLSessionTaskStateCompleted) { UIActivityIndicatorView *activityIndicatorView = self.activityIndicatorView;if (task.state == NSURLSessionTaskStateRunning) {
                [activityIndicatorView startAnimating];
            } else{ [activityIndicatorView stopAnimating]; } [notificationCenter addObserver:self selector:@selector(af_startAnimating) name:AFNetworkingTaskDidResumeNotification object:task]; [notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidCompleteNotification object:task]; [notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidSuspendNotification object:task]; }}}Copy the code

5. Associated object: To avoid repeated execution

Sometimes there are some methods in OC to obtain certain data, but the process of obtaining data only needs to be executed once, and the algorithm of obtaining data may have certain time complexity and space complexity. Does it have to be done every time it’s called? Is there a way to execute a method only once and get the result of that execution directly each time the method is called? Yes, the solution is to associate the data results of an object’s methods with that object as “properties.”

There is a requirement to convert dictionaries into model objects

Solution: We first get all the property names of the object (execute once), then add them to an array, and then iterate again, using KVC for key assignment. While the program is running, it grabs the properties of the object. At this point, it makes use of the associated object at runtime, as shown in the following code.

  • Gets the names of all the properties of the object
+ (NSArray *)propertyList { // 0. If yes, return /** 1> associated object 2> associated attribute key: In OC, the class is also essentially an object */ NSArray *pList = objc_getAssociatedObject(self, propertiesKey);if(pList ! = nil) {returnpList; } // 1. Get the attribute of 'class' /** 1> class 2> count pointer */ unsigned int count = 0; Objc_property_t objc_property_t *list = class_copyPropertyList([self class], &count); NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count]; // go through the number groupfor(unsigned int i = 0; i < count; ++ I) {objc_property_t pty = list[I]; Const char *cname = property_getName(pty); [arrayM addObject:[NSString stringWithUTF8String:cname]]; } NSLog(@"% @", arrayM); // Free the array of attributes free(list); Reatin, copy, assign */ objc_setAssociatedObject(self, objc_setAssociatedObject) propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC);return arrayM.copy;
}
Copy the code
  • KVC performs key assignment
+ (instancetype)objectWithDict:(NSDictionary *)dict {
    id obj = [[self alloc] init];
    
    //    [obj setValuesForKeysWithDictionary:dict]; NSArray *properties = [self propertyList]; // Iterate over the property arrayfor (NSString *key inProperties) {// Check whether the dictionary contains the keyif(dict[key] ! // use KVC to set the value [obj]setValue:dict[key] forKeyPath:key]; }}return obj;
}
Copy the code