iOS の Category

2020-04-08  本文已影响0人  游城十代2dai

0x00 Category 实现原理

编译之后每个 Category 文件都会生成一个 struct _category_t 的结构体

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

看下面源码可以知道( while i-- ), Category 是按照编译顺序倒序加入 method_list

// Category 主要部分源码
// 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;
    }
}

所以

  1. 通过 Runtime 加载某各类的所有 Category 数据
  2. 把所有内容(属性方法协议等)放入一个大的数组中(倒序排列)
  3. 合并后的数据插入到原始数据的前面, 所以并不是 Category 内实现的方法覆盖了原方法

0x01 Category 与 Extension 区别


0x02 Category 中的 +load 方法

  1. Category 的 load 方法什么时候调用?
  1. 调用顺序 (看下面的源码)
  1. 为什么 load 方法会被全部调用, 而其他的方法只会调用最前面的?
    通过源码可以知道因为 load 方法是系统通过函数地址调用, 所有都调了一遍, 而其他方法是手动通过消息发送机制调用, 故只调用了最前面的, 手动 load 也是消息机制也只会调用最前面的 load
/*  prepare_load_methods 这里会准备好执行 load 方法的类的顺序  */
 
 void
 load_images(const char *path __unused, const struct mach_header *mh)
 {
     // Return without taking locks if there are no +load methods here.
     if (!hasLoadMethods((const headerType *)mh)) return;

     recursive_mutex_locker_t lock(loadMethodLock);

     // Discover load methods
     {
         rwlock_writer_t lock2(runtimeLock);
         prepare_load_methods((const headerType *)mh);
     }

     // Call +load methods (without runtimeLock - re-entrant)
     call_load_methods();
 }

/*  递归将类和父类添加入数组, 优先加入的是父类, 而选择类的顺序为编译顺序(即调整编译顺序 load 调用也会改变)  */
 
 static void schedule_class_load(Class cls)
 {
     if (!cls) return;
     assert(cls->isRealized());  // _read_images should realize

     if (cls->data()->flags & RW_LOADED) return;

     // Ensure superclass-first ordering
     schedule_class_load(cls->superclass);

     add_class_to_loadable_list(cls);
     cls->setInfo(RW_LOADED);
 }


/*  while (loadable_classes_used > 0) { call_class_loads(); }
 *  上面的 while 就是说存在可用的类时就会调用类的 load 方法 (call_class_loads), 没有可用的类的时候就会调用类别的方法(call_category_loads)  */
 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;
 }


0x03 About Initialize

  1. 类第一次接收到消息的时候调用
  2. 调用子类的, 会先调用父类的
  3. 如果子类没有实现, 就会调用父类的, 所以会出现多次调用
  4. 如果 Category 实现了, 就会调用 Category 的
// 递归去做父类的初始化
supercls = cls->superclass;
if (supercls  &&  !supercls->isInitialized()) {
    _class_initialize(supercls);
} 
    
// 实际就是消息发送
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

0x04 为 Category 添加属性

Category 不能添加成员变量, 但是可以间接实现添加属性

主要是用 Runtime 的对象关联手段, Runtime 中有个 AssociationsManager 类做管理, 其中有全局的 hashmap 负责收藏属性的值

这个 hashmap 是两层的就比如下面的例子在实际的 hashmap 中是类似这样的{ &per : { name 的 key : name 的 value } }


0x05 我的测试代码

使用 Command Line Tool 创建测试就好

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

void _aboutCategory(void);
void _about_load(void);
void _about_initialize(void);
void _about_instance_ivar(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
//        _aboutCategory();
//        _about_load();
//        _about_initialize();
        _about_instance_ivar();
    }
    return 0;
}

void printMethodNamesOfClass(Class cls) {
    // 定义
    unsigned int outCount = 0;
    Method *methodList = NULL;
    Method tempMethod = NULL;
    NSMutableString *strResult = [NSMutableString string];
    NSString *strTemp = [NSString string];
    
    // copy 出方法列表
    methodList = class_copyMethodList(cls, &outCount);
    
    // 遍历
    for (int i = 0; i < outCount; ++i) {
        tempMethod = methodList[i];
        strTemp = NSStringFromSelector(method_getName(tempMethod));
        [strResult appendFormat:@"\n%@", strTemp];
    }
    
    NSLog(@"%@", strResult);
}


@interface Person : NSObject
- (void)walk;
@end
@implementation Person
+ (void)initialize {
    NSLog(@"+initialize Person");
}
+ (void)load {
    NSLog(@"+load Person");
}
- (void)walk {
    NSLog(@"any person can walk");
}
@end

@interface Person (Run)
- (void)run;
@end
@implementation Person (Run)
- (void)run {
    NSLog(@"any person can run");
}
@end

@interface Person (Eat)
- (void)eat;
@end
@implementation Person (Eat)
- (void)eat {
    NSLog(@"any person can eat");
}
@end

/**
 * Category 与 Extension 区别在于:
 * Extension 是在编译的时候数据就和类的信息放在一起
 * Category 是在运行时才将数据合并在一起
 */

