面试题专栏

iOS - Category本质探究

2020-07-08  本文已影响0人  紫金飞侠雷

iOS - Category

1、Category简介

Category是Objective-C 2.0之后添加的语言特性。

Category可以做什么:
  1. 给目标类添加方法。
  2. 将一个类的实现拆分成多个独立的源文件文件。
  3. 声明私有的方法; (模拟多继承,framework私有方法公开)
Category的优点:

​ 可以减少单个文件的体积;
​ 可以把不同功能的组织到不同的category中;
​ 可以按需求加载想要的category;

Category的缺点:

​ Category 非常强大,所以一旦误用就很可能会造成非常严重的后果。

​ 覆写系统类的方法,不管在任何情况下,切记一定不要这么做。

2、Category与Extension

  1. Extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。Extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加Extension,所以你无法为系统的类比如NSString添加Extension。

  2. Category则完全不一样,它是在运行期决议的。

  3. 就Category和Extension的区别来看,我们可以推导出一个明显的事实,Extension可以添加实例变量,而Category是无法添加实例变量的。

3、Category本质

在runtime层,category的结构体category_t(在objc-runtime-new.h中可以找到此定义):(源码网址

struct category_t {

  struct property_list_t *instanceProperties; 
    const char *name; // category的名字
    classref_t cls;
    struct method_list_t *instanceMethods; // category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods; // category中所有添加的类方法的列表
    struct protocol_list_t *protocols; // category实现的所有协议的列表
    struct property_list_t *instanceProperties; // category中添加的所有属性
    // 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;
    }

};

Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。

那我们都知道一个类对象可以写多个分类,那这些分类是如何加载的呢?

1、Objective-C的运行是依赖OC的runtime的,而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的。

对于OC运行时,入口方法如下(在objc-os.mm文件中):

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_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

在这个运行时的初始化方法中我们可以看到&map_images,这是一个函数的地址。我们进去看就会发现其实调用的是如下代码:

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

map_images 方法中其实是调用的map_images_nolock方法,而map_images_nolock这个方法中又会调用一个_read_images的方法,

_read_images会读取一些模块,比如一些类信息、分类信息这些东西。

// 这里加载分类,调用方法(跟之前的源码是不一样,这是写成了一个方法。)
// Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

在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());
                    }
                }
            }
        }
    };

    processCatlist(_getObjc2CategoryList(hi, &count));
    processCatlist(_getObjc2CategoryList2(hi, &count));
}

我们重点关注 attachCategories方法,看一下这个方法的实现:

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     只有少数类在启动期间有超过64个类别。这使用了一个小堆栈,并避免了malloc。
     *
     *类别必须按正确的顺序添加,即从后到前。为了完成分块操作,我们从前向后迭代cats_list,向后构建本地缓冲区,并在块上调用attachLists。attachLists将列表放在前面,因此最终结果将按照预期的顺序进行。
     
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    // 方法数组
    method_list_t   *mlists[ATTACH_BUFSIZ];
    // 属性数组
    property_list_t *proplists[ATTACH_BUFSIZ];
    // 协议
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();
    /*
     
    这里之前是 while 循环,倒序的,现在是一个正序的
    while (i--) {
        
    }
    */
    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);
                // 将所有分类对象的方法,附加到类对象方法列表中。
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            // 本次是从后往前添加 64 - 1
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

attachCategory做的工作相对比较简单,把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面,然后转交给了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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            // array()->lists 原来的方法列表,将array()->lists向后移动addedCount
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            // addedLists 所有分类的方法列表,将addedLists 拷贝到array()->lists的头位置
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));

attachLists:将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

所以我们可以得出结论:

1.category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA;

2.category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。

4、Category + load + initialize

+load 方法

+load方法会在runtime加载类、分类时调用。每个类、分类的+load,在程序运行过程中只调用一次。

load方法的调用顺序:

1.先调用类的+load
按照编译先后顺序调用(先编译,先调用)
调用子类的+load之前会先调用父类的+load
2.再调用分类的+load
按照编译先后顺序调用(先编译,先调用)

大家思考一下,上面我们看了category 本质的时候发现,如果分类中方法名和类中方法名一样,不是只执行分类的方法吗?为什么load 方法不是这样?

同样看源码我们也能找到答案。这里就不看这个源码了。给大家列出查看源码的顺序:

objc-os.mm

1、初始化方法_objc_init,查看load_images

2、call_load_methods方法

do {
        // 这里也可以看出先调用类的load 方法,再调用分类的load 方法
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

3、call_class_loads、call_category_loads

  // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // 直接找到load 方法的地址
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        // 直接调用load方法
        (*load_method)(cls, @selector(load));
    }
    

通过查看源码我们可以得出:

+load方法是根据函数地址直接调用,并不是经过objc_msgSend函数调用(消息机制调用,isa 找到类,然后找到方法列表去遍历)

+initialize 方法

+initialize方法会在类第一次接收到消息时调用。
调用顺序
先调用父类的+initialize,再调用子类的+initialize
(先初始化父类,再初始化子类,每个类只会初始化1次)

上源码调用顺序:

1、objc-runtime-new.mm中class_getInstanceMethod,寻找方法。

2、找到 lookUpImpOrForward方法,initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass

判断是否初始化过,保证只有第一次执行。

3、initializeNonMetaClass方法,callInitialize,然后objc_msgSend。

* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first
*父类优先初始化。
这也是一个递归实现。

对比:

+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:
1、如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)

​ 子类的isa 指针找到元类对象没有+initialize方法,然后superClass 中去找。

2、如果分类实现了+initialize,就覆盖类本身的+initialize调用

+Load +initialize
调用时机 Runtime加载类、分类的时候调用 收到第一条消息时,可能永远不调用
调用方式 根据函数地址直接调用 通过objc_msgSend调用
调用顺序 父类->子类->分类 父类->子类
是否需要显式调用父类实现 1次 可能会调用多次
分类中的实现 类和分类都执行 覆盖类中的方法,只执行分类的实现

5、Category 关联对象

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现。

// 项目写法
- (BYLoadingView *)loadingView {
    BYLoadingView *loadingView = objc_getAssociatedObject(self, &kLoadingView);
    return loadingView;
}

- (void)setLoadingView:(BYLoadingView *)loadingView {
    objc_setAssociatedObject(self, &kLoadingView, loadingView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

关联对象提供了以下API
添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

获得关联对象
id objc_getAssociatedObject(id object, const void * key)

移除所有的关联对象
void objc_removeAssociatedObjects(id object)

我们思考一下:

但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?

在objc-references.mm文件中有个方法_object_set_associative_reference:

我们可以看到所有的关联对象都由AssociationsManager管理,而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中有一个AssociationsHashMap:

//AssociationsHashMap 中有一个 ObjectAssociationMap
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

//ObjectAssociationMap 中 ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;

// ObjcAssociation的定义如下
uintptr_t _policy;
id _value;

所以我们也可以得到:

关联对象并不是存储在被关联对象本身内存中。

关联对象存储在全局的统一的一个AssociationsManager中。

设置关联对象为nil,就相当于是移除关联对象。

 if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* 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);

                    }
                }
            }
        }
如何销毁某一个:

objc-runtime-new.mm中objc_destructInstance。

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

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
上一篇下一篇

猜你喜欢

热点阅读