iOS底层原理之`OC语法`(Category、load、initialize、关联对象)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Bolted_snail/article/details/82258597

Category

  • CategoryObjective-C 2.0之后添加的语言特性,又叫分类、类别Category的主要作用是为已经存在的类添加方法,使模块化,组件化(不同功能不同的分类)。

底层实现

Person类创建两个分类Person (Test1)个Person (Test2)如下:

#import "Person.h"
@interface Person (Test1)
- (void)test1;
+ (void)classTest1;
@end
#import "Person+Test1.h"
@implementation Person (Test1)
- (void)test1{
    NSLog(@"test1");
}
+ (void)classTest1{
    NSLog(@"classTest1");
}
@end


#import "Person.h"
@interface Person (Test2)
- (void)test2;
+ (void)classTest2;
@end
@implementation Person (Test2)
- (void)test2{
    NSLog(@"test2");
}
+ (void)classTest2{
    NSLog(@"classTest2");
}
@end

通过命令行获取.m文件编译后的才c++文件。(xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test1.m -o Person+Test1.cpp)其中关键编译后的代码如下:
Category的本质是一个_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;//属性列表
};

Person (Test1)编一个后主要代码有:

//Person (Test1)编译后的结构体
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person",
	0, // &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test1,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test1,
	0,
	0,
};

//test1方法实现
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test1", "v16@0:8", (void *)_I_Person_Test1_test1}}
};

//classTest1方法实现
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_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"classTest1", "v16@0:8", (void *)_C_Person_Test1_classTest1}}
};

上面可以看出如果没有参数就是0,Person (Test2)也是如此,这里不再赘述。可以看出Category并没有生产新的类对象(类对象结构前面讲过,可参考:oc对象的本质),oc对象本质是objc_class结构体而分类本质是_category_t结构体,那么分类中的方法是如果调用的呢?

Categroy的加载过程

通过runtime原码分析(有些复杂,这里不再详述)可以得出Categroy的加载过程:

  1. 在编译时只是一个包含类名,类指针,方法列表,属性列表,协议列表的_category_t结构体;
  2. 在运行时通过runtime加载分类数据,把分类的方法、属性、协议数据合并到一个大数组中,后参与编译的分类会在数组的前面(i–倒数遍历数组添加的)(这也说明了分类中相同的方法后参与编译的分类会被调用);
  3. 合并后的分类数据会插入到原来类数据的前面(相同的方法,分类会优先于原来调用)。
    编译顺序是可以手动设置的:TARGETS->BuildPhases->Complle Sources。(自己可以验证一下,这里不再赘述)
    编译顺序

Category属性

上面Category的底层结构是_category_t结构体,里面可以看出有属性列表,那么是不是就是可以添加属性呢?
给上面Person (Test1)添加一个属性,并设置属性赋值

#import "Person.h"
@interface Person (Test1)
- (void)test1;
+ (void)classTest1;
@property(nonatomic ,copy) NSString * name;
@end

//ViewController中设置Person的name属性
- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p =[[Person alloc]init];
    p.name = @"马云";
}

给分类设置属性
可以看出在编译的时候是不报错的,但是在运行的时候报改属性方法找不到。实际上Category是允许添加属性的,同样可以使用@property,但是不会生成成员变量,也不会生成添加属性的gettersetter方法的实现。所以尽管添加了属性,在运行时调用属性方法就会报错。

+load方法

  • +load方法是在加载类和分类时系统调用,一般不手动调用,如果想要在类或分类加载时做一些事情,可以重写类或者分类的+load方法方法。

  • 每个类、分类的+load,在程序运行过程中只调用一次。

  • 调用顺序

  1. 类要优先于分类调用+load方法;
  2. 子类调用+load方法时,要先要调用父类的+load方法;(父类优先与子类,与继承不同);
  3. 不同的类按照编译先后顺序调用+load方法(先编译,先调用);
  4. 分类的按照编译先后顺序调用+load方法(先编译,先调用)。
  • 实例:
    创建Person、Dog、Cat类继承于NSObject; Man、Woman继承于Person;给Person添加分类Person+Test1Person+Test2,给Cat添加分类Cat+Test;,并在每个类里面重写+load方法,如下图结构:
    load
    编译顺序设置和结果打印如下:
    编译顺序设置和结果打印
    这里并没有将头文件到main文件中,也没有创建任何实例对象,但在程序运行的时候就调用了每个类和分类的load方法(编译时不会调用),说明在程序运行时,系统自动加载这些类和分类时就调用了该方法。

