iOS runtime探究

什么是runtime

runtime就是运行时,在实际开发中使用runtime的场景并不多,但是了解runtime有助于我们更好的理解OC的原理,从而提高开发水平。
runtime很强大,是OC最重要的一部分也是OC最大的特色,可以不夸张的说runtime成就了OC,尽管runtime是OC的一个模块而已。
我们都知道高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体,本文正是通过runtime源码分析来讲解runtime是如何将面向对象的类转变为面向过程的结构体。

深入代码理解instance、class object、metaclass

面向对象编程中,最重要的概念就是类,下面我们就从代码入手,看看OC是如何实现类的。

前文一直在说runtime将面向对象的类转变为面向过程的结构体,那这个结构体到底是什么样子的?打开#import<objc/objc.h>文件,可以发现以下几行代码

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

通过注释和代码不难发现,我们创建的一个对象或实例其实就是一个struct objc_object结构体,而我们常用的id也就是这个结构体的指针。有如下代码:

//以下两种写法都成立
id str = [[NSString alloc] init];
NSString *str = [[NSString alloc] init];

通过上述代码可以看出,我们创建的NSString类的实例str其实就是一个struct objc_object结构体指针,所以不管是Foundation框架中的类或是自定义的类,我们创建的类的实例最终获取的都是一个结构体指针,这个结构体只有一个成员变量就是Class类型的isa指针,Class是结构体指针,指向struct objc_class,那这个结构体又是什么呢?这里先透露一句话str is a NSString,再加上Class这个指针的名字,我们不难猜测,Class就是代表NSString这个类。
接下来会详细讲解这个结构体,现在再看另一个例子,有时我们也会通过下述方法来创建一个实例:

NSString *str = [[NSString alloc] initWithString: @"Hello World"];
Class c = [str class];
NSString *str2 = [[c alloc] initWithString: @"Hello World"];

可能你已经发现了,通过实例对象调用的class方法,我们能够获取到一个Class类型的变量,我们可以通过这个Class来创建相应的实例对象。
实际上,OC中的类也是一个对象,称为类对象,上述方法中通过[str class]方法获取到的就是NSString类类对象,接着我们就可以通过这个类对象来创建实例对象,那这个类对象又是什么东西呢?打开#import<objc/runtime.h>文件,我们可以找到结构体struct objc_class的定义,该结构体定义如下:

文件objc/runtime.h中有如下定义:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

    Class super_class                                        
    const char *name                                         
    long version                                             
    long info                                                
    long instance_size                                       
    struct objc_ivar_list *ivars                             
    struct objc_method_list **methodLists                    
    struct objc_cache *cache                                 
    struct objc_protocol_list *protocols                     
}
/* Use `Class` instead of `struct objc_class *` */

文件objc/objc.h文件中有如下定义
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

struct objc_class结构体定义了很多变量,通过命名不难发现,结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象类对象在编译期产生用于创建实例对象,是单例。

类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象类方法应该从哪里创建呢?就是从isa指针指向的结构体创建,类对象isa指针指向的我们称之为元类(metaclass)元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:

实例对象、类对象与元类简图

通过上图我们可以清晰的看出来一个实例对象也就是struct objc_object结构体它的isa指针指向类对象类对象isa指针指向了元类,super_class指针指向了父类的类对象,而元类super_class指针指向了父类的元类,那元类isa指针又指向了什么?为了更清晰的表达直接使用一个大神画的图。

实例对象、类对象与元类的自闭环

通过上图我们可以看出整个体系构成了一个自闭环,如果是从NSObject中继承而来的上图中的Root class就是NSObject。至此,整个实例类对象元类的概念也就讲清了,接下来我们在代码中看看这些概念该怎么应用。

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        Class c1 = [p class];
        Class c2 = [Person class];
        //输出 1
        NSLog(@"%d", c1 == c2);
    }
    return 0;
}

c1是通过一个实例对象获取的Class,实例对象可以获取到其类对象,类名作为消息的接受者时代表的是类对象,因此类对象获取Class得到的是其本身,同时也印证了类对象是一个单例的想法。
那么如果我们想获取isa指针的指向对象呢?

