iOSFuck iOS EveryDayiOS 开发每天分享优质文章

iOS原理篇(三): 关于Category

2019-05-08  本文已影响40人  75b9020bd6db

一、什么是类别

类别(Category)是Objective-C中一个灵活的类扩展机制,用于在不获悉、不改变原来代码的情况下往一个已经存在的类中添加新的方法,Category扩展的新方法有更高的优先级,会覆盖类中同名的已有方法。类别的设计体现了面向对象的核心原则,即开放封闭原则(Open Closed PrincipleOCP)。对扩展开放,对修改封闭,从而降低代码的耦合度。

二、Category的使用场合

  • 将类的实现分散到多个不同文件或多个不同框架中(为已有的类扩充新的方法
  • 创建对私有方法的前向引用
  • 可以向对象添加非正式协议

类别(Category)和 类扩展(Extension)的区别:

三、Category实现原理

Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中);

下面代码为DJTPerson添加了两个分类,之后分析都基于这段代码

//DJTPerson.h
@interface DJTPerson : NSObject
{
    int _age;
}
- (void)run;
@end

//DJTPerson.m
@implementation DJTPerson
- (void)run
{
    NSLog(@"run");
}
@end
//分类1
//DJTPerson+Test.h
@interface DJTPerson (Test)<NSCoding>
@property (assign, nonatomic) int age;

- (void)test;
+ (void)abc;

-(void)setAge:(int)age;
-(int)age;
@end

//DJTPerson+Test.m
@implementation DJTPerson (Test)
- (void)test
{
    NSLog(@"test");
}

+ (void)abc
{
    NSLog(@"abc");
}
- (void)setAge:(int)age
{
}
- (int)age
{
    return 10;
}
@end
//分类2
//DJTPerson+Test2.h
@interface DJTPerson (Test2)
@end

//DJTPerson+Test2.m"
@implementation DJTPerson (Test2)
- (void)run
{
    NSLog(@"DJTPerson (Test2) - run");
}
@end

我们知道实例对象的 isa指针指向类对象,类对象的isa指针指向元类对象,当person对象调用run方法时,通过isa指针找到类对象,然后在类对象的对象方法列表中查找方法,如果没找到就通过类对象的superclass指针找到父类对象,接着寻找run方法。
那么调用分类的方法时,是否按照同样的步骤呢?其实,OC中处理分类需要两步:

为了验证以上描述,首先通过clang编译器将分类转换为C++,查看分类编译后结构:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc DJTPerson+Test.m
在生成的C++文件中,会生成一个_category_t的结构体

struct _category_t {
    const char *name;//所属类名,本文为DJTPerson
    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_t结构体中,存放着类名name、对象方法列表instanceMethods、类方法列表classMethods、协议列表protocols和属性列表classProperties;并没有存放成员变量的列表,这也说明了分类中是不允许添加成员变量的,分类中添加的属性也不会帮助我们自动生成成员变量,只会生成getset方法的声明,需要我们自己去实现;
接着,在文件中我们可以看到_method_list_t类型的结构体:

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_DJTPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    3,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_DJTPerson_Test_test},
    {(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_DJTPerson_Test_setAge_},
    {(struct objc_selector *)"age", "i16@0:8", (void *)_I_DJTPerson_Test_age}}
};

_OBJC_$_CATEGORY_INSTANCE_METHODS_DJTPerson_$_Test这个结构体名字看出它是 INSTANCE_METHODS对象方法,并且为这个结构体赋值,结构体中存储了方法占用的内存、方法数量和方法列表。并且找到分类中我们实现的对象方法:testsetAgeage

同样的,可以看到C++文件中还包含了_method_list_t类型的类方法结构体:

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_DJTPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"abc", "v16@0:8", (void *)_C_DJTPerson_Test_abc}}
};

从名字也看出_OBJC_$_CATEGORY_CLASS_METHODS_DJTPerson_$_Test这个就是类方法列表结构体;

