美文网首页面试技术
iOS底层-Categroy、关联对象

iOS底层-Categroy、关联对象

作者: Engandend | 来源:发表于2019-12-23 17:57 被阅读0次

Categroy : 分类,也叫类别(请注意,和拓展是2个东西)

  • Category 的作用

  • 苹果推荐:
    1、为已存在的类添加方法、属性
    2、可以把类的实现分开在几个不同的文件里面。好处:
        a)可以减少单个文件的体积
        b)可以把不同的功能组织到不同的Category里
        c)可以由多个开发者共同完成一个类
        d)可以按需加载想要的Category等等。
    3、声明私有方法

  • 脑洞大的开发者
    4、 模拟多继承
    5、 把Framework的私有方法公开

1、Category的结构

1.1 正确的结构

在源码中能找到这样的信息: 在objc-runtime-new.h

struct category_t {
    const char *name;      // 分类名
    classref_t cls;        // 分类关联的类
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;

    // Fields below this point are not always present on disk.
.........
};

在分类的结构体中,能看到有类方法、实例方法、协议、属性,说明结构体是可以添加属性的

1.2 错误的结构

注意和另一个结构体区分 。后面的备注已经写了 objc2 不可用,也就是废弃

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}

从另外一个角度来看,在源码void _objc_init(void)中,加载分类的时候有一段代码是这样的:

for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
        }
.......
}

1.3 结构中的name、cls

定义一个 JEPerson+JECate.h的分类
通过clang命令转换成底层

clang -rewrite-objc JEPerson+JECate.m

在原有文件夹中找到 JEPerson+JECate.cpp 并打开,在靠近底部能看到这样的一个结构

static struct _category_t _OBJC_$_CATEGORY_JEPerson_$_JECate __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "JEPerson",              ///< name 属性
    0, // &OBJC_CLASS_$_JEPerson,
    0,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_JEPerson_$_JECate,
    0,
    0,
};

从这里能看到,编译阶段 name 是 JEPerson

_read_images中 有一个加载分类的流程

objc4-779版本为例

// Discover categories.
    for (EACH_HEADER) {
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        auto processCatlist = [&](category_t * const *catlist) {
            for (i = 0; i < count; i++) {
                category_t *cat = catlist[i];
                
                Class cls = remapClass(cat->cls);

// 添加这一行代码  来打印分类
                printf("categories classes: %s %s\n",cat->name,cls->mangledName());

                const char *cname = cat->name;
                const char *oname = "JECate";
                if (strcmp(cname, oname) == 0) {
                    printf("来了 老弟\n");
                }

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

    ts.log("IMAGE TIMES: discover categories");


打印结果:
.......
categories classes: XCTErrors NSError
categories classes: JECate JEPerson

对于分类中的name,我的理解为
编译时:是JEPerson
运行时:是JECate

⚠️
objc4-750版本 运行结果不一样:
cat->name 打印结果是 JEPerson
mac 10.15之后 objc4-750没有运行起来,所以cls没看到打印结果

2、分类的加载流程

非懒加载类有简单说到在启动时,非懒加载类的加载流程(懒加载类在启动的时候并没有被加载),那么对于分类的加载流程时怎样的?
因为分类是与主类相关联,所以需要分多种情况来看

2.1 、主类懒加载

2.1.1、 分类懒加载

主类和分类都是懒加载的情况下,在编译期自行处理自己的相关信息,然后在类第一次发送消息(比如初始化一个实例对象)的时候,分类进行关联

2个重要的方法

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {

    const char *cname = cls->mangledName();
    const char *oname = "JEPerson";
    if (strcmp(cname, oname) == 0) {
        printf("来了 老弟\n");   //正确的拿到我们需要研究的类   注意:进来的cls 是元类
    }
.....
    if (slowpath(!cls->isRealized())) {    // 因为都是懒加载类,所以第一次发送消息时,类并没有实现
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);   //  元类的实现
    }
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);   //在这里 进行元类和类的绑定,并实现类
    }
}

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
.....
    // Attach categories
    methodizeClass(cls, previously);    // 将分类的相关信息与主类相关联

    return cls;
}

