我们平时在开发的时分常常会运用分类来添加方法、协议、特色,但在添加特色的时分特色是不会自动生成成员变量的,这时分我们就需求相关方针来动态存储特色值。

@interface NSObject (Study)
@property (nonatomic, strong) NSObject *obj1;
@property (nonatomic, strong) NSObject *obj2;
- (void)instanceMethod;
+ (void)classMethod;
@end
static const void *NSObjectObj1Name = "NSOBJECT_OBJ1";
@implementation NSObject (Study)
@dynamic obj2;
- (void)setObj1:(NSObject *)obj1 {
    objc_setAssociatedObject(self, &NSObjectObj1Name, obj1, OBJC_ASSOCIATION_RETAIN);
}
- (NSObject *)obj1 {
    return objc_getAssociatedObject(self, &NSObjectObj1Name);
}
- (void)instanceMethod {
    NSLog(@"-类名:%@,方法名:%s,行数:%d",NSStringFromClass(self.class),__func__,__LINE__);
}
+ (void)classMethod {
    NSLog(@"+类名:%@,方法名:%s,行数:%d",NSStringFromClass(self.class),__func__,__LINE__);
}
@end

这段代码包含Object-C的两个知识点,分别是分类相关方针,本文首要围绕这两个知识点来进行探求。

分类

可以运用clang重写将上面的代码成c++代码。

iOS八股文(十)分类和相关方针源码解析

重写后的要害代码:

static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
    "NSObject",
    0, // &OBJC_CLASS_$_NSObject,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Study,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_Study,
    0,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_Study,
};

可以看到其根柢的完结是_category_t这个结构,那么我们可以凭仗objc4(828.2)源码来查找关于category_t的界说:

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

根据源码的界说,我们可以总结:

  • 分类里面即有实例方法列表又有类方法列表
  • 分类没有成员变量列表

分类的加载

分类的加载是在objc中完结的。 在源码attachCategories的完结中:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    //。。。缩简//
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    //新建rwe
    auto rwe = cls->data()->extAllocIfNeeded();
    //debug代码可以放这儿
    //遍历每个分类
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        //获取分类里面的方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
       //。。缩简了协议和特色的内容
    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        //添加分类的方法到rwe中
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

可以在这段代码里面加上类名判别,然后打上断点。运用lldb调试查看添加方法的进程。objc源码调试可参看这儿。

    const char *mangledName = cls->nonlazyMangledName();
    //你添加分类的类名
    const char *className = "OSObject";
    if (strcmp(mangledName, className) == 0 && !isMeta) {
        printf("debug find it");
    }

留心:这儿面,分类和本类都需求完结+load方法才可以。 我们先看断点的仓库信息

iOS八股文(十)分类和相关方针源码解析
可以看到是load_images中调用的。前面的文章现已说明过load_images的调用机会。 这儿可以再温习一遍。

接下来我们通过lldb来调试。在运用lldb的时分,要多看源码,在源码中寻找可以运用的方法。假设是地址就用*来去向地址里面的内容。假设是内容看源码中的界说,运用其方法获取有用信息。

iOS八股文(十)分类和相关方针源码解析

iOS八股文(十)分类和相关方针源码解析
这儿需求留心⚠️,运用了2种获取列表数量的方法,其间一种是不精确的,但是在加载完分类的时分,不精确的方法就正确了,暂时没找到原因。我的本类中有一个实例方法,分类里面也有一个实例方法,在没有加载分类的时分,我的方法列表里面的数量是1(第2中方法查看得到)。我们持续过断点,再设置完分类后,我们相同方法再来看效果:

iOS八股文(十)分类和相关方针源码解析
可以看到加载完分类之后,方法列表的数量是2。

纠正

这儿的lldb指令有些过于杂乱,在我们获取到method_array_t的时分,该结构体有一个count()方法的,该方法可直接获取数量。

运用count()方法的调试如下:

