当前位置:
首页
文章
移动端
详情

iOS手把手带你探索Category

在我们的实际开发中Category分类的使用必不可少,那么我们通过以下几个方面来探索一下分类

  • 1.什么是分类Category
  • 2.Category的作用
  • 3.CategoryExension的区别
  • 4.Category底层探究
  • 5.关联对象的探索

什么是分类(Category)

CategoryOvjective-C 2.0 之后添加的语言特性,Category作用是为已经存在的类添加方法

Category的作用

  • 1.可以减少单个文件的体积
  • 2.可以把不同的功能组织到不同的Category中
  • 3.可以按需加载
  • 4.声明私有方法
  • 5.把framework的私有方法公开

Category和Exension的区别

  • 1.Category依托当前类存在,Exension则是类的一部分
  • 2.Category在运行时期确定,Exension在编译器确定
  • 3.Category不能添加变量,Exension可以添加变量(编译时期对象的内存布局已经确定)

Category底层探究

4.1 Category的数据结构

Category的数据结构如下

struct category_t {
    //当前类的名称
    const char *name;
    //需要扩展的类对象,在编译期没有值
    classref_t cls;
    //实例方法
    WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
    //对象方法
    WrappedPtr<method_list_t, method_list_t::Ptrauth> 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;
    }
};

  • const char *name;: 当前类的名称
  • classref_t cls;:需要扩展的类对象,在编译期没有值
  • WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;:实例方法列表
  • WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;:对象方法列表
  • struct protocol_list_t *protocols;:协议列表
  • struct property_list_t *instanceProperties;:实例属性
  • struct property_list_t *_classProperties;:类属性

4.2 Category在编译时期做了什么

#import "Person+A.h"

@implementation Person (A)

- (void)funcA {
    NSLog(@"A");
}
@end

首先我们创建这样一个分类,下面我们通过clang看下在编译时期分类到底做了些什么

clang -rewrite-objc "文件名.m/h"

我们可以通过上面这个命令在终端中获取到.cpp文件

static struct _category_t _OBJC_$_CATEGORY_Person_$_A __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_A,
    0,
    0,
    0,
};

我们编译后的东西都存放在Mach-o的可执行文件中,通过字段标识当前内容。上述内容就是存在__DATA中的__objc_const的字段中

  • Person ->name字段 类的名称
  • 0, // &OBJC_CLASS_$_Person, ->cls 需要扩展的类对象,在编译期没有值
  • (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_A, -> instanceMethods 在分类中声明的实例方法

我们再来看下实例方法列表这个结构体里有些啥

 _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_A __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"funcA", "v16@0:8", (void *)_I_Person_A_funcA}}
};
  • funcA方法编号
  • v16@0:8方法签名
  • (void *)_I_Person_A_funcA 真实的函数地址
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_Person_$_A,
};

将编译期生成的分类结构体存放在__DATA中的__objc_catlist字段中

总结 Category在编译期做了什么

  • 1.把分类编译成结构体
  • 2.在对应的字段中填充相应的数据
  • 3.把所有的分类存放在__DATA数据段中的__objc_catlist字段中

4.3 Category在运行时期做了什么

要想探索Category在运行时期做了什么,首先我们来倒app的入口函数_objc_init

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    //环境变量初始化
    environ_init();
    //线程寄存器初始化
    tls_init();
    //静态初始化
    static_init();
    //runtime初始化
    runtime_init();
    //异常初始化
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

首先进行了一些初始化的操作,environ_init (),tls_init (),static_init (),runtime_init (),exception_init.

然后调用_dyld_objc_notify_register(&map_images, load_images, unmap_image);函数,通过dyld注册三个函数:map_images(map映射,当前dyld把images(镜像)加载进内存时,会出发map_images的回调), load_images(当dyld初始化images(镜像)模块时调用,+load方法也会在这里被调用), unmap_image(dyld把image移除内存时调用)

这里我们重点关注map_images

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    static bool firstTime = YES;
    //头文件信息
    header_info *hList[mhCount];
    //头文件数
    uint32_t hCount;
    size_t selrefCount = 0; 
    // Find all images with Objective-C metadata.
    hCount = 0;
    // Count classes. Size various table based on the total.
    //类的数量
    int totalClasses = 0;
    if (hCount > 0) { //读取镜像文件,读取OC相关的Section
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    } 
}

由于map_images_nolock函数中代码太多,截取了部分关键代码

_read_images函数中做了什么

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    
#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++

    if (didInitialAttachCategories) {
        //遍历整个头文件列表
        for (EACH_HEADER) { 
            //加载分类
            load_categories_nolock(hi);
        }
    }
}

由于_read_images函数中代码太多,截取了部分关键代码

  • EACH_HEADER是一个宏定义,用来遍历整个头文件列表
  • load_categories_nolock加载分类

接着往下看 load_categories_nolock

static void load_categories_nolock(header_info *hi) {
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            locstamped_category_t lc{cat, hi};

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category.
            if (cls->isStubClass()) {
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                        //把分类中声明的属性,方法添加到累的属性,方法列表中
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        //把当前分类加载到对应的类中
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                        //addForClass底层数据结构的映射
                    }
                }
            }
        }
    };
    //读取分类列表
    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

