OC分类

作者: KG丿夏沫 | 来源:发表于2021-03-29 11:56 被阅读0次

OC分类

一、分类的作用

对现有已存在的类添加方法,但是不想在现有类中进行修改,如果团队开发,直接在现有类中进行修改,会导致别人的代码出现bug或者其他意外,所以有了分类。

疑问:

1、分类中能否添加成员变量?

首先分类中没有isa指针,而且在objc源码中查看分类的结构体时会发现,并没有存储成员变量的列表。所以没有办法给分类添加成员变量

2、分类中能否添加属性?

分类中可以添加属性,系统会给我们声明setter以及getter方法,但是在setter和getter方法里面并没有进行赋值以及取值操作,所以正常理论上来说是不能添加属性的。但是结合runtime就可以实现给分类添加属性,runtime有两个API可以辅助我们实现id objc_getAssociatedObject(id object, const void *key)实现取值,void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)赋值。如下示例:

#import "LGPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson (KGPerson)
@property (nonatomic,copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "LGPerson+KGPerson.h"
#import <objc/runtime.h>
static NSString *nameKey = @"NAME_KEY";
@implementation LGPerson (KGPerson)
- (NSString *)name{
   return objc_getAssociatedObject(self, &nameKey);
}
- (void)setName:(NSString *)name{
   objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

二、分类的结构体

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;
    }
};

从以上分类的结构体可以看出,我们可以在分类中添加对象方法、类方法、协议以及属性,但是需要注意的就是系统只会生成setter和getter方法的声明,需要我们自己去实现setter和getter方法。

针对属性实现setter方法赋值和getter方法取值,我们考虑有哪些方法处理?如果有怎么实现,方案的缺点?

有如下几种方案:

(1)、声明一个全局的变量。缺点:如果有多个有多个对象,就会共用一个全局变量,其中一个对象修改,所有对象的值都会改变。

(2)、声明一个全局的字典存储。优点:避免了对象之间赋值相互影响问题。缺点:线程不安全

(3)、使用runtime的api。优点:避免了对象之间赋值相互影响问题。

疑问:在分类中添加的方法以及属性那些,为什么我们在类对象中可以访问到?我们并没有去创建分类对象,怎么去访问到的?下面带着疑问开始重点Category分类的加载过程探索,

三、分类的加载过程

由于整个合并加载过程由runtime去完成,所以我们从runtime的初始化方法开始进行分析,以下是runtime的初始化方法。

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();
    //调用C++静态构造函数
    static_init();
    //初始化runtime
    runtime_init();
    //初始化libobjc的异常处理系统。
    exception_init();
    //初始化缓存
    cache_t::init();
    _imp_implementationWithBlock_init();
    //注册函数通知(镜像的映射,加载,解除映射),在这里主要看map_images的实现
         /* _dyld_objc_notify_register*
         * map_images:方法回调的地址作为参数,主要是读取处理给定的镜像文件
         * load_images:这个方法就是将经过处理的镜像文件进行加载
         * unmap_image:处理未映射的镜像
         */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

从初始化方法中我们看到我们需要的Category的加载过程是在void map_images(unsigned count, const char * const paths[],const struct mach_header * const mhdrs[])中实现的,所以重点看下这个方法的实现。处理dyld映射到的给定图像

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_nolock方法,我们继续往下查找。

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    ...
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    ...
}

在这个方法里面主要做了四件事情:

1、拿到dyld传过来的header,进行封装

2、初始化selector

3、初始化autorelease pool page

4、读取images

经过一系列的处理,最后开始去读取镜像_read_images,这个方法内容比较多,先将判断以及循环等条件关系进行折叠,然后我们会发现系统给出了很清晰的注释,按照注释我们逐步去寻找解读我们需要的内容,大概内容如下:

20210226001.png

1、在这个方法中第一个条件判断是否是首次加载,在这个判断中主要做了以下几件事情:

(1)、如果镜像文件中包含swift代码,并且该swift框架早于版本swift3.0,那么禁用isa指针。

(2)、如果是Mac OS X系统环境,第一:系统版本是在10.11版本以前的旧版本,那么禁用isa指针,但是这块的代码被注释了,具体原因不太清楚,可能是苹果对以前的旧版本进行了某些兼容,或者不在支持以前的旧版本。第二:如果镜像文件中包含__DATA__objc_rawisa字段,那么禁用ISA指针