接下来是协议方法列表:

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", 0},
    {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
    0,
    "NSCoding",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCoding = &_OBJC_PROTOCOL_NSCoding;

static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_DJTPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1,
    &_OBJC_PROTOCOL_NSCoding
};

属性列表:

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_DJTPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"age","Ti,N"}}
};

最后在_OBJC_$_CATEGORY_DJTPerson_$_Test结构体中对上面的结构体一一赋值:

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_DJTPerson;
static struct _category_t _OBJC_$_CATEGORY_DJTPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "DJTPerson",
    0, // &OBJC_CLASS_$_DJTPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_DJTPerson_$_Test,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_DJTPerson_$_Test,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_DJTPerson_$_Test,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_DJTPerson_$_Test,
};
static void OBJC_CATEGORY_SETUP_$_DJTPerson_$_Test(void ) {
    _OBJC_$_CATEGORY_DJTPerson_$_Test.cls = &OBJC_CLASS_$_DJTPerson;
}

最后将_OBJC_$_CATEGORY_DJTPreson_$_Testcls指针指向_class_t类型的OBJC_CLASS_$_DJTPreson结构体地址,所以cls指向的应该是分类的主类类对象的地址

通过以上分析,分类编译后确实将我们定义的对象方法,类方法,属性等都存放在_catagory_t结构体中,接下来分析runtime源码,查看_catagory_t中存储的方法、属性、协议等是如何存储在类对象中的。

首先来到runtime初始化函数

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

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();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

&map_images读取模块(images这里代表模块)返回的map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码:

 // Discover categories. 
    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);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

这段代码用来查找有没有分类,通过_getObjc2CategoryList函数获取到分类列表进行遍历,来获取其中的方法、协议和属性等,而最终都调用了remethodizeClass(cls)函数,我们查看remethodizeClass(cls)函数:

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

分析上述代码发现attachCategories函数接收了类对象cls和分类数组cats,如一开始写的代码所示,一个类可以有多个分类,之前我们说到一个分类对应一个category_t结构体,那么多个分类则将这些这些category_t保存在category_list中。再看attachCategories函数:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    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);
}

从源码看出,首先根据方法列表、属性列表和协议列表malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。遍历分类方法、属性以及协议放入对应 mlistproplistsprotolosts数组中。
然后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,而class_rw_t中存放着类对象的方法、属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。
接着分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的方法、属性,和协议进行了合并,我们来看一下attachLists函数内部:

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]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

上述源代码中,array()->lists数组代表类对象原来的方法列表、属性列表和协议列表, addedLists传入所有分类的方法列表、属性列表和协议列表;attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝,memmove能保证以前的数据能完整的挪动到指定位置;
经过memmovememcpy方法之后,分类的方法、属性和协议列表被放在了类对象中原本存储的方法、属性和协议列表前面。
那为什么要将分类方法列表追加到本来的对象方法前面呢,这是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法;其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用,本类的方法依然在内存中,可以通过打印类的所有方法进行查看,再DJTPerson类中存储着两个run方法;

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 释放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ - %@", cls, methodNames);
}

- (void)viewDidLoad {
    [super viewDidLoad];    
    DJTPreson *p = [[DJTPreson alloc] init];
    [p run];
    [self printMethodNamesOfClass:[DJTPreson class]];
}

打印结果:

2019-05-04[49082:260754984] DJTPerson (Test2) - run
2019-05-04[49082:260754984] DJTPerson - test, run, run, setAge:, setAge:, age, age,

四、+load()+initialize()

+load方法调用原理

+load方法是在runtime加载类、分类的时候调用,每个类、分类的+load方法,在程序运行过程中只调用一次;他和其他方法的调用不同,不是通过消息机制(isa一层层查找),而是在类、分类加载时直接通过函数地址调用。

下面示例代码中,定义DJTPerson类和它的两个分类Test1Test2:

// DJTPerson
@interface DJTPerson : NSObject
@end

@implementation DJTPerson
+ (void)load
{
    NSLog(@"DJTPerson +load");
}
@end

//分类DJTPerson+Test1
@interface DJTPerson (Test1)
@end

