类拓展、分类、关联对象

720 阅读4分钟

类扩展与分类的区别。

  1. category: 类别,分类
  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员变量,也无法取到(注意:其实可以通过 runtime 给分类添加属性)
  • 分类中用 property 定义变量,只会生成变量的 getter, setter 方法的声明,不能生成方法实现和带下划线的成员变量。
  1. extension: 类扩展
  • 可以说成是特殊的分类,也称作匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

类扩展底层代码实现

首先我们在 main.m 文件中定义一个 LGPerson 类,并在类扩展中添加属性与方法。然后通过 clang -rewrite-objc main.m 生成 cpp 文件来看一下底层 c++ 代码的实现。

@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



int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson * person = [LGPerson alloc];
        [person saySomething];
    }
    return 0;
}

image.png

生成 cpp 文件之后我们可以看到在类扩展里面添加的方法 ext_instanceMethod 一样被加载到 method_list 中来了。

类扩展是否影响类的加载和编译

前面我们讲分类的时候知道分类会影响类的加载和编译,那么类扩展是否也会影响呢?这里我们为 LGPerson 类添加类扩展并添加方法,然后执行源码并在 realizeClassWithoutSwift 函数中打印 ro 来看一下。

@interface LGPerson ()

- (void)ext_instanceMethod;

+ (void)ext_classMethod;
@end

@implementation LGPerson

+ (void)load{}

- (void)saySomething{
    NSLog(@"%s",__func__);
}

+ (void)sayHappy{
    NSLog(@"LGPerson say : Happy!!!");
}



- (void)sayHello1{
    NSLog(@"sayHello1 %s", __func__);
}

+ (void)say6661{
    NSLog(@"say6661 %s", __func__);
}


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

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

@end

image.png

我们在这里不断 p $1.get(n).big(),当 p $1.get(2).big() 的时候可以看到输出了类扩展中添加的方法 ext_instanceMethod,可以看出类扩展中数据会作为类的一部分被一块加载。

关联对象

- (void)setCate_name:(NSString *)cate_name{
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_name{

    return  objc_getAssociatedObject(self, "cate_name");

}

我们通常通过 runtimeobjc_setAssociatedObject 方法来添加分类的关联对象,那么我们来看一下 objc_setAssociatedObject 方法的底层是如何实现的(这里是 779 版本)。

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

通过底层源码可以看到这里会调用 _object_set_associative_reference 方法,不同版本的源码这里实现会有不同,但是最上层都是调用 objc_setAssociatedObject 方法,这里就提现了苹果的分层思想,也是值得我们学习的,这里就保证了最上层 api 的稳定性。

接着我们继续分析 _object_set_associative_reference 方法。

image.png image.png

首先我们先看下 _object_set_associative_reference 方法的简单注释及这张结构体图。通过结构图我们可以看到,ObjcAssociationObjcAssociation 会被包装成 ObjcAssociation 的形式,然后通过键值匹配的形式被存储到 ObjectAssociationMap,每个对象会对应一张 ObjectAssociationMap 表,最后每个对象跟对应的ObjectAssociationMap 又会以键值的形式存到 AssociationsHashMap,这里是双层 hashMap 结构。AssociationsHashMap 是一张全局唯一的表。具体原因我们可以看下 AssociationsManager

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    // _mapStorage 是一个静态变量,写到这里代表只有 AssociationsManager 才能调起 _mapStorage
    // 所以不同的 AssociationsManager 这里调用的都是同一个 _mapStorage
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    // 所以得到的 AssociationsHashMap 是唯一的
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};

image.png

通过打印我们也可以看到 AssociationsHashMaprefs_result 的数据结构。这里我们大致了解了关联对象的数据结构,那么 key , value 具体是如何存储的呢?我们接着继续往下看。

if (value) {
            // 第一次调用的时候 try_emplace 会创建一个空的 TheBucket
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            // 第二次再执行的时候会对 TheBucket 的 value 赋值为 association
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            // 如果 value 为空这里就会清空
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    // 这里会先创建一个空的 BucketT
    BucketT *TheBucket;
    // 这里 key 是包装的对象,被关联对象,会判断能不能找到对应的 TheBucket
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // 找不到就会来到这里会插入一个空的 TheBucket
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

最后总结一下关联对象的设值流程:

关联对象:设值流程
1:创建一个AssociationsManager管理类
2:获取唯一的全局静态哈希Map
3:判断是否插入的关联值是否存在:
3.1: 存在走第4步
3.2:不存在就走:关联对象插入空流程
4:创建一个空的ObjectAssociationMap去取查询的键值对
5:如果发现没有这个key就插入一个空的BucketT进去返回
6:标记对象存在关联对象
7:用当前修饰策略和值组成了一个ObjcAssociation 替换原来BucketT中的空
8:标记一下ObjectAssociationMap的第一次为 false\

关联对象插入空流程
1:根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器
2:清理迭代器
3:其实如果插入空置相当于清除\