runtime 学习

2016-09-08  本文已影响35人  婉卿容若

学习资料

南峰子博客
Objective-C中的Runtime
runtime源码
onecat
详解Runtime运行时机制

补充
神经病院Objective-C Runtime入院第一天—isa和Class

说明

这篇文章只是留做自己以后回顾用,内容是参考上面几位大大的文章,然后根据自己的理解做了些整合以及思考.由于是初学,理解不到位,如有疑问可以评论,大家一起探讨.也可以通过上面链接进入大大的文章寻找答案.

初学

概念
源码

打开objc.xcodeproj项目,我们主要了解/Public Headers目录下的objc.hruntime.h两个文件


objc.h文件中:


typedef struct objc_class *Class;

Class是一个指向结构体objc_class的指针,这就是我们所说的类

typedef struct objc_object {
    Class isa;
} *id;

id是一个指向结构体objc_object的指针,这就是我们所说的对象
isa:是一个指向当前对象所属的类的指针,也就是上段代码的Class.它是每个对象结构体的首个成员,是个 Class类型的变量


runtime.h文件中:

我们首先看到objc_class结构体的实现:

struct objc_class {
    Class isa;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE; // 父类,,它定义了本类的超类.类对象所属类型(isa 指针指向的类型)是另一个类,叫做"元类"(metaClass)
    const char *name                                         OBJC2_UNAVAILABLE; // 类名
    long version                                             OBJC2_UNAVAILABLE; // 类的版本信息, 默认为0
    long info                                                OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
    long instance_size                                       OBJC2_UNAVAILABLE; // 该类的实例变量大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE; // 该类的而成员变量列表
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE; // 该类方法定义的列表  类的实例方法都在methodLists里,类方法在元类的methodLists里
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE; // 方法缓存列表
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE; // 协议列表
#endif

} OBJC2_UNAVAILABLE;
类与对象的继承关系(图片来自网络).png

看图说话:
上图中的层次关系主要是这样四条:
Instance of SubClass -> SubClass -> SubClass(meta)
Instance of SuperClass -> SuperClass -> SuperClass(meta)
Instance of RootClass -> RootClass -> RootClass(meta)
SubClass -> SuperClass -> RootClass

类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。

1.Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
2.isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass:方法来确定实例对象的类。因为KVO的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。


SEL 是selector在Objective-C中的表示类型。selector可以理解为区别方法的唯一标识.

typedef struct objc_selector    *SEL;

它是映射到方法的字符串, SEL 类型代表着方法的签名.在类对象的 methodLists 中存储着该签名与方法代码的对应关系,每个方法都有一个与之对应的 SEL 类型的对象,根据一个 SEL 对象就可以找到方法的地址,进而调用


Method 代表类中的某个方法的类型,在Runtime的头文件中的定义如下:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;  // 方法名
    char *method_types                                       OBJC2_UNAVAILABLE; // 方法类型,主要存储方法的参数类型和返回值类型
    IMP method_imp                                           OBJC2_UNAVAILABLE; // 方法的实现,函数指针.
}                                                            OBJC2_UNAVAILABLE;

获取某个类的成员方法列表

class_copyMethodList(Class cls, unsigned int *outCount) 

Ivar 代表类中实例变量的类型,在Runtime的头文件中的定义如下:

typedef struct objc_ivar *Ivar;
struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE; //  变量名
    char *ivar_type                                          OBJC2_UNAVAILABLE; // 变量类型
    int ivar_offset                                          OBJC2_UNAVAILABLE; // 基地址偏移字节
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE; // 占用空间
#endif
}            

获取某个类的成员变量列表

class_copyIvarList(Class cls, unsigned int *outCount)  

objc_property_t 是属性,在Runtime的头文件中的的定义如下:
获取某个类的属性列表

class_copyPropertyList(Class cls, unsigned int *outCount) 

IMP 是方法的实现,在Runtime的头文件中的的定义如下:

typedef id (*IMP)(id, SEL, ...);

IMP(implementation)是一个函数指针,它是由编译器生成的。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。


Cache 在Runtime的头文件中的的定义如下:

typedef struct objc_cache *Cache
struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE; // 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置
    unsigned int occupied               OBJC2_UNAVAILABLE; // 实际占用cache buckets的总数
    Method buckets[1]                   OBJC2_UNAVAILABLE; //指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
};