@implementation DJTPerson (Test1)
+ (void)load
{
    NSLog(@"DJTPerson(Test1) +load");
}
@end

//分类DJTPerson+Test2
@interface DJTPerson (Test2)
@end

@implementation DJTPerson (Test2)
+ (void)load
{
    NSLog(@"DJTPerson(Test2) +load");
}
@end

直接运行程序,即使没有在main函数中引用上述头文件,依然有如下打印:


这说明虽然并没有使用到这些类,但程序运行这些类会被载进内存,就会调用它们的 +load方法;这和前面所说的在分类中定义同名的run方法会覆盖原类的run方法不同,明明分类中有定义load方法,覆盖了原类中的load方法,那为什么DJTPerson中的load方法还会被调用呢?(这里说覆盖并不准确,而是分类中同名方法经合并会放在方法列表的前面);
通过阅读运行时源码,在运行是入口找到load_images方法:

load_images方法内部会调用call_load_methods(void)方法

从源码看出,先调用类的load方法,在调用分类的load方法,这和编译顺序无关,如下调换编译顺序,依然先调用 DJTPersonload方法:

那类的load方法又是如何调用呢?进入call_class_loads()方法:


这里调用类的load方法是直接通过函数指针进行调用,不会向之前调用run方法那样去方法列表查找,或者说不是通过消息发送机制的形式。

分类的load方法是如何调用呢?进入call_category_loads(void)方法:


我们发现它也是通过指针指向分类load方法地址,然后直接调用;
总结:从上面看出确实是优先调用类的load方法,然后调用分类的load方法;

接着,考虑有多个类,一个类有多个分类,以及类存在父类的情况,我们看看此时+load方法的调用顺序,新增DJTDogDJTCat两个类继承自NSObject,新增DJTStudent类继承自DJTPerson,同时为DJTStudent添加两个分类Test1Test2,编译顺序如下:


从打印结果看出:

+initialize方法调用原理

+initialize方法会在类第一次接收到消息时调用,并且它的调用顺序是先调用父类的+initialize,再调用子类的+initialize,前提是父类的+initialize方法没有被调用过。

查看源码,在objc-class.mm文件中,有一个获取对象方法的函数class_getInstanceMethod()和一个获取类方法的函数class_getClassMethod(),在class_getClassMethod()函数中其实也是调用的class_getInstanceMethod()方法,上源码:

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
#warning fixme build and search caches
    // Search method lists, try method resolver, etc.
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

内部调用lookUpImpOrNil(),再调用lookUpImpOrForward():

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ...............

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
    ...................

上面if中判断传入的参数initialize,表示是否需要初始化,cls->isInitialized()判断这个类是否已经初始化,在满足需要初始化并且类未初始化时,调用_class_initialize()方法进行初始化,我们看一下_class_initialize()中都做了什么:

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());
    Class supercls;
    bool reallyInitialize = NO;
    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
    // Try to atomically set CLS_INITIALIZING.
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }
    if (reallyInitialize) {
     .............
     @try
        {
            callInitialize(cls);
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
    else if (cls->isInitialized()) {
        // Set CLS_INITIALIZING failed because someone else already 
        return;
    }
    else {
        // We shouldn't be here. 
        _objc_fatal("thread-safe class init in objc runtime is buggy!");
    }
}

if判断中,如果存在父类,并且父类没有初始化,先去初始化父类,即调用callInitialize(cls)方法:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

从上面源码看出,+initialize()方法最终是通过objc_msgSend消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此如果分类方法中有实现,会优先调用分类方法中的实现;

综上,我们为DJTPresonDJTStudentDJTStudent+Test 添加initialize方法,当类第一次接收到消息时(或者说第一次使用类的时),就会调用initialize,在调用子类的initialize之前,会先保证调用父类的initialize方法,如果之前已经调用过initialize,就不会再调用initialize方法了,当分类重写initialize方法时会先调用分类的方法。

五、总结

上一篇下一篇

猜你喜欢

热点阅读