介绍两个函数

OBJC_EXPORT BOOL class_isMetaClass(Class cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
    
OBJC_EXPORT Class object_getClass(id obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

class_isMetaClass用于判断Class对象是否为元类object_getClass用于获取对象的isa指针指向的对象。

再看如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        //输出1
        NSLog(@"%d", [p class] == object_getClass(p));
        //输出0
        NSLog(@"%d", class_isMetaClass(object_getClass(p)));
        //输出1
        NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
        //输出0
        NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
    }
    return 0;
}

通过代码可以看出,一个实例对象通过class方法获取的Class就是它的isa指针指向的类对象,而类对象不是元类类对象isa指针指向的对象是元类

总结

通过上文的代码分析,我们已经了解了OC中的类和实例是如何映射到C语言结构体的,实例对象是一个结构体,这个结构体只有一个成员变量,指向构造它的那个类对象,这个类对象中存储了一切实例对象需要的信息包括实例变量、实例方法等,而类对象是通过元类创建的,元类中保存了类变量和类方法,这样就完美解释了整个类和实例是如何映射到结构体的

你不知道的msg_send

我们知道在OC中的实例对象调用一个方法称作消息传递,比如有如下代码:

NSMutableString *str = [[NSMutableString alloc] initWithString: @"Jiaming Chen"];
[str appendString:@" is a good guy."];

上述代码中的第二句str称为消息的接受者,appendString:称作选择子也就是我们常用的selectorselector参数共同构成了消息,所以第二句话可以理解为将消息:"增加一个字符串: is a good guy"发送给消息的接受者str
OC中里的消息传递采用动态绑定机制来决定具体调用哪个方法,OC的实例方法在转写为C语言后实际就是一个函数,但是OC并不是在编译期决定调用哪个函数,而是在运行期决定,因为编译期根本不能确定最终会调用哪个函数,这是由于运行期可以修改方法的实现,在后文会有讲解。举个栗子,有如下代码:

id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];

上述代码在编译期没有任何问题,因为id类型可以指向任何类型的实例对象,NSString有一个方法appendString:,在编译期不确定这个num到底具体指代什么类型的实例对象,并且在运行期还可以给NSNumber类型添加新的方法,因此编译期发现有appendString:的函数声明就不会报错,但在运行时找不到在NSNumber类中找不到appendString:方法,就会报错。这也就是消息传递的强大之处和弊端,编译期无法检查到未定义的方法,运行期可以添加新的方法。

讲了这么多OC究竟是怎么将实例方法转换为C语言的函数,又是如何调用这些函数的呢?这些都依靠强大的runtime

在深入代码之前介绍一个clang编译器的命令:

clang -rewrite-objc main.m
该命令可以将.m的OC文件转写为.cpp文件

有如下代码:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

- (void)showMyself;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

- (void)showMyself {
    NSLog(@"My name is %@ I am %ld years old.", self.name, self.age);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
         //为了方便查看转写后的C语言代码,将alloc和init分两步完成
        Person *p = [Person alloc];
        p = [p init];
        p.name = @"Jiaming Chen";
        [p showMyself];
    }
    return 0;
}

通过上述clang命令可以转写代码,然后找到如下定义:

static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }

// @synthesize age = _age;
static NSUInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, NSUInteger age) { (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }

static void _I_Person_showMyself(Person * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")), ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
}

// @end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
        p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));

    }
    return 0;
}

关于属性property生成的gettersetter和实例变量相关代码在另一篇博客iOS @property探究(二): 深入理解中有详细介绍,本文不再赘述,本文仅针对自定义的方法来讲解。

可以发现转写后的C语言代码将实例方法转写为了一个静态函数。接下来一行一行的分析上述代码,第一行代码可以简要表示为如下代码:

Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

这一行代码做了三件事情,第一获取Person类,第二注册alloc方法,第三发送消息,将消息alloc发送给类对象,可以简单的将注册方法理解为,通过方法名获取到转写后C语言函数的函数指针。
第二行代码就可以简写为如下代码:

p = objc_msgSend(p, sel_registerName("init"));

这一行代码与上一行类似,注册了init方法,然后通过objc_msgSend函数将消息init发送给消息的接受者p
第三行是一个对setter的调用,同样的也可以简写为如下代码:

//这一行是用来查找参数的地址,取名为name
(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1)
objc_msgSend(p, sel_registerName("setName:"), name);

这一行代码同样是先注册方法setName:然后通过objc_msgSend函数将消息setName:发送给消息的接收者,只是多了一个参数的传递。
同理,最后一行代码也可以简写为如下:

objc_msgSend(p, sel_registerName("showMyself"));

解释与上述相同,不再赘述。

到这里,我们应该就可以看出OC的runtime通过objc_msgSend函数将一个面向对象的消息传递转为了面向过程的函数调用。
objc_msgSend函数根据消息的接受者和selector选择适当的方法来调用,那它又是如何选择的呢?这就涉及到前一篇博客讲解的内容iOS runtime探究(一): 从runtime开始: 理解面向对象的类到面向过程的结构体,这一篇博客中详细讲解了OC的runtime是如何将面向对象的类映射为面向过程的结构体的,再来回顾一下几个主要的结构体:

文件objc/runtime.h中有如下定义:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

    Class super_class                                        
    const char *name                                         
    long version                                             
    long info                                                
    long instance_size                                       
    struct objc_ivar_list *ivars                             
    struct objc_method_list **methodLists                    
    struct objc_cache *cache                                 
    struct objc_protocol_list *protocols                     
}
/* Use `Class` instead of `struct objc_class *` */

文件objc/objc.h文件中有如下定义
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

注意结构体struct objc_class中包含一个成员变量struct objc_method_list **methodLists,通过名称我们分析出这个成员变量保存了实例方法列表,继续查找结构体struct objc_method_list的定义如下:

static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        5,
        {{(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
        {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
        {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
        {(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
        {(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};

struct _objc_method {
        struct objc_selector * _cmd;
        const char *method_type;
        void  *_imp;
};

我们发现struct objc_method_list中还包含了一个未知的结构体struct _objc_method同时也找到它的定义,为了方便查看将两者写在一起。
结构体struct objc_method_list里面包含以下几个成员变量:结构体struct _objc_method的大小、方法个数以及最重要的方法列表,方法列表存储的是方法描述结构体struct _objc_method,该结构体里保存了选择子、方法类型以及方法的具体实现。可以看出方法的具体实现就是一个函数指针,也就是我们自定义的实例方法,选择子也就是selector可以理解为是一个字符串类型的名称,用于查找对应的函数实现(由于苹果没有开源selector的相关代码,但是可以查到GNU OC中关于selector的定义,也是一个结构体但是结构体里存储的就是一个字符串类型的名称)。

这样就能解释objc_msgSend的工作原理的,为了匹配消息的接收者和选择子,需要在消息的接收者所在的类中去搜索这个struct objc_method_list方法列表,如果能找到就可以直接跳转到相关的具体实现中去调用,如果找不到,那就会通过super_class指针沿着继承树向上去搜索,如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到,那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这个方法就会报unrecognized selector错误(其实在调用这个方法之前还会进行消息转发,还有三次机会来处理,消息转发在后文会有介绍)。

这样一看,要发送消息真的好复杂,需要经过这么多步骤,难道不会影响性能吗?当然了,这样一次次搜索和静态绑定那样直接跳转到函数指针指向的位置去执行来比肯定是耗时很多的,因此,类对象也就是结构体struct objc_class中有一个成员变量struct objc_cache,这个缓存里缓存的正是搜索方法的匹配结果,这样在第二次及以后再访问时就可以采用映射的方式找到相关实现的具体位置。

到这里我们就已经弄清楚了整个发送消息的过程,但是当对象无法接收相关消息时又会发生什么?以及前文说的三次机会又是什么?下文将会介绍消息转发。

消息转发: unrecognized selector的最后三次机会

还是那个栗子:

id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];

前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。

第一次机会: 所属类动态方法解析

首先,如果沿继承树没有搜索到相关方法则会向接收者所属的类进行一次请求,看是否能够动态的添加一个方法,注意这是一个类方法,因为是向接收者所属的类进行请求。

+(BOOL)resolveInstanceMethod:(SEL)name

举个栗子吧:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;
//如果需要传参直接在参数列表后面添加就好了
void dynamicAdditionMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamicAdditionMethodIMP");
}

+ (BOOL)resolveInstanceMethod:(SEL)name {
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
    if (name == @selector(appendString:)) {
        class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:name];
}

+ (BOOL)resolveClassMethod:(SEL)name {
    NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
    return [super resolveClassMethod:name];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id p = [[Person alloc] init];
        [p appendString:@""];
    }
    return 0;
}

先看一下最后的输出结果吧:

2017-03-24 19:05:25.092404 OCTest[5142:1185077] resolveInstanceMethod: appendString:
2017-03-24 19:05:25.092810 OCTest[5142:1185077] dynamicAdditionMethodIMP

先看一下main函数,首先创建了一个Person的实例对象,一定要用id类型来声明,否则会在编译期就报错,因为找不到相关函数的声明,id类型由于可以指向任何类型的对象,因此编译时能够找到NSString类的相关方法声明就不会报错。
由于Person类没有声明和定义appendString:方法,所以运行时应该会报unrecognized selector错误,但是并没有,因为我们重写了类方法+ (BOOL)resolveInstanceMethod:(SEL)name,当找不到相关实例方法的时候就会调用该类方法去询问是否可以动态添加,如果返回True就会再次执行相关方法,接下来看一下如何给一个类动态添加一个方法,那就是调用runtime库中的class_addMethod方法,该方法的原型是

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

通过参数名可以看出第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。具体设置方法可以看注释。

第二次机会: 备援接收者

当对象所属类不能动态添加方法后,runtime就会询问当前的接受者是否有其他对象可以处理这个未知的selector,相关方法声明如下:

- (id)forwardingTargetForSelector:(SEL)aSelector;

该方法的参数就是那个未知的selector,这是一个实例方法,因为是询问该实例对象是否有其他实例对象可以接收这个未知的selector,如果没有就返回nil,可以自行实验。

第三次机会: 消息重定向

当没有备援接收者时,就只剩下最后一次机会,那就是消息重定向。这个时候runtime会将未知消息的所有细节都封装为NSInvocation对象,然后调用下述方法:

- (void)forwardInvocation: (NSInvocation*)invocation;

调用这个方法如果不能处理就会调用父类的相关方法,一直到NSObject的这个方法,如果NSObject都无法处理就会调用doesNotRecognizeSelector:方法抛出异常。

整个消息转发流程如下图所示:


消息转发流程

总结

本文通过对runtime的分析,详细解释了整个发送消息和消息转发的流程,对OC的runtime能有一个更清晰的掌握。


关联对象 Associated Object

如果我们想为系统的类添加一个方法可以采用类别的方式进行扩展,相对来说比较简单,但如果要添加一个属性或称为成员变量,通常采用的方法就是继承,这样就比较繁琐了,如果不想去继承那就可以通过runtime来进行关联对象操作。

使用runtime关联对象添加属性与我们自定义类时定义的属性其实是两个不同的概念,通过关联对象添加属性本质上是使用类别进行扩展,通过添加settergetter方法从而在访问时可以使用点语法进行方法,在使用上与自定义类定义的属性没有区别。

具体需要使用的C函数如下:

//为一个实例对象添加一个关联对象,由于是C函数只能使用C字符串,这个key就是关联对象的名称,value为具体的关联对象的值,policy为关联对象策略,与我们自定义属性时设置的修饰符类似
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通过key和实例对象获取关联对象的值
id objc_getAssociatedObject(id object, const void *key);
//删除实例对象的关联对象
void objc_removeAssociatedObjects(id object);

通过注释和函数名不难发现上诉三个方法分别是设置关联对象、获取关联对象和删除关联对象。

需要说明一下objc_AssociationPolicy,具体的定义如下:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

这些关键词很眼熟,没错,就是property使用的修饰符,具体含义也与property修饰符相同,如果对propertyproperty修饰符等有疑问可以查阅本系列教程第三篇文章从runtime开始: 理解OC的属性property或本博客另外两篇关于property的讲解文章:iOS @property探究(一): 基础详解iOS @property探究(二): 深入理解

说了这么多,接下来举个具体的栗子,为一个已有类添加一个关联对象。

@interface Person : NSObject

@property (nonatomic, copy) NSString* cjmName;
@property (nonatomic, assign) NSUInteger cjmAge;

@end

@implementation Person

@synthesize cjmName = _cjmName;
@synthesize cjmAge = _cjmAge;

@end

@interface NSArray (MyPerson)

- (void)setPerson:(Person*)person;
- (Person*)person;

@end

@implementation NSArray (MyPerson)

- (void)setPerson:(Person *)person {
    objc_setAssociatedObject(self, "_person", person, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (Person*)person {
    return objc_getAssociatedObject(self, "_person");
}
@end

这个栗子设置的关联对象其实没有任何实际意义,通过代码可以看出,使用runtime为一个已有类添加属性就是通过类别扩展gettersetter方法。

实例方法

在本系列文章的第二篇iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制,我们详细介绍了runtime对方法的底层处理,以及发送消息和消息转发机制,这里就不再赘述了,如有需要可以查看相关文章,本文会介绍OC层面对方法的相关操作,同时会介绍method swizzling的方法。

先来回顾一下实例方法相关的结构体和底层实现,有如下代码:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;

- (void)showMyself;

- (void)helloWorld;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
    if (self = [super init]) {
        self.name = name;
        self.age = age;
    }
    return self;
}

- (void)showMyself {
    NSLog(@"Hello World, My name is %@ I\'m %ld years old.", self.name, self.age);
}

- (void)helloWorld {
    NSLog(@"Hello World");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
        [p showMyself];
        unsigned int count = 0;
        Method *methodList = class_copyMethodList([p class], &count);
        for (int i = 0; i < count; i++) {
            SEL s = method_getName(methodList[i]);
            NSLog(@"%@", NSStringFromSelector(s));
            if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
                IMP imp = method_getImplementation(methodList[i]);
                imp();
            }
        }
    }
    return 0;
}

通过clang转写后可以找到如下与实例方法相关的定义:

struct _objc_method {
        struct objc_selector * _cmd;
        const char *method_type;
        void  *_imp;
};

static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[7];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        7,
        {{(struct objc_selector *)"initWithName:age:", "@32@0:8@16Q24", (void *)_I_Person_initWithName_age_},
        {(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
        {(struct objc_selector *)"helloWorld", "v16@0:8", (void *)_I_Person_helloWorld},
        {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
        {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
        {(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
        {(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};

上一篇文章iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制已经详细介绍了上述结构体,这里不再赘述了。

通过上述代码可以看出,一个实例方法在底层就是一个方法描述和一个C函数的具体实现,我们可以通过如下代码获取这个方法描述结构体:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
        unsigned int count = 0;
        Method *methodList = class_copyMethodList([p class], &count);
        for (int i = 0; i < count; i++) {
            SEL s = method_getName(methodList[i]);
            NSLog(@"%@ %s", NSStringFromSelector(s), method_getTypeEncoding(methodList[i]));
            if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
                IMP imp = method_getImplementation(methodList[i]);
                imp();
            }
        }
    }
    return 0;
}

首先看一下Method是什么,在objc/runtime.h中可以找到相关定义:

typedef struct objc_method *Method;

它是一个指向结构体struct objc_method的指针,这里的结构体struct objc_method其实就是前文中.cpp文件中的struct _objc_method结构体,通过class_copyMethodList方法就可以获取到相关类的所有实例方法,具体函数声明如下:

/** 
 * Describes the instance methods implemented by a class.
 * 
 * @param cls The class you want to inspect.
 * @param outCount On return, contains the length of the returned array. 
 *  If outCount is NULL, the length is not returned.
 * 
 * @return An array of pointers of type Method describing the instance methods 
 *  implemented by the class—any instance methods implemented by superclasses are not included. 
 *  The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
 * 
 *  If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
 * 
 * @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
 * @note To get the implementations of methods that may be implemented by superclasses, 
 *  use \c class_getInstanceMethod or \c class_getClassMethod.
 */
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

通过注释可以看出,第一个参数是相关类的类对象(如有疑问可以查阅本系列文章的前两篇文章),第二个参数是一个指向unsigned int的指针,用于指明Method的数量,通过该方法就能够获取到所有的实例方法,接下来可以通过method_getName方法获取成员变量_cmd,这是一个选择子selector可以通过方法NSStringFromSelector获取到实例方法的名称。通过方法method_getTypeEncoding就可以获得函数类型method_type。通过方法method_getImplementation就可以获取到实例方法的具体实现imp,这个具体实现就是我们自定义的实例方法的一个C函数,因此,如果该方法内不访问任何其他实例变量并且没有任何参数就可以直接执行该函数。

上述代码的输出结果如下:

2017-03-27 12:36:12.342715 OCTest[4135:952839] initWithName:age: @32@0:8@16Q24
2017-03-27 12:36:12.342795 OCTest[4135:952839] showMyself v16@0:8
2017-03-27 12:36:12.342843 OCTest[4135:952839] helloWorld v16@0:8
2017-03-27 12:36:12.342866 OCTest[4135:952839] Hello World
2017-03-27 12:36:12.342884 OCTest[4135:952839] .cxx_destruct v16@0:8
2017-03-27 12:36:12.342911 OCTest[4135:952839] name @16@0:8
2017-03-27 12:36:12.342929 OCTest[4135:952839] setName: v24@0:8@16
2017-03-27 12:36:12.342951 OCTest[4135:952839] age Q16@0:8
2017-03-27 12:36:12.342966 OCTest[4135:952839] setAge: v24@0:8Q16

我们也可以通过class_addMethod函数动态的为一个类添加实例方法,具体的栗子可以查看前文从runtime开始: 深入理解OC消息转发机制这里不再赘述。

Method Swizzling

通过前面的介绍,我们知道一个实例方法在底层就是一个方法描述加上方法类型和具体的C函数实现,Foundation等框架都是闭源的,我们没有办法直接修改代码,通常情况下可以通过继承、类别、关联属性等手段添加属性或实例方法,在某些情况下通过上述方法实现的代码还是比较复杂或繁琐。接下来本文将介绍一种方法用于交换两个实例方法的实现,从而达到修改闭源代码的效果,这个方法就是Method Swizzling

Method Swizzling方法的本质就是修改前文介绍的方法描述结构体,方法描述结构体struct _objc_method中有一个struct objc_selector类型的成员变量_cmd,这就是我们常用的selector选择子,同时也有一个函数指针_imp,这个函数指针就指向实例方法的具体实现。了解了这些我们就可以手动修改selector对应的_imp,也就是修改实例方法的具体实现,下面举个栗子:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
        Method method1 = class_getInstanceMethod([p class], @selector(helloWorld));
        Method method2 = class_getInstanceMethod([p class], @selector(showMyself));
        method_exchangeImplementations(method1, method2);
        
        [p showMyself];
        [p helloWorld];
    }
    return 0;
}

上述代码使用了一个C函数:

/** 
 * Exchanges the implementations of two methods.
 * 
 * @param m1 Method to exchange with second method.
 * @param m2 Method to exchange with first method.
 * 
 * @note This is an atomic version of the following:
 *  \code 
 *  IMP imp1 = method_getImplementation(m1);
 *  IMP imp2 = method_getImplementation(m2);
 *  method_setImplementation(m1, imp2);
 *  method_setImplementation(m2, imp1);
 *  \endcode
 */
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

通过注释和函数名称不难发现,该函数用于交换两个方法的实现,也就是说前文讲述的结构体struct _objc_method中的函数指针_imp被交换了,原来的选择子@selector(helloWorld)对应着方法helloWorld的实现,原来的选择子@selector(showMyself)对应着方法showMyself的实现。如下图所示:

交换前

通过上述方法将两个结构体的_imp成员变量进行了一次交换操作,也就是说选择子@selector(helloWorld)对应着方法showMyself的实现,而选择子@selector(showMyself)对应着方法helloWorld的实现,如下图所示:

交换后

因此上述代码的输出结果如下:

2017-03-27 15:35:54.077598 OCTest[6061:1472928] Hello World
2017-03-27 15:35:54.077853 OCTest[6061:1472928] Hello World, My name is Jiaming Chen I'm 22 years old.

runtime强大到可以改变一个实例方法的具体实现,但是上面的例子好像并没有什么用,没有人会闲的没事去交换两个实例方法的实现。

考虑一个需求,现在需要为每一个页面添加一个手势用于执行某项固定操作,比如添加一个长按收拾,用户可以在任意界面长按后弹出一个视图或是执行某项操作,又比如需要统计每个视图打开的次数,你可能会想到在每一个的视图控制器的viewDidLoad方法中添加这个手势或在viewDidAppear方法中进行统计操作,但是这样太繁琐了。你也可能想到通过继承来实现上述方法,但是你就需要继承UIViewControllerUITableViewControllerUINavigationController等,你在代码中使用过的任意视图控制器,这样一看似乎也挺麻烦的而且代码也不统一。
通过前面的学习我们可以通过使用类别加上Method Swizzling来实现在不修改使用方式的前提下执行自定义操作了。

具体栗子如下:

@interface UIViewController (MyUIViewController)

@end

@implementation UIViewController(MyUIViewController)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewWillAppear:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        
        SEL exchangeSelector = @selector(myViewWillAppear:);
        Method exchangeMethod = class_getInstanceMethod([self class], exchangeSelector);
        
        method_exchangeImplementations(originalMethod, exchangeMethod);
    });
}

- (void)myViewWillAppear:(BOOL)animated {
    [self myViewWillAppear:animated];
    NSLog(@"MyViewWillAppear %@", self);
}

@end

首先需要使用类方法load来进行实例方法实现的交换操作,因为load方法会保证在类第一次被加载的时候调用,这样可以保证一定会执行方法交换操作。其次使用GCDdispatch_once来保证交换两个实例方法的实现只进行一次。接下来通过前文介绍的方法来获取自定义的myViewWillAppear:以及UIViewController的选择子和具体的方法描述结构体,最后调用前文介绍的method_exchangeImplementations函数将两个实例方法的实现进行交换就可以了。
可能你看到myViewWillAppear:方法会有疑问,这样不就会导致递归调用吗?需要注意的是,交换两个方法的实现是在运行时进行的,当你调用myViewWillAppear:方法时,实际会执行viewWillAppear:的方法实现,因此不会导致递归调用。


weak

weak不论是用作property修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak修饰的属性或变量均会被设置为nil,这样可以防止野指针错误,本文要讲解的也正是这个特性,runtime如何将weak修饰的变量的对象在销毁时自动置为nil

那么runtime是如何实现在weak修饰的变量的对象在被销毁时自动置为nil的呢?一个普遍的解释是:runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

了解了以上知识后就可以深入runtiem代码来看看具体实现细节,有兴趣的读者可以继续阅读。

深入runtime理解weak

这部分内容参考《Objective-C高级编程:iOS与OS X多线程和内存管理》,可以看出具体的实现方式就是使用了一个HashTable

NSString *name = [[NSString alloc] initWithString: @"Jiaming Chen"];
__weak NSString *weakStr = name;

当为weakStr这一weak类型的对象赋值时,编译器会根据name的地址为key去查找weak哈希表,该表项的值为一个数组,将weakStr对象的地址加入到数组中,当name变量超出变量作用域或引用计数为0时,会执行dealloc函数,在执行该函数时,编译器会以name变量的地址去查找weak哈希表的值,并将数组里所有 weak对象全部赋值为nil。





  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值