上一篇分析了分类的底层加载流程,那么关于类拓展以及如何关联对象的,接下来将一一分析。
1.类的拓展
分类category:为主类提供分类,在oc中生成.h和.m文件。可以添加方法,实现他供主类调用。也可以添加属性,但是不会生成setter,getter方法,需要手动生成才能调用。拓展extension:特殊的分类,也叫匿名分类。可以添加属性和方法,但是是私有的。 创建方式:
1.1 拓展extension的cpp文件分析
在日常开发中,我们创建一个ViewController的时候会自动创建它的拓展
我们在这边添加的属性和方法这在
ViewController.m中访问,外界无法访问,所以它是私有的。
那么拓展的底层是什么呢?在main中自定义一个类以及它的拓展。
使用
clang -rewrite-objc main.mm -o main.cpp 生成cpp文件查看下。搜索下ext_name
属性生成带下划线的变量,并生成setter和getter方法。
同时在编译的时候把拓展的方法添加到主类的
方法列表
1.2 拓展extension的底层源码分析
我们依然在实现类的地方打上断点
在实现类的时候类拓展的方法也在方法列表中,说明在编译的时候拓展和主类
一起编译进来,拓展依附于主类。
2. 分类关联对象分析
之前我们知道在分类中添加属性不会自动生成setter和getter方法
所以要用运行时
手动实现他们,才能储值和取值。
2.1 关联对象setter方法分析
objc_setAssociatedObject方法
void
//外部调用
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);//内部实现
}
这个是一种接口模式,外部不会变化,底层更新不会影响外部的调用。参数1:对象,将要赋值的对象;参数2:标识符,方便下次查找;参数3:值;参数4:策略,属性的描述比如copy,strong等。
objc_AssociationPolicy如下:
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. */
};
继续看下object_set_associative_reference的代码
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{ //传参:对象,要存值的对象;标识符,方便下次值;要存的值;策略,属性的策略,属性的修饰nomatoic
//这段代码在nil被传递给object和key时工作。一些代码可能依赖它来避免崩溃。明确地检查和处理它
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;//对象和值不存在直接返回
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
DisguisedPtr<objc_object> disguised{(objc_object *)object};//封装一下object,类型是DisguisedPtr
ObjcAssociation association{policy, value};//包装一下策略,和值。
// retain the new value (if any) outside the lock.
association.acquireValue();//根据策略类型进行处理
bool isFirstAssociation = false;
{
AssociationsManager manager;//初始化manager,不是全场唯一
AssociationsHashMap &associations(manager.get());//AssociationsHashMap唯一
if (value) {
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//根据
//返回一个类
if (refs_result.second) {//refs_result的second是否为true
/* it's the first association we make */
isFirstAssociation = true;//对象object标记第一次关联
}
/* establish or replace the association *///建立或者替换关联
auto &refs = refs_result.first->second;//取出关联类的储存关联键值信息。
/*
refs是👇对象第一次关联的时候
second = {
Buckets = nil
NumEntries = 0
NumTombstones = 0
NumBuckets = 0
}第二次
Buckets = 0x0000000101039190
NumEntries = 1
NumTombstones = 0
NumBuckets = 4
*/
auto result = refs.try_emplace(key, std::move(association));////查找当前的key是否有association关联对象
if (!result.second) {//如果为false,说明key存储了值
association.swap(result.first->second);//替换下新插入的值
}
} else {//如果传的是空值,则移除关联,相当于移除
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);
}
}
}
}
}
// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
if (isFirstAssociation)
//第一次关联 在锁外面调用setHasAssociatedObjects,因为这个将调用对象的_noteAssociatedObjects方法有一个,这个可以触发+initialize任意东西,包括设置更多的关联对象。
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();//旧的值release。
}
- 判断是否有值并且存储值的对象
是否存在?都不存在退出。 - 包装一下object成
DisguisedPtr数据结构,包装一下策略和值成类型是ObjcAssociation。 - 初始化
AssociationsManager(不是唯一的)。通过manage获取全场唯一AssociationsHashMap(通过map表哈希获取对应的对象表) - 判断当前是否有值?没有就走移除关联操作。
- 有值的话,通过
try_emplace去对象关联哈希表查找关联的对象,没有关联的对象创建一个,赋值空的数据结构。 - 通过key取对象
association类型是ObjcAssociation(我们一开始封装的值和策略) - 判断是否存储过值,有的话,把传进来包装过后的值进行
替换原来的值。没有的话,创建新的bucket并把包装的数据塞进去,之后setHasAssociatedObjects标记。
AssociationsManager & AssociationsHashMap分析
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>
AssociationsHashMap;
// class AssociationsManager manages a lock / hash table singleton pair.
//AssociationsManager类管理一个锁/哈希表单例对。
// Allocating an instance acquires the lock 分配实例将获得锁
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;//静态变量
public:
AssociationsManager() { AssociationsManagerLock.lock(); }//构造函数 加锁
~AssociationsManager() { AssociationsManagerLock.unlock(); }//析构函数 解锁
AssociationsHashMap &get() {
return _mapStorage.get();//关联hash表 关联get,_mapStorage是静态变量修饰的,AssociationsHashMap是唯一的
}
static void init() {
_mapStorage.init();//初始化
}
};
定义AssociationsManager,相当于自动执行它的代码块进行析构初始化,加锁不是唯一的,只是为了确保线程重复创建,初始化只是作用外面的代码块区间。外面可以定义多个AssociationsManager manager。我们仿照这个格式自己写一个
AssociationsHashMap类型的哈希map, 是通过_mapStorage.get()获取的,而_mapStorage是stantic修饰的所以是唯一的
之后对要储存的值进行包装
发现refs_result的类型很多,我们只要关注它的值就好,也就是由{objc,bool}组成。
try_emplace分析
// Inserts key,value pair into the map if the key isn't already in the map.
//如果键不在映射中,则将键、值对插入映射。
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.如果key不在map中,则就地构造该值,否则不会移动它。
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))//根据key(对象的封装)查找,找到了赋值给TheBucket
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.//已经在map表中,返回TheBucket,false表示已经存在了
// Otherwise, insert the new element.
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);//插入新的
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);//true 表示第一次往哈希map中添加
}
- 根据这个
key也就是我们之前包装的对象,在哈希map表中查找,有的话直接返回TheBucket,并构造makeIterator最后一个传false,表示已经存在了。 - 没有找到,就创建一个
BucketT,返回这个空的BucketT,最后一个值传true,表示第一次关联这个对象。 进入LookupBucketFor进行查看发现2个这个方法,但是传参不一样。调试外部是调用第二个重载函数,内部是调第一个。
先看内部的实现LookupBucketFor主要内部通过whild循环,哈希查找。
外部外部调用LookupBucketFor
继续下面的流程,通过
template我们会在对象的哈希表中查找到这个对象TheBucket,没有的话,就创建一个,并进行标记也就是下面second的值为true。
继续下面的流程,isFirstAssociation =true 标记第一次关联对象。之后我们打印下关联对象的键值
第一次打印的时候是空的,下次给这个对象关联赋值的时候有值了。
查找到对象后在相同的方式通过
try_emplace在哈希查找我们要存储的键值对,没有的话就把之前包装的赋值进去,已经有值的话,包装的替换原有的
- 简单总结下setter的过程:
通过
AssociationsManager获取到全局唯一的关联对象哈希表,通过我们传进来的对象使用try_emplace哈希查找这个对象的哈希表,没找到就创建一个空的对象哈希表。之后根据我们传的key再次用try_emplace在对象哈希表查找对应的键值,没有的话就把包装好的键值塞进去,有值的话替换成新的。二次哈希表查找,如下图:
2.2 关联对象的getter分析
objc_getAssociatedObject外部调用和上面set类似都是借口模式。看下内部具体代码
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{
AssociationsManager manager;//初始化manager
AssociationsHashMap &associations(manager.get());//通过manager获取关联对象哈希表
AssociationsHashMap::iterator i = associations.find((objc_object *)object);//查找这个对象buckets
if (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取
ObjectAssociationMap &refs = i->second;//找到迭代器的属性键值表
ObjectAssociationMap::iterator j = refs.find(key);//根据key去查找属性列表找到对应的bucket
if (j != refs.end()) {
association = j->second;//找到对应的ObjcAssociation
association.retainReturnedValue();//策略类型是retain的话 objc_retain(_value)
}
}
}
return association.autoreleaseReturnedValue();//返回值_value
}
- 和setter中一样创建初始化
manger,通过manger获取唯一的关联对象哈希map表 - 根据传入的object在对象哈希表中根据
find查找这个迭代查询器 - 如果这个迭代查询器不是最后一个,取出
ObjectAssociationMap,就是关联对象。 - 在关联对象有
Buckets表中通过key,find这个对应的桶子。 - 找到这个桶子赋值给开始定义的
association - 通过
association取出这个值返回。 具体流程:
哈希表中
NumBuckets:开辟的空间大小几个bucket;Buckets:桶子数组类;NumEntries:实际存储的个数;NumTombstones:销毁的数量。根据对象找到对应的迭代器。
发现其实关联对象哈希表和具体属性哈希表结构都一样都是
Buckets,NumEntries,NumTombstones,NumBuckets。这里通过find查找键值对,找到bucket。
3.补充
- 不管对象哈希表还是键值哈希表,在插入的时候都遵守
3/4和2倍扩容规律。
内部实现的方法
InsertIntoBucketImpl和我们之前cache_t中缓存策略类似
3/4扩容。我们新增1个属性,一共3个属性。
- AssociationsHashMap 唯一性验证 我们在作用域复制一份相同代码
同时关闭线程锁
添加断点,打印里面的值说明是一样的
4. 总结
- 类的拓展是一个
匿名的分类,它的属性和方法是私有的。在oc中,通常在.m系统会帮我们自动实现。在编译的时候就添加到主类的方法列表和属性列表中在data()中,实现类的时候会赋值给类。 - 分类通常支持属性或者成员变量的
setter和getter方法,需要添加的话,要手动实现它的setter和getter方法。 setter方法底层是通过关联对象哈希表对应的object找到这个对象,没有的话创建一个空的桶子。在根据对象的属性哈希表通过传入的key哈希找到键值对bucket。是否是存在,不存在包装好的association存入到对应的key。存在的话进行替换。也就是2次哈希查找。getter方法底层是通过传入的对象,用关联对象哈希表查找到这个对象,之后在这个对象的属性列表中通过key哈希查找到对应的value,返回。