笔记 - Category

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方法,都是让系统自动去调用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342