iOS八股文(十)分类和相关方针源码解析
在没有加载分类的时分count为1,在看看加载完分类之后的:

iOS八股文(十)分类和相关方针源码解析
可以看到count为2,得到了和上面相同的结论。

相关方针

回到我们一开始的代码,还有一个相关方针。我们先在objc源码中找到相关方针api的完结部分:

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}
void objc_removeAssociatedObjects(id object)
{
  if (object && object->hasAssociatedObjects()) {
    _object_remove_assocations(object, /*deallocating*/false);
  }
}

可以看到是调用了内部函数_object_set_associative_reference,解析注解如下:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // 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;
//isa有一位信息为禁止相关方针,假设设置了,直接报错
    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};
    //包装值和特色信息
    ObjcAssociation association{policy, value};
    // retain the new value (if any) outside the lock.
    //设置特色信息
    association.acquireValue();
    bool isFirstAssociation = false;
    {
        //调用结构函数,结构函数内加锁操作
        AssociationsManager manager;
        //获取全局的HasMap
        AssociationsHashMap &associations(manager.get());
        //假设值不为空
        if (value) {
            //去相关方针表中找方针对应的二级表,假设没有内部会从头生成一个
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            //假设没有找到
            if (refs_result.second) {
                /* it's the first association we make */
                //说明是第一次设置相关方针,把是否相关方针设置为YES
                isFirstAssociation = true;
            }
            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            //在二级表中找key对应的内容,
            auto result = refs.try_emplace(key, std::move(association));
            //假设现已有内容了,没有内容上面根据association现已刺进了值,所以啥也不用干
            if (!result.second) {
                //替换掉
                association.swap(result.first->second);
            }
         //假设value为空
        } else {
            //通过object找对应的二级表
            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);
                    }
                }
            }
        }
    }
    // 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)
        object->setHasAssociatedObjects();
    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

其间需求留心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.
  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(KeyT &&Key, Ts &&... Args) {
    BucketT *TheBucket;
    //假设现已存在了
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.
    // Otherwise, insert the new element.
    //不存在就刺进一个新的方针
    TheBucket =
        InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

这儿回来的是一个迭代器,假设有内容回来对应的迭代器,假设没有的话,添加一个,并回来迭代器。

可以看到运用了两次try_emplace方法,可以得知他是嵌套两层的HashMap结构,根据上面代码的了解,可以得到以下结构图:

iOS八股文(十)分类和相关方针源码解析
下面我们在看看get_associtiond的源码:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};
    {   //加锁
        AssociationsManager manager;
        //全局的表
        AssociationsHashMap &associations(manager.get());
        //通过object找对应的二级表
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            //在二级表内通过key在找对应的值
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }
    //取值并回来然后放到自动释放池中
    return association.autoreleaseReturnedValue();
}

除了getset方法,在方针被毁掉的时分还会调用remove方法,从全局的相关方针表中把方针对应的相关表删除。后边在收拾方针毁掉流程的时分会涉及。

参看链接

  • objc4源码编译与调试

以上就是我们常常在分类里面编码对应源码的解析。希望各位看官有什么不同见地可以一同交流学习。

到这儿,现已通过十篇文章对Object-C的动态性,从多角度进行了剖析。虽然起名为八股文系列,但个人认为除了面试装X之外,认证探求后,对底层也有更深一步的了解,能为往后编码供给必定的帮助,尤其是通过对源码的剖析,可以效法优异代码的逻辑构思和编码风格。这些博客的目的,首先是希望自己能通过写博客,增强回忆,在往后边试温习的时分有一份不错的温习资料,其二是记载学习探求进程,能对外传达自己微薄的能量,能和优异的从业者交流交流,然后进一步进步自己。在写博客的进程中查看学习了很多优异的博客,大多现已注明晰参看链接,也有部分遗失的,如有雷同,可联络作者,作者会第一时间补全出处。写博客不易,转载注明出处。