在这个函数中做了三件事

  • 1.processCatlist(hi->catlist(&count));,processCatlist(hi->catlist2(&count));读取分类列表
    1. objc::unattachedCategories.addForClass(lc, cls->ISA());当前分类加载到对应的类中
    1. attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS); 把分类中声明的属性,方法,协议添加到类的属性,方法,协议列表中

attachCategories函数的实现

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    //声明 方法列表,属性列表,协议列表所用的空间
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    if (mcount > 0) {
        //准备当前的方法列表
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        //把分类方法追加进类的方法列表中
        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);
}

由于attachCategories函数中代码太多,截取了部分关键代码

  • 1.prepareMethodLists准备当前的方法列表,核心:把方法名称放到方法列表中
  • 2.attachLists分类和本类相应的对象方法,属性,和协议进行了合并。

attachLists内部实现

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            //多对多
            uint32_t oldCount = array()->count; //获取原来方法列表中的方法个数
            uint32_t newCount = oldCount + addedCount; //得到添加后的方法个数
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); //开辟新的空间
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i]; //把原来的方法放在后面
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i]; //把新添加的方法放在数组的前面
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

这里会分三种情况:多对多,0对1,1对多,这里就以多对多为例

  • uint32_t oldCount = array()->count;获取原来方法列表中的方法个数
  • uint32_t newCount = oldCount + addedCount得到添加后的方法个数
  • array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));开辟新的空间
  • newArray->lists[i + addedCount] = array()->lists[i];把原来的方法放在后面
  • newArray->lists[i] = addedLists[i] 把新添加的方法放在数组的前面

总结Category在运行时做了什么
1.通过catlist,catlist2 (_getObjc2CategoryList)读取分类列表
2.通过addForClass当前分类加载到对应的类中
3.attachCategories把分类中声明的属性,方法,协议添加到类的属性,方法,协议列表中
4.prepareMethodLists准备当前的方法列表,核心:把方法名称放到方法列表中
5.attachLists分类和本类相应的对象方法,属性,和协议进行了合并
6.合并的顺序,先为分类中的方法,再是原类中的方法(这就是同名方法分类会优先于本类调用的原因)

多个分类/类也有同名方法,方法查找,本质上调用Runtime遍历方法列表,通过二分查找算法,不会立马返回imp指针,往前移动,找是否有同名方法。

关联对象的探索

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

在类中声明一个属性,系统会自动帮我们生成getter setter ivar.

当我们使用分类想为原类添加属性时,我们则需要使用关联对象的方式,那么关联对象和直接声明数据原理是否一样呢,我们接下来分析下

分类中我们使用objc_setAssociatedObject,objc_getAssociatedObject这两函数添加属性

首先我们来看下关联对象怎么存的objc_setAssociatedObject

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;

    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}; //把外部传入的value和策略存起来

    // retain the new value (if any) outside the lock.
    //根据上面存的policy,设置内存管理语义
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        AssociationsManager manager; //声明一个管理者,用来管理下面的AssociationsHashMap
        AssociationsHashMap &associations(manager.get());

        if (value) {
            //<first, second> -> <disguised, ObjectAssociationMap{}>
            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;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                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)
        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}
  • DisguisedPtr<objc_object> disguised{(objc_object *)object}按位取反
  • ObjcAssociation association{policy, value};把外部传入的value和策略存起来
  • association.acquireValue();根据上面存的policy,设置内存管理语义,copy, retain
  • AssociationsManager manager;声明一个管理者,用来管理下面的AssociationsHashMap
  • AssociationsHashMap &associations(manager.get());初始化HashMap
  • object->setHasAssociatedObjects();与关联对象做绑定,方便释放

根据上面的代码我们就明白了关联对象是怎么存对象的了
AssociationsManager -> <disguised , ObjectAssociationMap >
ObjectAssociationMap -> <key(外部传入的), ObjcAssociation>
ObjcAssociation -> 有俩属性 外部传入的uintptr_t _policy,id _value

objc_getAssociatedObject获取关联对象实现

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &amp;associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &amp;refs = i-&gt;second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j-&gt;second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

我们探索了objc_setAssociatedObject的实现,objc_getAssociatedObject的实现就一幕了然了
通过AssociationsManager获取到AssociationsHashMap,再通过获取的AssociationsHashMap得到association

下面我们再来看下关联对像的释放

- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj-&gt;rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer                     &amp;&amp;
                 !isa.weakly_referenced             &amp;&amp;
                 !isa.has_assoc                     &amp;&amp;
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &amp;&amp;
#else
                 !isa.getClass(false)-&gt;hasCxxDtor() &amp;&amp;
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj-&gt;hasCxxDtor();
        bool assoc = obj-&gt;hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj-&gt;clearDeallocating();
    }

    return obj;
}


/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

delloac -> _objc_rootDealloc -> rootDealloc (首先判断是否有关联对象等,如果没有则直接释放)如果没有 -> object_dispose -> objc_destructInstance -> _object_remove_assocations(释放关联对象)

免责申明:本站发布的内容(图片、视频和文字)以转载和分享为主,文章观点不代表本站立场,如涉及侵权请联系站长邮箱:xbc-online@qq.com进行反馈,一经查实,将立刻删除涉嫌侵权内容。