简单整理一下流程(初始化流程)
lookUpImpOrForward() -> realizeClassMaybeSwiftAndLeaveLocked()
-》
realizeClassWithoutSwift()
-》
lookUpImpOrForward() -> initializeAndLeaveLocked()
-》
getMaybeUnrealizedNonMetaClass(cls, inst) // 元类与类进行关联
-> realizeClassMaybeSwiftAndUnlock(); //实现类
-》
realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift()
-》
methodizeClass()分类的相关信息在这里面处理

2.1.2 、 分类非懒加载

read_image()中,因为主类是懒加载类,所以在这里主类是不加载的,但是分类会被加载(代码里的// Discover categories部分),

if (cls->isRealized()) {
    attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
    objc::unattachedCategories.addForClass(lc, cls);
}

因为主类是懒加载类,最后走的是 unattachedCategories方法,也就是将分类先存储到哈希表里。
addForClass() 中的 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.

那具体主类是什么时候被实现的呢? load_image的时候 (所有版本类都是这个流程)

void _objc_init(void)
{
  .....
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
// 就是这里面的  map_image 完毕之后 执行的 load_images 里面
}
load_images 
-》
prepare_load_methods

load_images(const char *path __unused, const struct mach_header *mh)
{
  .....
        prepare_load_methods((const headerType *)mh);
    call_load_methods();
}

void prepare_load_methods(const headerType *mhdr)
{
   ....
        if (!cls) continue; 
        realizeClassWithoutSwift(cls, nil);  // 这里进行主类的实现 并与分类进行关联,
}

主类懒加载+分类分懒加载的情况, read_images里面 主类并不处实现,将分类的信息先加入哈希表里
read_images完毕之后,进入 load_images时,进行了 prepare_load_methods的操作,在这里,如果主类没有实现,就去实现主类,然后对分类进行attach操作

2.2 主类非懒加载

2.2.1、分类懒加载

主类非懒加载,所以在read_images里面会 调用realizeClassWithoutSwift 对主类进行实现. 我们在这个方法里面去断点打印

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    ro = (const class_ro_t *)cls->data();    // 从cls中拿到data信息,进过处理后 赋值给 rw的ro
    if (ro->flags & RO_FUTURE) {
        ....
    } else {
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }
 ⬅️ //在这个位置断点
....
    // Attach categories
    methodizeClass(cls, previously);

    return cls;
}

通过lldb打印ro信息

(lldb) p *ro
(const class_ro_t) $15 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100000ed1 "JEPerson"
  baseMethodList = 0x0000000100002028
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $15.baseMethodList
(method_list_t *const) $16 = 0x0000000100002028
(lldb) p *$16
(method_list_t) $17 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2          // 有2个方法
    first = {            // 第一个方法信息
      name = "personCateMethod"    // 这是在分类里定义实例方法
      types = 0x0000000100000f2f "v16@0:8"
      imp = 0x0000000100000d20 (KCObjcTest`-[JEPerson(JECate) personCateMethod] at JEPerson+JECate.m:17)
    }
  }
}
(lldb) p $17.getOrEnd(1)        //  获取第二个方法信息  下标为1
(method_t) $18 = {
  name = "method1"              // 这是在主类里定义的实例方法
  types = 0x0000000100000f2f "v16@0:8"
  imp = 0x0000000100000e50 (KCObjcTest`-[JEPerson method1])
}

从以上打印信息中可以看出,分类的信息 在编译时就已经添加到了data信息里面

2.2.2、分类非懒加载

read_images里面 先加载分类,主类还没有实现,所以通过objc::unattachedCategories.addForClass(lc, cls);存在哈希表里

再加载主类,对主类进行实现realizeClassWithoutSwift(),在这用2.2.1打印ro的步骤,打印出ro的相关信息