每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。


** Catagory** 在Runtime的头文件中的的定义如下:

typedef struct objc_category *Category;
struct objc_category {
    char *category_name                           OBJC2_UNAVAILABLE; // 类别名称
    char *class_name                              OBJC2_UNAVAILABLE; // 类名
    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 实例方法列表
    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 类方法列表
    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 协议列表
}

这就是我们平时使用的类别

** Objective-C的消息传递机制**

在面向对象的编程中,对象调用方法的过程就叫做发送消息.在编译时,程序的源代码就会从对象发送消息转换成 runtime 的 objc_msgSend 函数调用.

举个栗子:
某实例对象 person 调用唱歌方法 sing

[person sing];

runtime会将消息转成这样的代码

objc_msgSend(person,@selector(sing))

传递消息的几种函数:
objc_msgSend:普通的消息都会通过该函数发送。
objc_msgSend_stret:消息中有结构体作为返回值时,通过此函数发送和接收返回值。
objc_msgSend_fpret:消息中返回的是浮点数,可交由此函数处理。
objc_msgSendSuper:和objc_msgSend类似,这里把消息发送给超类。
objc_msgSendSuper_stret:和objc_msgSend_stret类似,这里把消息发送给超类。
objc_msgSendSuper_fpret:和objc_msgSend_fpret类似,这里把消息发送给超类。
编译器会根据情况选择一个函数来执行。

objc_msgSend发送消息的原理:

消息转发(message forwarding)

在上面第四步中,对象发出的消息无法解读,它就会将消息实施转发.转发的主要步骤函数如下:

// 第一步: 我们不动态添加方法,返回 NO, 进入第二步.....我们一般在声明了方法但没有实现的情况下,在这步解析中给对象添加方法

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO ;
}

// 第二步: 指定备选对象来响应 aSelector
// 当有备选对象响应,消息处理;没有则进入第三步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
//    // 情况一: 有备选备选响应
//    People *p = [[People alloc] init] ;
//    
//    return p ;
    
    // 情况二: 无备选对象响应
    return nil ;
}

// 第三步: 返回方法签名,进入第四步,,,如果返回 nil, 则消息无法处理

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    
    // 情况一 : 返回签名
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"] ;
    }
    
    return [super methodSignatureForSelector:aSelector] ;
//    
//    // 情况二 : 返回 nil
//    return  nil ;
}

// 第四步: 通过 aInvacation 对象做各种不同的处理,例如修改方法实现,修改响应对象等等

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//    // 1: we change the object which is called
//    People *p = [[People alloc] init] ;
//    p.name = @"hahhahahah" ;
//    [anInvocation invokeWithTarget:p] ;
    
    // 2: we change the impelement of the method
    [anInvocation setSelector:@selector(dance)] ;
    [anInvocation invokeWithTarget:self] ; // 这里我们还要指定哪个对象来实现....如果指定的方法是别的类的,对该类进行声明...我们这里用的是自己类的方法,所以用 self
   // [anInvocation invoke] ; // invoke 方法默认调用的对象是 self,与 [anInvocation invokeWithTarget:self] ; 作用相同
}


// 消息无法处理时进入这里,,如果没有实现这个方法则程序 crash
- (void) doesNotRecognizeSelector:(SEL)aSelector
{
 
    NSLog(@"没有找到该方法!!!!") ;
}
消息转发流程(图片来自网络).jpg

** 看图说消息转发步骤:**

结合上面的步骤函数看,相信你能有点初步的了解.

实战

说来说去,我们还是要运用到项目中才有用.我们学习前面的理论知识,我们自然会思考怎么运用到项目中?在哪里用?什么情况下用?
由于我也是初学,怎么运用都是从上面的几篇文章了解到的.现在写这个仍然是参考大大的文章,记录下来,只为以后用来回顾.自己写的符合自己的思路,以后看起来会好理解一点.同时,这也是看了好几篇文章总结的自己需要的东西.

下面是我回顾上面的理论,根据自己的理解,大致学到的怎么利用 runtime 知识进行实战编程.

1.动态创建类 + 动态添加方法

#pragma mark - 动态创建一个类

