笔记 - Category

作者: 强子ly | 来源:发表于2019-07-21 20:43 被阅读12次

1、Category的基本使用

Category的使用场合是什么?

- 将一个类拆成很多模块(其实就是解耦,将相关的功能放到一起)

2、Category的实现原理?

- 通过runtime动态将分类的方法合并到类对象、元类对象中
- Category编译之后的底层结构是 struct_category_t , 里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将 Category 的数据,合并到类信息中(类对象、元类对象)
- 转C++代码
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m

- 分类结构体
struct _category_t {
    const char *name;      // 类名
    struct _class_t *cls;
    const struct _method_list_t *instance_methods; // 实例方法列表
    const struct _method_list_t *class_methods;    // 类方法列表
    const struct _protocol_list_t *protocols;      // 协议列表 (分类里面也能遵守协议)
    const struct _prop_list_t *properties;         // 属性列表 (分类里能实现属性)
};


- Test分类
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MJPerson",
    0, // &OBJC_CLASS_$_MJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
    0,
    0,
    0,
};


- Eat分类
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MJPerson",
    0, // &OBJC_CLASS_$_MJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
    0,
    0,
    0,
};

3、源码分析1

objc4-723.tar.gz

源码定义

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

Category的加载处理过程

- 通过Runtime加载某个类的所有Category数据
- 把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
- 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
源码解读顺序

objc-os.mm
- _objc_init
- map_images
- map_images_nolock

objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、memcpy
cls = [MJPerson class]
cats = [category_t(Test), category_t(Eat)]

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    /*
     方法数组
     [
       [method_t, method_t],
       [method_t, method_t]
     ]
     */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    
    /*
     属性数组
     [
       [property_t, property_t],
       [property_t, property_t]
     ]
     */
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    
    /*
     协议数组
     [
       [protocol_t, protocol_t],
       [protocol_t, protocol_t]
     ]
     */
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

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

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    // 得到类对象里面的数据
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 将所有分类的对象方法,附加到类对象方法列表中
    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);
}

5、memmove、memcopy的区别

void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count); 

- 他们的作用是一样的
- 唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确

参考链接


6、load基本使用

Category中有load方法么?load方法什么时候调用的?load方法能继承么?

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

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

7、load调用原理

-load方法调用

test方法和load方法的本质区别?(+load方法为什么不会被覆盖)
- test方法是通过消息机制调用 objc_msgSend([MJPerson class], @selector(test))
- + load方法调用,直接找到内存中的地址,进行方法调用

8、load调用顺序

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

调用顺序
- 1、先调用类的+load
     按照编译先后顺序调用(先编译,先调用)
     调用子类的+load之前会先调用父类的+load

- 2、再调用分类的+load
     按照编译的顺序调用(先编译,先调用)
objc4源码解读过程:objc-os.mm

- _objc_init

- load_images

- prepare_load_methods
  schedule_class_methods
  add_class_to_loadable_list
  add_category_to_loadable_list

- call_load_methods
  call_class_loads
  call_category_loads
  (*load_method)(cls, SEL_load)

+ load方法时根据方法地址直接调用,并不是经过objc_msgSend函数调用

  • 先调用类方法,后调用分类方法
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 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);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

如果有100个类,会先调用哪个类的方法(按照数组的先后顺序)

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        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_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

9、initialize的基本使用

initialize 方法(分类会覆盖类方法,通过消息机制(objc_msgSend)调用)

- initialize方法会在类第一次接收到消息时调用
- 调用顺序:
  先调用父类的+initialize,再调用子类的+initialize(如果父类已经调用过了,调用子类的时候,父类就不回再调用了)

为什么load三个方法都会调用?
- 直接通过函数指针调用,不是通过消息机制(objc_msgSend)调用

10、initialize 注意点

initialize和load的很大区别是,
- +initialize是通过objc_msgSend进行调用的
- +load方法是直接找到内存地址进行调用的

所以initialize有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用

// 伪代码(为什么会打印三次的原因,不代表父类初始化了3次)
if (MJStudent没有初始化) {
    if (MJPerson没有初始化) {
        objc_msgSend([MJPerson class], @selector(initialize))
    }
    objc_msgSend([MJStudent class], @selector(initialize))
}

if (MJTeacher没有初始化) {
    if (MJPerson没有初始化) {
        objc_msgSend([MJPerson class], @selector(initialize))
    }
    objc_msgSend([MJTeacher class], @selector(initialize))
}

11、面试题

1、load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时他们之间的调用过程?


区别:
- 调用方式
1、load是根据函数地址直接调用
2、initialize是荣光objc_msgSend调用

- 调用时刻
1、load是runtime加载类、分类的时候调用(只会调用1次)
2、initialize是类第一次接收消息时调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)


load、initialize的调用顺序
- load
1、先调用类的load
   先编译的类,优先调用load
   调用子类的load之前,会先调用父类的load
2、再调用分类的load
   先编译的分类,优先调用load

- initialize
  先初始化父类
  再初始化子类(可能最终调用的是父类的initialize方法)

2、不同Category中存在同一个方法,会执行哪个方法?如果是连个都执行,执行顺序是什么样的?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *test = [[NSObject alloc] init];
        [test printTest];
    }
    return 0;
}


@implementation NSObject (Test1)

- (void)printTest {
    NSLog(@"-----test1");
}

@end


@implementation NSObject (Test2)

- (void)printTest {
    NSLog(@"-----test2");
}

@end

2019-07-04 08:56:36.870570+0800 test[911:13532] -----test2
✅.jpg

3、Category和 Class Extension的区别?(分类和 类扩展(就是.m文件中的.h) 的区别)

- Class Extension 在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中

4、Category中有load方法么?load方法什么时候调用的?load方法能继承么?

- 有load方法
- +load方法会在runtime加载类、分类时调用;
- load方法可以继承,但是一般情况下不回主动去调用load方法,都是让系统自动去调用

相关文章

网友评论

    本文标题:笔记 - Category

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