(lldb) p *$1
(method_list_t) $2 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 1        //方法只有一个  
    first = {
      name = "method1"
      types = 0x0000000100000f1f "v16@0:8"
      imp = 0x0000000100000e40 (KCObjcTest`-[JEPerson method1])
    }
  }
}

从打印结果看,方法只有一个,是在主类里定义的方法,分类里的方法并没有。其通过methodizeClass() -> objc::unattachedCategories.attachToClass() -> attachCategories进行信息绑定

3、 分类属性

3.1、 为分类添加属性

⚠️:属性和实例对象不是同一个东西

正常情况,我们为类添加属性的写法:

@property (nonatomic, assign) NSInteger age;

由于系统会自动生成get、set方法,我们可以直接通过get、set方法进行访问,但是对于分类(类别),如果直接用get、set方法访问,比如:

//Student 的分类
@interface Student (JE)
@property (nonatomic, assign) NSInteger age;
@end

//Student中调用
- (instancetype)init
{
    if (self = [super init]) {
        self.age = 10;
    }
    return self;
}

//就会出现这个错误:
[Student setAge:]: unrecognized selector sent to instance 0x600003ea8250

经典的错误: unrecognized selector 找不到方法,这是因为系统并没有为我们生成其get、set方法,如何正确的添加属性?----通过关联对象的方式

// static const char je_age_cate;

@property (nonatomic, assign) NSInteger age;

- (void)setAge:(NSInteger)age
{
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 // 因为写入的值是@(age) NSNumber类型,所以需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC缓存策略

//  objc_setAssociatedObject(self, &je_age_cate, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSInteger)age
{
    return [objc_getAssociatedObject(self, @selector(age)) integerValue];
//  return [objc_getAssociatedObject(self, &je_age_cate) integerValue];
}

3.2、 分类属性结构

  • 分类的属性存储在哪?

我们知道,在类加载的时候,系统就会为类对象分配其所需的大小,后续并不能添加,那么用分类的方式为类添加属性之后,这个属性在哪里?
答案是在:AssociationHashMap 。 其由AssociationsManager管理。所有的分类属性都在这里。

//以:objc_setAssociatedObject()为入口

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
   .....
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        .....
    }
....
}
  
//查看AssociationsManager结构
class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
......
};
- (void)setAge:(NSInteger)age
{
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

以这个结构为例,可以解析为:

借助别人的一张图: 结构

AssociationPolicy和value封装成ObjcAssociation的结构,然后和key建立的映射关系构成ObjcAssociationMap,再对应由object的地址通过DISGUISE函数返回值生成的键存储在AssociationHashMap中

分类存储结构

3.3、分类属性释放

正常的类的属性是在dealloc中,向其发送release信号,当引用值为0的时候,其被置空、释放,那么对于关联对象的释放逻辑是怎么样的?还是从dealloc的源码中来看:

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

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);    //  <--- 这里是对关联对象进行释放
    }
}


void *objc_destructInstance(id obj) 
{
    if (obj) {
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);    // <----- 如果是关联对象,进行remove操作
        obj->clearDeallocating();
    }

    return obj;
}

简单来说:在dealloc的时候,先进行release操作,如果有关联对象,进行_object_remove_assocations操作

3.4、分类的方法

分类的方法存储在哪? ------------在类对象/元类对象中。

从源码中来看:

_objc_init () -> map_images -> map_images_nolock() -> _read_images()
在 _read_images() 里有一段

for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();
....
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());     //<  再进这个方法
                }
}

remethodizeClass() -> attachCategories()

//将cls和category连接起来
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
....
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
....
}


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;
            // 将原来的元素移动到扩容后数组的尾部
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            // 把addedLists中的成员放在array的前面
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }  
        ....
    }

由此可见:
1、分类的方法会整合到类对象的方法列表中。
2、如果categorty和类本身有相同的方法,那么在类对象的方法列表中 有2个相同的方法名,而不是把原本的方法替换掉。
3、新方法会加在旧方法的前面,这样在调用的时候,从前往后找,找到第一个,就不再找了


用另外一种方法来验证:------ 直接打印方法列表的方法名

//Student.h
- (void)categoryMethod
{
    NSLog(@"student categoryMethod %p",__FUNCTION__);
}

// Student+JE.h
- (void)categoryMethod
{
    NSLog(@"categoryMethod %p",__FUNCTION__);
}


//ViewController.m
Student *s = [Student new];
[self methodInfo:s];

- (void)methodInfo:(id)obj
{
    Class class = [obj class];
    
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(class, &methodCount);
    
    for (NSInteger i = 0; i < methodCount; i ++) {
        Method method = methodList[i];
        SEL methodName = method_getName(method);
        NSLog(@"方法名:%@", NSStringFromSelector(methodName));
    }
}

// 打印结果
方法名:categoryMethod
方法名:categoryMethod

从打印结果看,同一个方法名,打印了2次,说明分类的方法并没有覆盖类对象本身的方法

用同样的方法,可以验证,分类的方法是在类对象方法的前面的(方法列表中,分类的方法下标小)。

关联对象key的4种写法

分类关联对象key的四种写法

4、分类(category)和拓展类(extension)

拓展类也叫匿名分类

4.1、拓展的形式

这2个东西其实很好区分,之所以拿出来说一下,是因为我在深入学习分类之前,一直对这2个名字搞混。

  • 分类:
    其形式为 Class+Category.hClass+Category.m2个文件组成
//比如Student的分类    Student+JE
//.h文件
@interface Student (JE)
@end

//.m文件

@implementation Student (JE)
@end
  • 拓展类
    其形式为 Class+ Extension.h,只有 1个文件
// 比如Student的拓展类  Student+Extent
@interface Student ()
@end

但是在一般开发中,单独定义给一个拓展类是比较少的,而是再接在类中写

//比如Student类 其继承Person
#import "Person.h"

@interface Student ()      //这2行就是为Student添加了拓展
@end

@interface Student : Person

@end

这种写法就很熟悉了,在开发中经常会这样写

4.2、 拓展的加载

拓展类的信息作用在编译时、在编译时信息就被编译到了data的ro里面。

这也侧面说明了 拓展类的变量存储在类的结构里,编译之后,ro数据不能再操作(只读属性),所以分类无法添加成员变量,之前也说过另外一个原因,就是类编译之后,系统在 alloc的时候,就为类分类了固定的大小,无法在动态去改变。


4.3、 分类与拓展类的区别

  • 分类和扩展有什么区别?
    1、分类多用于扩展方法实现,类扩展多用于申明私有变量和方法。
    2、类扩展作用在编译期,直接和原类在一起,而分类作用在运行时,加载类的时候动态添加到原类中。
    3、类扩展可以定义属性,系统自动生成get、set、实例变量。分类中定义的属性需要手动添加get、set进行关联对象。

  • 分类有哪些局限性?
    1.分类只能给现有的类加方法或协议,不能添加实例变量(ivar)。
    2.分类添加的方法如果与现有的重名,会覆盖原有方法的实现(⚠️假覆盖,事实上2个方法都在,并没有消失)。如果多个分类方法都重名,则根据编译顺序执行最后一个。

相关文章

  • iOS底层-Categroy、关联对象

    Categroy : 分类,也叫类别(请注意,和拓展是2个东西) Category 的作用 苹果推荐:1、为已存在...

  • iOS底层原理总结 - 关联对象实现原理

    iOS底层原理总结 - 关联对象实现原理 iOS底层原理总结 - 关联对象实现原理

  • 探寻block的本质

    转自:探寻block的本质拓展:探寻OC对象的本质iOS底层原理总结 - 关联对象实现原理iOS底层原理总结 - ...

  • iOS底层原理 - 关联对象

    面试题引发的思考: Q: Category能否添加成员变量?如果可以,如何给Category添加成员变量? 不能直...

  • iOS-底层-关联对象

    前两篇文章我们学习了关于Category的知识Category分类和load和initialize,现在再看一个问...

  • iOS底层之关联对象

    首先我们来简单的描述一下分类的一些基本概念:1、用来给类添加新方法2、不能给类添加成员属性,添加了成员变量,也无法...

  • iOS底层-关联对象探索

    关联对象探索 其底层原理的实现,主要分为两部分: 通过objc_setAssociatedObject设值流程 通...

  • iOS底层之关联对象

    首先我们来回忆一个经典的面试题 Category能否添加成员变量?如果可以,如何给Category添加成员变量? ...

  • iOS 关联对象 底层原理

    主要分为两部分: objc_setAssociatedObject设值流程 objc_getAssociatedO...

  • iOS关联对象技术原理

    iOS关联对象技术原理 iOS关联对象技术原理

网友评论

    本文标题:iOS底层-Categroy、关联对象

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