- (void) createPersonClass
{
    // 动态创建对象: 创建一个名为 Person 的类,它是 NSObject 的子类
    Class Person = objc_allocateClassPair([NSObject class], "Person", 0) ;
    
    
    /**
     *  为该类添加一个 eat 的方法 class_addMethod(Class cls, SEL name, IMP imp, const char *types)
     *
     *  @param cls    被添加方法的类
     *  @param name   可以理解为方法名,貌似可以随便起名
     *  @param imp    实现这个方法的函数
     *  @param types  一个定义该函数的返回值类型和参数类型的字符串
     *
     */
    // 关于参数二与参数三
    // 参数二是方法的声明,参数三是方法的实现,,实例对象调用方法(参数二)执行参数三里面的内容
    
    // 关于最后一个参数的解释
    // "v@:@"
    // v -> 表示 void, 如果是 i 则表示 int
    // @ -> 表示参数 id (self)
    // : -> 表示 SEL(_cmd)
    // @ -> 表示 id(str) ,当没有参数时不写,当多个参数是就写多个
    SEL eat = sel_registerName("eat:") ; // 注册 eat 方法
    class_addMethod(Person, eat, (IMP) eatFun, "v@:@") ;
    
    // 为该类添加成员变量
    class_addIvar(Person, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*)) ; // NSString
    class_addIvar(Person, "_age", sizeof(int), sizeof(int), @encode(int)) ; // int
    
    
    
    // 注册该类
    objc_registerClassPair(Person);
    
    // 创建一个实例对象
    id p = [[Person alloc] init] ;
    
    // KVC 动态改变对象的实例变量
    [p setValue:@"许嵩" forKey:@"name"] ;
    [p setValue:@29 forKey:@"age"] ;
    
    // 从类中获取成员变量并赋值
    Ivar ageIvar = class_getInstanceVariable(Person, "_age") ;
    object_setIvar(p, ageIvar, @30) ;
    
    // performSelector 是运行时负责去找方法的,在编译时不做任何校验
   //[p performSelector:@selector(eat)] ;
    
    // 调用 eat 方法,这么写好点(需要引入 <objc/message.h> )
    // 强制转换objc_msgSend函数类型为带三个参数且返回值为void函数,然后才能传三个参数
     ((void (*)(id, SEL, id))objc_msgSend)(p,eat, @"香蕉") ;
    
    p = nil ; // 当 Person 类或者它的子类的实例还存在,则不能调用 objc_disposeClassPair 这个方法.因此必须先销毁实例才能销毁类
    
    objc_disposeClassPair(Person) ; // 销毁类
}

2.消息转发时添加方法+改变对象+改变实现

#import "Bird.h"
#import "People.h"

@implementation Bird

// 第一步: 我们不动态添加方法,返回 NO, 进入第二步.....我们一般在声明了方法但没有实现的情况下,在这步解析中给对象添加方法

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO ;
}

// 第二步: 指定备选对象来响应 aSelector
// 当有备选对象响应,消息处理;没有则进入第三步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
//    // 情况一: 有备选备选响应
//    People *p = [[People alloc] init] ;
//    
//    return p ;
    
    // 情况二: 无备选对象响应
    return nil ;
}

// 第三步: 返回方法签名,进入第四步,,,如果返回 nil, 则消息无法处理

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    
    // 情况一 : 返回签名
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"] ;
    }
    
    return [super methodSignatureForSelector:aSelector] ;
//    
//    // 情况二 : 返回 nil
//    return  nil ;
}

// 第四步: 通过 aInvacation 对象做各种不同的处理,例如修改方法实现,修改响应对象等等

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//    // 1: we change the object which is called
//    People *p = [[People alloc] init] ;
//    p.name = @"hahhahahah" ;
//    [anInvocation invokeWithTarget:p] ;
    
    // 2: we change the impelement of the method
    [anInvocation setSelector:@selector(dance)] ;
    [anInvocation invokeWithTarget:self] ; // 这里我们还要指定哪个对象来实现....如果指定的方法是别的类的,对该类进行声明...我们这里用的是自己类的方法,所以用 self
   // [anInvocation invoke] ; // invoke 方法默认调用的对象是 self,与 [anInvocation invokeWithTarget:self] ; 作用相同
}


// 消息无法处理时进入这里,,如果没有实现这个方法则程序 crash
- (void) doesNotRecognizeSelector:(SEL)aSelector
{
 
    NSLog(@"没有找到该方法!!!!") ;
}