+load方法底层实现

  • 下载runtime底层框架源码:runtime底层框架源码

  • objc4源码解读+load方法实现过程:
    objc-os.mm
    _objc_init
    load_images
    prepare_load_methods
    schedule_class_load
    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方法时调用:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();
  //类列表
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
   //分类列表
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

调度类的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
    //在调度类的load方法前,要先调度分类的load方法(递归)
    schedule_class_load(cls->superclass);
    //添加到能够加载的类的列表中
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

调用load方法:

//调用load方法
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) {
        //调用类的load方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        //调用分类的load方法
        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;
}

调用类的load方法

//加载类的load方法
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方法
        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方法
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

调用分类的load方法

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
       //获取分类
        Category cat = cats[i].cat;
        //获取分类的load方法
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            //分类调用load方法
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }
    return new_categories_added;
}
  • 由上面原码分析可以看出:在将要调度load方法时,先把类加入的加载类数组中,分类加入到分类的列表中,并且类的load方法先调用;在调用类的load方法时会用递归的方法先调用父类的的load方法,所以load方法是类优先于分类调用,父类优先于子类调用。
  • +load方法是系统根据方法地址直接调用,并不是经过objc_msgSend函数调用(isa,superClass那一套机制),如果认为手动调用,则是通过消息机制调用。

+initialize方法

  • +initialize方法会在类第一次接收到消息时(即第一次分配内存创建实例对象(alloc))调用,我们如果想在类第一次初始化时做一些事情,可以重写类的+initialize方法。
  • 调用顺序:
    先调用父类的+initialize,再调用子类的+initialize
    (先初始化父类,再初始化子类,每个类只会初始化1次)
    initialize
    上面创建了两次Man的实例对象,三次Woman的实例对象,却只打印三次;因为第一次创建Man的实例对象有线调用父类的+initialize方法,由于是通过objc_msgSend进行调用的,所以会先查找到分类的+initialize方法并调用,然后会调用Man自己的+initialize方法,第二次创建时则不再调用+initialize;Woman在第一次创建时因为其父类的+initialize已经调用过一次,所以不再调用,直接调用自己的+initialize方法,第二次、三次创建时都不在调用Woman+initialize方法了。

+initialize底层原理

  • +initialize 的objc4源码解读过程(原码下载地址同load):
    objc-msg-arm64.s
    objc_msgSend
    objc-runtime-new.m
    class_getInstanceMethod
    lookUpImpOrNil
    lookUpImpOrForward
    _class_initialize
    callInitialize
    objc_msgSend(cls, SEL_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);
    }
    }

调用Initialize方法

void callInitialize(Class cls)
{   //本质是objc_msgSend
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}
  • 由上面原码分析可以看出:+initialize是通过objc_msgSend(isa,superclass机制)进行调用的,但是在调用自己的 +initialize方法之前会优先递归调用父类(前提是父类没有初始化,用isInitialized布尔值判断的)。
  • 如果子类没有实现 +initialize方法,父类的+initialize方法会被调用多次,原理同上;比如把上面ManWoman+initialize方法都注销:
    +initialize
    上面打印结果可见三次打印的都是Person+initialize,因为第一次创建Man的实例对象时,会先调用Person+initialize的方法打印一次,然后调用Man+initialize方法,通过isaMan原类电的类方法列表中没有找到该方法就通过superClass就查找父类Person用有就调用了,就打印了第二次,当Woman第一次创建时因为父类的+initialize已经调用了一次,所以会调用自己的+initialize方法,发现自己的原类中没有就去父类中找,所以Person+initialize方法调用了三次。

关联对象(给Categroy添加成员变量)

  • Categroy底层结构可以看出是有属性列表(properties)的,可以添加属性;但是并没有成员变量列表(ivars),直接给分类添加成员变量是会报错的,事实上给分类添加属性只是产生setter/getter方法的声明,没有产生带下划线的成员变量,也没有产生setter/getter方法实现。
  • 能否给Categroy添加成员变量呢?直接添加肯定是不行的,但是可以间接添加。
  1. 方案一:给分类添加全局变量,并且手动重写setter/getter方法实现。
#import "Person+Test.h"
@implementation Person (Test)
NSString * name_;
int age_;

-(void)setName:(NSString *)name{
    name_ = name;
}
- (NSString *)name{
    return name_;
}

-(void)setAge:(int)age{
    age_ = age;
}
-(int)age{
    return age_;
}
@end


