ios 底层类的扩展和关联对象

164 阅读6分钟

前言

上一篇分类的加载,我们分析了分类的加载,以及类的被迫营业,即如果主类没有实现load()方法而分类实现了load(),编译时会把主类强制优化非懒加载类同时把分类优化掉。我们知道类中添加属性,在编译时会生成set、get方法,但是在分类中添加的属性,编译时并不会生成set、get方法,那么分类中的属性该如何存储呢?下面就分析下类的关联对象以及类的扩展

类扩展 Extension

  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

其实我们一直在用类扩展,比如在.m文件中

@interface ViewController ()
@property(nonatomic,strong)UIWindow* __nullable newwindow;
@end
@implementation ViewController
-(void)viewDidAppear:(BOOL)animated{
}
-(void)viewWillDisappear:(BOOL)animated{
}

属性newwindow就是我们的扩展属性,注意扩展必须是放在声明之后,实现之前。我们新建一个类的扩展只是一个.h文件,编译时会被合并进.m中。我们写个demo通过clang编译验证下。

@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod;
+ (void)classMethod;
@end
@interface LGStudent ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, assign) int ext_age;
- (void)ext_instanceMethod;
+ (void)ext_classMethod;
@end
@implementation LGStudent
- (void)instanceMethod{
    NSLog(@"%s",__func__);
}

+ (void)classMethod{
    NSLog(@"%s",__func__);
}
- (void)ext_instanceMethod{
    NSLog(@"%s",__func__);
}
+ (void)ext_classMethod{

    NSLog(@"%s",__func__);
}
@end

main.m中写了一个LGStudent的扩展,定义了属性ext_nameext_age,以及方法ext_instanceMethodext_classMethod,clang看一下编译后的情况main.cpp文件

struct LGStudent_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	int _ext_age;
	NSString *_name;
	NSString *_ext_name;
};
static void _C_LGStudent_ext_classMethod(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_40_l7v4nh0543xbbd0lfg8dy5g00000gn_T_main_50c04f_mi_3,__func__);
}

static NSString * _I_LGStudent_name(LGStudent * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGStudent$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LGStudent_setName_(LGStudent * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGStudent, _name), (id)name, 0, 1); }

static int _I_LGStudent_age(LGStudent * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_age)); }
static void _I_LGStudent_setAge_(LGStudent * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_age)) = age; }

static NSString * _I_LGStudent_ext_name(LGStudent * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGStudent$_ext_name)); }
static void _I_LGStudent_setExt_name_(LGStudent * self, SEL _cmd, NSString *ext_name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGStudent, _ext_name), (id)ext_name, 0, 1); }

static int _I_LGStudent_ext_age(LGStudent * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_ext_age)); }
static void _I_LGStudent_setExt_age_(LGStudent * self, SEL _cmd, int ext_age) { (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_ext_age)) = ext_age; }

我们看到在编译时类的扩展属性以及方法就被合并到了主类中,所以类的扩展不会像分类那样影响类的加载。

关联对象

我们知道如果在分类中添加属性以及读写属性,必须使用关联对象,分类中的属性是动态添加的所以没有get和set方法,那么关联对象是如何读写属性的呢?我们先用关联对象实现一下分类中属性的读写。

@interface LGPerson (wgy)
@property(nonatomic,copy)NSString* wgyname;
@end
@implementation LGPerson (wgy)
-(void)setWgyname:(NSString *)wgyname{
    objc_setAssociatedObject(self, "wgyname", wgyname, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)wgyname{
    return objc_getAssociatedObject(self, "wgyname");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson * person = [LGPerson alloc];
        person.wgyname=@"NB";
    }
    return 0;
}

分析:添加一个LGPerson分类,在分类中添加属性wgyname,通过关联方法objc_setAssociatedObject实现属性的存储,通过objc_getAssociatedObject实现属性的读取。下面看一下objc_setAssociatedObject源码是如何实现属性值的存储的,跟进objc_setAssociatedObject源码发现调用了_object_set_associative_reference方法。

_object_set_associative_reference存储值

void

_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    if (!object && !value) return;
    if (object->getIsa()->forbidsAssociatedObjects())
    //把object构造成一个统一的对象disguised,
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    //把value构造成统一的对象
    ObjcAssociation association{policy, value};
    association.acquireValue();
    bool isFirstAssociation = false;
    {   //AssociationsManager线程安全获取和释放锁
        //通过构造函数获取锁,跳出作用域通过析构函数释放锁
        AssociationsManager manager;
        //获取关联对象hash表的单例
        AssociationsHashMap &associations(manager.get());
        if (value) {
             //根据对象查找对象关联表,如果存在就返回,如果没有就创建对象关联表
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                isFirstAssociation = true;
            }
            //在对象关联表中,根据key查找属性关联表,如果找到就返回,没找到就创建,并且把value赋值
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {//如果value为空,就erase移除关联属性
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                //根据key查询对应的关联表
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }
    if (isFirstAssociation)
        //对象标记为存在关联对象,这个很重要,对象释放的时候会根据这个标记状态释放关联对象
        object->setHasAssociatedObjects();
    //release旧值
    association.releaseHeldValue();

}

分析:

  • 把对象和value 构造成统一的对象,便于hash存储。
  • 通过AssociationsManager获取关联表单例,为了线程安全通过构造函数获取和释放锁
  • 如果新值value不为空,根据对象查找对象关联表,如果没有就新建对象关联表;在对象关联表中,根据key查找属性关联表,如果找到就返回,没找到就创建,并且把value赋值。
  • 如果新值value为空,根据key移除关联关系
  • 第一次的时候把对象标记为存在关联对象,这个很重要,在对象释放的时候会根据这个标记状态释放关联对象表

通过上面的分析首先关联表是一个双层hash表,一个是通过对象查找的对象关联表,一个是在对象中通过key查找的属性关联表,try_emplace调用了两次,第一次是操作的对象disguised,第二次是操作的属性key。我们再来看下AssociationsManager对象以及是try_emplace是如何存储value的。

AssociationsManager

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    //全局对象
    static Storage _mapStorage;
public:
    //构造函数 获取锁
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    //析构函数释放锁
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &get() {
       //获取全局的关联表单例
        return _mapStorage.get();
    }
    static void init() {
        _mapStorage.init();
    }
};

分析:

  • AssociationsManager本身只是一个对象而非单例,只是提供一个全局静态变量_mapStorage去获取一张关联表
  • 在AssociationsManager作用域内获取锁,出了作用域析构函数释放锁

try_emplace

template <typename... Ts>

  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {

    BucketT *TheBucket;
   //根据key去查找对应的Bucket
    if (LookupBucketFor(Key, TheBucket))
     //如果找到,false表示不是第一次插入关联表
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.
    // 如果没有查询到 将数据插入bucket中,返回bucket
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
   //通过 make_pair生成相应的键值对,true表示第一次往哈希关联表中添加bucket
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

分析:

  • 根据key查找对应的bucket,如果找到了就封装返回bucket
  • 如果没有,就新建空的bucket,然后将数据存在bucket
  • bucket使我们联想到探索类时cache中的bucket,类中的bucket存储的是SEL和IMP

第一次调用try_emplace,key是对象,目的是在关联表中查找对象,如果找不到就插入对象;第二次调用try_emplace,key是属性,目的是在关联表中查找属性如果找不到就插入属性

关联对象存储过程双hash表图

image.png

总结:

  • 编译时类的扩展属性以及方法就被合并到了主类中,所以类的扩展不会像分类那样影响类的加载
  • 分类中属性的读写是通过一张全局的关联表维护的,如果类释放了,关联表中相对应的关联关系也释放。