void _aboutCategory() {
    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;
    };
    /**
     * 编译之后每个 Category 文件都会生成一个 struct _category_t 的结构体
     * 看下面源码可以知道( while i-- ), Category 是按照编译顺序倒序加入 method_list 内
     * 实现原理如下:
     * 1. 通过 Runtime 加载某各类的所有 Category 数据
     * 2. 把所有内容(属性方法协议等)放入一个大的数组中(倒序排列)
     * 3. 合并后的数据插入到原始数据的前面, 所以并不是 Category 内实现的方法覆盖了原方法
     */
    
    Person *person = Person.alloc.init;
    
    [person walk];
    [person eat];
    [person run];
    
    printMethodNamesOfClass(person.class);
    
}

// Category 主要部分源码
// 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;
//    }
//}

@interface Student : Person
+ (void)test;
@end
@implementation Student
//+ (void)initialize {
//    NSLog(@"+initialize Student");
//}
+ (void)load {
    NSLog(@"+load Student");
}
+ (void)test {
    NSLog(@"+test");
}
@end

@interface Student (Run)

@end
@implementation Student (Run)
//+ (void)initialize {
//    NSLog(@"+initialize Run");
//}
+ (void)load {
    NSLog(@"+load Run");
}
+ (void)test {
    NSLog(@"+test Run");
}
@end

@interface Student (Eat)

@end
@implementation Student (Eat)
//+ (void)initialize {
//    NSLog(@"+initialize Eat");
//}
+ (void)load {
    NSLog(@"+load Eat");
}
+ (void)test {
    NSLog(@"+test Eat");
}
@end

/** 下面注释中的是相关部分源码
 * 1. Category 的 load 方法什么时候调用?
 * load 方法是在运行时加载类和分类的时候调用, 父类会优先调用
 *
 * 2. 调用顺序 (看下面的源码)
 * 先调用类的 load 方法, 先按照编译顺序, 然后按照先父类, 后子类
 * 再调用Category 的 load 方法, 按照编译顺序
 *
 * 3. 为什么 load 方法会被全部调用, 而其他的方法只会调用最前面的?
 * 通过源码可以知道因为 load 方法是系统通过函数地址调用, 所有都调了一遍, 而其他方法是手动通过消息发送机制调用, 故只调用了最前面的, 手动 load 也是消息机制也只会调用最前面的 load
 */
void _about_load() {
    [Student test];
    Student *std = Student.alloc.init;
    printMethodNamesOfClass(object_getClass(std.class));
}

/*  prepare_load_methods 这里会准备好执行 load 方法的类的顺序
 
 void
 load_images(const char *path __unused, const struct mach_header *mh)
 {
     // Return without taking locks if there are no +load methods here.
     if (!hasLoadMethods((const headerType *)mh)) return;

     recursive_mutex_locker_t lock(loadMethodLock);

     // Discover load methods
     {
         rwlock_writer_t lock2(runtimeLock);
         prepare_load_methods((const headerType *)mh);
     }

     // Call +load methods (without runtimeLock - re-entrant)
     call_load_methods();
 }
 */

/*  递归将类和父类添加入数组, 优先加入的是父类, 而选择类的顺序为编译顺序(即调整编译顺序 load 调用也会改变)
 
 static void schedule_class_load(Class cls)
 {
     if (!cls) return;
     assert(cls->isRealized());  // _read_images should realize

     if (cls->data()->flags & RW_LOADED) return;

     // Ensure superclass-first ordering
     schedule_class_load(cls->superclass);

     add_class_to_loadable_list(cls);
     cls->setInfo(RW_LOADED);
 }
 
 */

/*  while (loadable_classes_used > 0) { call_class_loads(); }
 *  上面的 while 就是说存在可用的类时就会调用类的 load 方法 (call_class_loads), 没有可用的类的时候就会调用类别的方法(call_category_loads   )
 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;
 }

 */


/** initialize
 * 1. 类第一次接收到消息的时候调用
 * 2. 调用子类的, 会先调用父类的
 * 3. 如果子类没有实现, 就会调用父类的, 所以会出现多次调用
 * 4. 如果 Category 实现了, 就会调用 Category 的
 */
void _about_initialize(void) {
//    [Student alloc]  不调用就不执行
    [Student alloc];
}


@interface Person (Name)
//{ NSString * _name }  成员变量无法添加的

@property (nonatomic, copy) NSString *name;
@end

@implementation Person (Name)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, _cmd);
}

@end

/**
 * Category 不能添加成员变量, 但是可以间接实现添加属性
 * 主要是用 Runtime 的对象关联手段, Runtime 中有个 AssociationsManager 类做管理, 其中有全局的 hashmap 负责收藏属性的值
 * 这个 hashmap 是两层的就比如下面的例子在实际的 hashmap 中是类似这样的 {  &per : { name 的 key :  name 的 value } }
 */
void _about_instance_ivar(void) {
    Person *per = Person.alloc.init;
    
    per.name = @"lily";
    NSLog(@"%@", per.name);
    
    per.name = @"lily_2";
    NSLog(@"%@", per.name);
}

上一篇 下一篇

猜你喜欢

热点阅读