【iOS分类、关联对象】如何使用关联对象给分类实现一个weak的属性

如何使用关联对象给分类实现一个weak的属性

通过关联对象objc_setAssociatedObject中的策略policy可知,并不支持使用weak修饰对象属性:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0, //assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //strong, nonatomic
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //copy, nonatomic
    OBJC_ASSOCIATION_RETAIN = 01401, //strong, atomic
    OBJC_ASSOCIATION_COPY = 01403 //copy, atomic
};

思考:能否用assign实现?

weak和assign的区别如下:

  • **assign:**对应的所有权类型为__unsafe_unretained,当修饰对象的时候,修饰的指针指向该对象,不会去持有该对象,也不会使retainCount +1,而在指向的对象被释放时,依然指向原来的对象地址,不会被自动置为nil,所以造成了野指针,是不安全的;
  • **weak:**弱引用,不会影响对象的释放,而当对象被释放时(引用计数为0),所有指向它的弱引用都会自定被置为nil,防止野指针,之后再向该对象发消息也不会崩溃,weak是安全的;

    看以下测试代码,使用policy为OBJC_ASSOCIATION_ASSIGN的策略,会发生什么样的情况?

    //定义Person类
    @interface Person : NSObject
    @end
    @implementation Person
    - (void)dealloc {
        NSLog(@"Person dealloc");
    }
    @end
    @interface Person (Test)
    //在分类中声明UIViewController属性,用assign修饰
    @property(assign, nonatomic) UIViewController *viewController;
    @end
    @implementation Person (Test)
    - (void)setViewController:(UIViewController *)viewController {
        //利用objc_setAssociatedObject设置值,policy为OBJC_ASSOCIATION_ASSIGN
        objc_setAssociatedObject(self, @selector(viewController), viewController, OBJC_ASSOCIATION_ASSIGN);
    }
    - (UIViewController *)viewController {//取值
        return objc_getAssociatedObject(self, _cmd);
    }
    @end
    

    使用assign修饰对象,当离开作用域后,产生野指针访问Crash(如图),如何避免这个问题?

    1、通过中间对象的方式

    1.1、利用OBJC_ASSOCIATION_RETAIN_NONATOMIC + weak来实现;

    创建中间类:

    @interface WeakObjWrapper : NSObject
    @property(weak, nonatomic) id weakObj;
    @end
    @implementation WeakObjWrapper
    - (instancetype)initWithWeakObject:(id)weakObj {
        if (self = [super init]) {
            _weakObj = weakObj;
        }
        return self;
    }
    @end
    

    实现属性的setter和getter:

    @interface Person (Test)
    @property(weak, nonatomic) UIViewController *viewController;
    @end
    @implementation Person (Test)
    - (void)setViewController:(UIViewController *)viewController {
        WeakObjWrapper *warpper = objc_getAssociatedObject(self, @selector(viewController));
        //用warpper保存传递进来的值
        if (!warpper) {//warpper不存在则创建
            warpper = [[WeakObjWrapper alloc] initWithWeakObject:viewController];
        }
        else {//已经存在直接赋值
            warpper.weakObj = viewController;
        }
        //保存的实际上是warpper对象
        objc_setAssociatedObject(self, @selector(viewController), warpper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (UIViewController *)viewController {
        //获取到warpper
        WeakObjWrapper *warpper = objc_getAssociatedObject(self, _cmd);
        //取出warpper中的值
        return warpper.weakObj;
    }
    @end
    

    objc_setAssociatedObject实际上存储的是WeakObjWrapper对象,对WeakObjWrapper对象产生强引用,WeakObjWrapper对象内部弱持有传递进去的值,保证在对象释放的时候,自动把值设置为nil,避免了崩溃;

    1.2、借助OBJC_ASSOCIATION_COPY_NONATOMIC和弱引用block

    -(void)setWeakvalue:(NSObject *)weakvalue {
        __weak typeof(weakvalue) weakObj = weakvalue;
        typeof(weakvalue) (^block)() = ^(){
            return weakObj;
        };
        objc_setAssociatedObject(self, weakValueKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    -(NSObject *)weakvalue {
        id (^block)() = objc_getAssociatedObject(self, weakValueKey);
        return block();
    }
    

    2、借助runtime

    继续使用OBJC_ASSOCIATION_ASSIGN,利用runtime动态的创建传进去值的类的子类,在子类的dealloc中,将objc_setAssociatedObject中的value设置为nil,销毁并移除了关联对象,避免Crash,具体实现如下(具体使用已在注释中说明):

    void weak_setAssociatedObject(id _Nonnull object,
                                  const void * _Nonnull key,
                                  id _Nullable value) {
        //派生一个子类,类名为WeakObjWrapper+value对应的类名
        const char *clsName = [[NSString stringWithFormat:@"WeakObjWrapper%@", [value class]] UTF8String];
        
        //获取派生的子类
        Class childCls = objc_getClass(clsName);
        
        //如果子类不存在,利用runtime动态的创建一个子类
        if (!childCls) {
            childCls = objc_allocateClassPair([value class], clsName, 0);
            objc_registerClassPair(childCls);
        }
    
     //注册dealloc方法SEL
        SEL sel = sel_registerName("dealloc");
        
        //获取dealloc对应的类型编码
        const char *deallocEncoding = method_getTypeEncoding(class_getInstanceMethod([value class], sel));
        
        // 注意:内部持有value此处需要弱引用处理一下
        __weak typeof(value) weakValue = value;
        
        // 创建一个指向在调用dealloc方法时调用指定block的函数指针
        IMP deallocImp = imp_implementationWithBlock(^(id _childCls) {
            //在子类的dealloc方法中将value设置为nil,避免崩溃
            objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_ASSIGN);
            //派生的子类的dealloc方法会被调用,父类的不再被调用,故在此处调用一下父类的
            ((void (*)(id, SEL))(void *)objc_msgSend)(weakValue, sel);
        });
    
     //给子类添加dealloc方法
        class_addMethod(childCls, sel, deallocImp, deallocEncoding);
        
        //将value对应的isa指向子类
        object_setClass(value, childCls);
        
        //设置关联对象
        objc_setAssociatedObject(object, key, value, OBJC_ASSOCIATION_ASSIGN);
    }
    

    注意:在派生的子类,添加的实现dealloc的方法中,重新调用一下父类的dealloc保证原有的类的释放关系不被破坏;调用(实现属性的getter和setter):

    @interface Person (Test)
    @property(assign, nonatomic) UIViewController *viewController;
    @end
    @implementation Person (Test)
    - (void)setViewController:(UIViewController *)viewController {
        weak_setAssociatedObject(self, @selector(viewController), viewController);
    }
    - (UIViewController *)viewController {
        return objc_getAssociatedObject(self, _cmd);
    }
    @end
    

    总结

    关联对象中如何实现weak属性?

    • 关联对象objc_setAssociatedObject中的策略policy可知,并不支持使用weak修饰对象属性;
    • 可以借助中间类(OBJC_ASSOCIATION_RETAIN_NONATOMIC + weak)来实现;
    • 也可以继续使用OBJC_ASSOCIATION_ASSIGN,利用runtime动态的创建传进去值的类的子类,在子类的dealloc中,将objc_setAssociatedObject中的value设置为nil,销毁并移除了关联对象,从而避免了Crash;

      参考1:https://www.cnblogs.com/huangzs/p/14479408.html

      参考2:https://developer.aliyun.com/article/1321927#:~:text=1%20关联对象objc_setAssociatedObject中的策略policy可知,并不支持使用weak修饰对象属性;%202%20可以借助中间类(OBJC_ASSOCIATION_RETAIN_NONATOMIC,%2B%20weak)来实现;%203%20也可以继续使用OBJC_ASSOCIATION_ASSIGN,利用runtime动态的创建传进去值的类的子类,在子类的dealloc中,将objc_setAssociatedObject中的value设置为nil,销毁并移除了关联对象,从而避免了Crash;