#import <Foundation/Foundation.h>
#import "Person+Test.h"
extern NSString * name_;
extern int age_;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person * p = [[Person alloc]init];
        p.name = @"a";
        p.age = 1;
        Person * p2 = [[Person alloc]init];
        p2.name = @"b";
        p2.age = 2;
        name_ = @"c";
        age_ = 3;
        NSLog(@"p---name:%@---age:%d",p.name,p.age);
        NSLog(@"p2---name:%@---age:%d",p2.name,p2.age);
    }
    return 0;
}

全局变量
如果只创建一个对象是可以用上面方法的,如果创建多个对象 由于是全局变量,会多次设置这个全局变量,结果不是我们想要的,并且也可以用extern来访问该全局变量,所以方案一不客取得。

  1. 方案二:将变量保存在全局可变字典中,并且以当前对象的指针(唯一性)为key来设置和取出值。
#import "Person+Test.h"
#define PKey [NSString stringWithFormat:@"%p", self]
@implementation Person (Test)
static NSMutableDictionary *names_;
static NSMutableDictionary *ages_;
+(void)load{
    names_ = [NSMutableDictionary dictionary];
    ages_ = [NSMutableDictionary dictionary];
}
-(void)setName:(NSString *)name{
    names_[PKey] = name;
}
-(NSString *)name{
    return names_[PKey];
}
-(void)setAge:(int)age{
    ages_[PKey] = @(age);
}
-(int)age{
    return [ages_[PKey] intValue];
}
@end

方案二
方案是可以达到我们想要的结果,但是声明的全局变量可变字典在对象销毁时无法销毁,同时比较繁琐,所以不实用。

  • 其实runtime提供了关联对象的方法可以很方便的给分类添加成员变量。
  • 关联对象提供了以下API:
添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
获得关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象
void objc_removeAssociatedObjects(id object)
  1. 方案三:通过关联对象给分类添加成员变量
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)
//关联对象
-(void)setName:(NSString *)name{
   //添加关联对象
    /*
     参数:
     id _Nonnull object:被关联的对象
     const void * _Nonnull key:指针类型,设置值和取值唯一的key,可以说任意指针,唯一就行
     id _Nullable value:设置关联对象的值
     objc_AssociationPolicy policy:关联的策略
     */
    
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)name{
    // 隐式参数
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}
-(void)setAge:(int)age{
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}
-(int)age{
    return [objc_getAssociatedObject(self, @selector(age)) intValue];
}
@end

打印结构同上,其中在添加关联是有两个参数需要注意,第二个const void * _Nonnull key参数指针类型,设置值和取值唯一的key,可以说任意指针,唯一就行;第四个参数objc_AssociationPolicy policy是关联的策略,类似属性中的strong,copy等.
关联策略对比参考:

objc_AssociationPolicy 对应的修饰符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

注意:并没有与weak对等的关联策略。

关联对象的原理

  • 实现关联对象技术的核心对象有:AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation。

  • objc4源码解读:objc-references.mm
    关联对象的原理

  • void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)方法解析。
    添加关联对象时底层方法解析
    上面可以看出:关联对象并没有添加到被关联对象的内存中(上面分类的name属性并没有将一个_name的成员变量添加的Person类的成员变列表中),关联对象对保存在AssociationsManager中,是一个HashMap(类似于字典),被关联对象object作为key保存AssociationsHashMap,然后第二个参数指针key作为key保存ObjectAssociationMap,而ObjectAssociationMap保存的是关联策略和关联对象的值。

面试题:

  1. Category的实现原理
    Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中),并且添加在类信息的前面,后编译的分类方法有优先调用;
  2. CategoryClass Extension的区别是什么?
    Class Extension在编译的时候,它的数据就已经包含在类信息中;
    Category是在运行时,才会将数据合并到类信息中。
  3. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
    load方法,load方法在runtime加载类、分类的时候调用,load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
  4. loadinitialize方法的区别什么?它们在Category中的调用的顺序?以及出现继承时他们之间的调用过程?
    load是在加载类的时候就调用,initialize是在可第一次初始化(创建)的时候调用;
    load方法的调用顺序是类优先于分类,父类优先于子类,不同的类是先参与编译先调用,不同的分了也是先参与编译先调用。(类优先与分类,父类优先于子类,先编译先调用
    initialize是在类中调用时要先调用父类的该方法,由于是objc_msgSend机制,所有分类要优先于类调用。(分类优先于类,父类优先于子类);
  5. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
    不能直接给Category添加成员变量,可以通过runtime的管理对象的方法简洁的给分类添加成员变量。

猜你喜欢

转载自blog.csdn.net/Bolted_snail/article/details/82258597