(3)、主要就是这块,在这里将发现的类添加到类的gdb_objc_realized_classes 表中。

2、修复选择器引用

3、发现类。修复未解决的未来类

4、重新映射类

5、修复旧的objc_msgSend_fixup调用站点

6、发现协议。修复协议引用

7、修复@protocol引用

8、发现类别,在这块代码中有一些注释,如下:

(1)、只有在完成了初始的类别附件后才能这样做。对于在启动时出现的类别,发现延迟到对_dyld_objc_notify_register的调用完成后的第一次load_images调用

(2)、类别发现必须延迟,以避免潜在的竞争,当其他线程调用新的类别代码之前,这个线程完成它的修复

(3)、在prepare_load_methods方法中处理所有类别的加载

9、实现非懒惰类

10、实现未来类

到这里map_images对镜像文件的处理意见完成,那么接下来看下,Category内的属性、方法、协议等是怎么添加到类中的,我们跟随系统断点一直跟到load_images函数中,前面map_images这个函数只会走一次,而load_images函数会走很多次。

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    /*
     * didInitialAttachCategories启动时出现的类别初始附件是否有
     * didCallDyldNotifyRegister对_dyld_objc_notify_register的调用是否已经完成
     * 只要进入时didInitialAttachCategories=false didCallDyldNotifyRegister=false,这个方法就不会走
     */
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    // 这里判断镜像有没有+load方法,如果没有直接返回。
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    // 发现加载方法
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    // 调用+加载方法(没有runtimlock - re-entrant)
    call_load_methods();
}

通过加断点验证发现,load_images这个方法确实走了很多次,同时验证了第一个判断,当启动时出现的类别初始附件没有并且_dyld_objc_notify_register函数调用未完成的时候,是不会走加载所有分类的函数。所以按照流程先跳过这个函数,去看下void prepare_load_methods(const headerType *mhdr)这个函数的实现,函数如下:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    // 操作前先进行加锁保护
    runtimeLock.assertLocked();

    // 获取所有类的方法加入到数组中
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    // 获取所有分类的方法加入到数组中
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class 被忽略的弱链接类的类别
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

既然找到了分类加载到数组中的方法,那么继续查看是如何进行处理的,继续查看add_category_to_loadable_list函数的实现,如下:

/***********************************************************************
* add_category_to_loadable_list
* 添加类别到可加载列表
* Category cat's parent class exists and the category has been attached to its class. Schedule this category for +load after its parent class  becomes connected and has its own +load method called.
* 类别cat的父类存在,并且类别已经被附加到它的类上。在其父类连接并调用自己的+load方法后,为+load调度这个类别。
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();
    // 获取分类的load方法
    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    // 如果分类中没有load方法,那么直接返回
    if (!method) return;

    /**
     * PrintLoading:记录对class和category +load方法的调用
     */
    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    /**
     * loadable_categories_allocated:需要+load调用的类别列表(挂起的父类+load)
     */
    if (loadable_categories_used == loadable_categories_allocated) {
        // 需要扩展的空间大小计算
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        // 开辟内存空间
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    // 绑定分类和分类的load方法关系,保存到数中
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

在这个函数中主要进行了一下几个操作:

(1)、从class_ro_t中获取load方法

(2)、如果内存已满,进行扩容,申请现有内存的2倍+16字节大小的空间

(3)、保存分类的分类和方法到数组中

然后调用call_load_methods方法加载类以及分类的load方法到数组中,函数实现如下:

***********************************************************************
 * call_load_methods
 *调用所有挂起的类和类别+加载方法。
 *类+加载方法被称为超类优先。
 在父类的+load之后才会调用Category +load方法。

 *这个方法必须是可重入的,因为+load会触发更多的图像映射。此外,在面对重入调用时,必须保持超类优先顺序。因此,只有该函数的最外层调用才会执行任何操作,而该调用将处理所有可加载类,甚至是在它运行时生成的类。

 *下面的序列在+load加载图像时保持+load顺序,并确保no +load方法被遗忘,因为它是在+load调用时添加的。
 *序列:
 * 1。反复调用class +load,直到没有其他的类为止
 * 2。调用category +load一次。
 * 3。如果:
 * (a)有更多的类需要加载,或
 * (b)有一些潜在的类别+负载,仍然没有尝试。
 * Category +load只运行一次,以确保“父类优先”的排序,即使一个Category +load触发了一个新的可加载类和一个新的可加载类别附加到这个类。

 * Locking: loadMethodLock必须被调用者持有
 *所有其他锁都不能被持有。
**********************************************************************/
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // 重入调用不做任何事情;最外层的调用将完成任务。
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. 反复调用class +load,直到没有其他的类为止
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 调用category +load一次
        more_categories = call_category_loads();

        // 3. 如果有类或更多未尝试的类别,则运行更多+load
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

在这个函数中,主要进行了以下操作:

(1)、创建一个自动释放池

(2)、循环调用class的load方法,直到没有其他的类为止

(3)、调用Category的load方法,这个只调用一次,因为要保证父类优先的排序

(4)、如果有类或更多未尝试的类别,则运行更多+load

(5)、从函数实现可以看到,为什么分类load方法加载要比宿主类的load方法要晚一些的原因,是因为先执行while循环,加载宿主类的load方法,然后再去加载分类的laod方法

(6)、销毁自动释放池

当所有类以及分类的load方法加载完成后,开始执行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) {
                // 类别的目标类不见了(可能是弱链接)。
                // 忽略的类别。
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }

            // 这个类别的过程。
            if (cls->isStubClass()) {
                // 存根类永远不会实现。存根类在初始化之前不知道它们的元类,所以我们必须向存根本身添加带有类方法或属性的类别。methodizeclass()会找到它们并将它们适当添加到元类中。
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    // 从分类添加方法到宿主类
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // 首先,将类别注册到它的目标类。然后,如果实现了类,重新构建类的方法列表(等等)。
                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(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

在这个函数中主要进行以下操作:

1、遍历获取分类以及宿主类
2、在这个方法中首先去判断类的宿主类是否实现,或许会问为什么会有类是否实现这一说呢?因为无论是类还是分类都是有懒加载和非懒加载模式两种,在这里就是判断宿主类是否是懒加载模式,有以下几种情况以及处理方式:

(1)、类懒加载+分类懒加载
当宿主类与分类都没有实现load方法时,那么这个类都不会加载,在消息第一次调用时进行数据加载

(2)、类懒加载+分类非懒加载
当宿主类没有实现load方法而分类实现load方法时,分类会迫使宿主类从懒加载变成非懒加载模式来提前加载数据

(3)、类非懒加载+分类懒加载
当宿主类实现了load方法而分类没有实现load方法是,当前分类会变成非懒加载模式加载数据

(4)、类非懒加载+分类非懒加载
宿主类与分类都在运行时加载,即提前加载

3、从判断可以看出如果已经实现类,直接将分类的方法等添加到宿主类中,如果没有实现,先将分类注册到宿主类,然后进行方法添加到宿主类

到这里我们差不多分类的加载过程就探索完毕了,文章是自己探索过程的笔记,如有失误请及时指出!!!

相关文章

  • 分类、类扩展与继承

    在OC中,扩展一个类的方式有两种,继承和分类。 分类(Category) 概念 分类(Category),是OC中...

  • OC分类

    1.分类的作用? 声明私有方法,分解体积大的类文件,把framework的私有方法公开 2.分类的特点 运行时决议...

  • OC 分类

    1.分类的作用 声明私有方法, 分解体积大的类文件, 把framework的私有化方法公开 2.分类的特点 运行时...

  • OC分类

    OC分类 一、分类的作用 对现有已存在的类添加方法,但是不想在现有类中进行修改,如果团队开发,直接在现有类中进行修...

  • OC分类

    Category底层结构 Category加载过程 1.通过Runtime加载某个类的所有Category数据 2...

  • iOS学习相关内容

    OC基础_分类: 分类的定义:分类是OC中的特有语法,它是表示一个指向分类结构体的指针,它是为了扩展系统类的方法而...

  • UIButton倒计时

    oc 用法 NSObject分类 swift 用法

  • OC中分类,拓展 和 swift中拓展

    OC中分类 分类创建 分类格式: UIColor+ColorChange.h头文件 UIColor+ColorCh...

  • iOS面试之OC模块

    OC oc内容如下:1.分类2.关联对象3.扩展4.代理5.通知6.KVO7.KVC8.属性关键字 1.分类 分类...

  • 分类(Category)与类拓展(Extension)

    分类(Category) 1.分类(Category)是什么? 分类是oc特有的语法,表示指向分类的结构体指针。分...

网友评论

      本文标题:OC分类

      本文链接:https://www.haomeiwen.com/subject/lmfbhltx.html