- (void)dance
{
    NSLog(@"你竟然跳舞了!!!") ;
}

@end

3. 关联对象
.h中:


#import "People.h"

typedef void (^CallBack)(); // 回调

@interface People (Associated)

@property (nonatomic,strong) NSString *newAblum ; // 新专辑
@property (nonatomic,copy) CallBack associatedCallBack ; // 回调

@end

.m中:


#import "People+Associated.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation People (Associated)

// 添加属性

- (void) setNewAblum:(NSString *)newAblum
{
    // 设置关联属性
    // 第一个入参: 关联对象
    // 第二个入参: key 值,唯一并且是常量(static char),,我们这里选择子作为 key
    // 第三个入参: 关联类型  OBJC_ASSOCIATION_RETAIN_NONATOMIC 与 (nonatomic,strong) 对应
    /*
     * OBJC_ASSOCIATION_ASSIGN = (assign) or (unsafe_unretained)
     * OBJC_ASSOCIATION_RETAIN_NONATOMIC = (nonatomic,strong)
     * OBJC_ASSOCIATION_COPY_NONATOMIC = (nonatomic,copy)
     * OBJC_ASSOCIATION_RETAIN = (atomic,strong)
     * OBJC_ASSOCIATION_COPY = (atomic,copy)
     */
    objc_setAssociatedObject(self, @selector(newAblum), newAblum, OBJC_ASSOCIATION_RETAIN_NONATOMIC) ;
}

- (NSString *)newAblum
{
    // get 关联对象
    return objc_getAssociatedObject(self, @selector(newAblum)) ;
}

// 添加回调 -- 实际开发过程中使用的更多

- (void) setAssociatedCallBack:(CallBack)associatedCallBack
{
    objc_setAssociatedObject(self, @selector(associatedCallBack), associatedCallBack, OBJC_ASSOCIATION_COPY_NONATOMIC) ;
}

- (CallBack)associatedCallBack
{
    return objc_getAssociatedObject(self, @selector(associatedCallBack)) ;
}


@end

添加属性没有什么意义,我们平时在开发过成功中用的比较多的就是添加回调了。

4. 归档

- (void)setIgnoredIvarNames:(NSArray *)ignoredIvarNames
{
    objc_setAssociatedObject(self, @selector(ignoredIvarNames),ignoredIvarNames, OBJC_ASSOCIATION_RETAIN_NONATOMIC) ;
}

- (NSArray *)ignoredIvarNames
{
    return  objc_getAssociatedObject(self, @selector(ignoredIvarNames)) ;
}

- (void)encode:(NSCoder *)aCoder
{
    unsigned int outCount = 0 ;
    Ivar *ivars = class_copyIvarList([self class], &outCount) ;
    for (unsigned int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i] ;
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
        if ([self.ignoredIvarNames containsObject:key]) {
            continue ;
        }
        
        id value = [self valueForKey:key] ;
        [aCoder encodeObject:value forKey:key] ;
    }
    
    free(ivars) ;
}

- (void)decode:(NSCoder *)aDecoder
{
    unsigned int outCount = 0 ;
    Ivar *ivars = class_copyIvarList([self class], &outCount) ;
    for (unsigned int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i] ;
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
        if ([self.ignoredIvarNames containsObject:key]) {
            continue ;
        }
        
        id value = [aDecoder decodeObjectForKey:key] ;
        [self setValue:value forKey:key] ;
    }
    
    free(ivars) ;

}

5. 方法交换

// 全局替换 UIViewController 的 dealloc 函数
// 在 load 函数中利用 runtime 交换两个方法的实现

+ (void)load
{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        Method nativeDealloc = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc")) ;
        Method myDealloc = class_getInstanceMethod(self, @selector(my_dealloc)) ;
        method_exchangeImplementations(nativeDealloc, myDealloc) ;
    }) ;
}

- (void)my_dealloc
{
    NSLog(@"%@销毁了",self) ;
    [self my_dealloc] ;
}

补充:
刚刚在这里看到一篇利用runtime进行万能界面跳转
万能界面跳转

最后

由于我个人水平有限,文中如果有错误的地方,希望指正,三颗柚~
demo下载链接

上一篇下一篇

猜你喜欢

热点阅读