runtime的底层原理和使用

先来了解一下isa的组成

我们去这个网站(https://opensource.apple.com/tarballs/objc4/)搜索objc4,然后下载最新的压缩文件,这个就是苹果开源的部分的底层代码(所以我们不能说苹果是完全闭元的),如图所示:

image.png

解压 打开工程搜索isa_t 如图:
image.png

可以看到如下的结构,代码为:

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if SUPPORT_PACKED_ISA
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
};

其中我只分析arm64的 x86_64的是模拟器 或者mac的 我们不分析了 ,其中共用体里面有一个结构体,可以看到 结构体中加起来正好是64 也就说 我们的一个对象的isa中存放这些东西以及他们的内存分配情况,我们知道这个结构体是用来更高好的做解读说明的,也就是isa中存放这些东西 ,那么他的每一个东西都是干什么用的呢 我会具体的解释每一个的作用

        uintptr_t nonpointer        : 1;// 存储着class meta-class的对象的内存地址,0 :代表普通 1:代表优化过的。
        uintptr_t has_assoc         : 1;// 是否有没有设置过关联对象 ,如果没有设置过关联对象 释放的就会更快。(0:代表没有1:代表有)
        uintptr_t has_cxx_dtor      : 1;// 是否有c++的析构函数,如果没有释放的更快。(0:代表没有1:代表有)
        uintptr_t shiftcls          : 33; // 这33为 存储的是对象的内存地址信息。
        uintptr_t magic             : 6; // 这6为用于调试时是否为完成初始化。 
        uintptr_t weakly_referenced : 1;// 是否是为被弱引用指向过
        uintptr_t deallocating      : 1; // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;// 里面引用计数是否过大无法存储在isa中,如果为1 则证明过大,那么引用计数会存在SideTable的类中
        uintptr_t extra_rc          : 19;// 存储的是引用计数器减1

为了验证我的结论,我拿出一个例子来进行验证
代码1为:

 DGPerson *person = [[DGPerson alloc] init];
  NSLog(@"-------------");

打印person的内存地址


image.png

将我们的内存地址放到我们的系统自带的计算器中 可以看到


image.png

没有设置关联对象的这个位置是0,接下来设置一下关联对象
代码2为:
DGPerson *person = [[DGPerson alloc] init];
    objc_setAssociatedObject(person, @"nameKey", @"asdasdasdasd", OBJC_ASSOCIATION_COPY_NONATOMIC);

可以看到 第二位为1了


image.png

其他的不一一验证了

了解一下class的组成以及每一部分的作用

  • 大家我们上面所说的底层的代码。搜索objc_class,如图所示,找到这个文件objc-runtime-new.h文件(运行时的文件)


    image.png

    经过精简:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;  // 用于指向父类的指针
    cache_t cache;             // 缓存方法,为了下次快速查找
    class_data_bits_t bits; // 用于获取具体的类信息
}

其中superclass 是如果在当前的类对象中找不到就通过superclass到父类中去查找。
cache_t的结构可以看到为:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
struct bucket_t {
    cache_key_t _key;
    IMP _imp;
}

其中_buckets存储的是一个个的bucket_t,而_mask是散列表的长度-1,(比如散列表的长度是10,那么他就是9)_occupied是已经缓存的方法的个数。cache_t 是通过散列表(哈希表)的方式进行缓存的,这样的做的目的是更加快速找到找到我们曾经缓存的方法。bucket_t中存在一个key和imp,其中SEL作为key,而imp为方法的地址 。比如我们下面的代码:

   DGPerson *person = [[DGPerson alloc] init];
   [person test];
   
   [person test];
   [person test];
   [person test];

这样的方法 在第一次person 第一次执行test方法的时候是按部就班的执行,先去当前类中查找,如果找不到就到父类中查找,以此类推。但是第二次在调用的时候就会就直接从缓存中查找了 那样的话查找的速度就更加的快了。

  • class_data_bits_t这个&上FAST_DATA_MASK就会获得这个class_rw_t,他的样子为:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

对其中重点的东西进行说明:methods 就是方法列表,properties属性列表,protocols协议列表。其中methods是一个二维的数组,methods存放的是method_list_t, method_list_t中存放的是method_t ,结构如图所示:


image.png

我们重点研究一下ro(只读)。可以看到class_ro_t的结构如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

其中他也有一个baseMethodList,他的baseMethodList存放的是method_t,其中class_ro_t中的methodlist 和class_rw_t中的methodlist有什么区别呢?可以说class_ro_t中的methodlist 是只读的,是类原始的方法等等,而class_rw_t中的methodlist是整个的比如后面分类中增加的方法都加入到这里来了,可以说class_rw_t中的methodlist大于等于class_ro_t中的methodlist的方法。

  • method_t的结构以及解释:
struct method_t {
    SEL name; // 函数名字
    const char *types; // 编码(返回值类型、参数类型)
    IMP imp;// 指向函数的指针(函数的地址)
};

解释:
1.其中imp是指向函数的指针,代表着具体函数的实现。
2.SEL是函数的方法选择器
可以通过一下几种方法生成

    SEL method1 = @selector(name);
    SEL method2 = sel_registerName("name");
    NSLog(@"method1 : %p -- method2:%p",method1,method2);

而且不同类中只要方法的名字相同生成的方法选择器是相同的,比如以上的打印:


image.png

当然你可以试试不同的类 我这里不试了 因为确实是这样的。
还可以通过以下的或者相应的字符串

    SEL method1 = @selector(name);
    SEL method2 = sel_registerName("name");
    NSString *methodName1 = NSStringFromSelector(method1);
    const char *methodName2 = sel_getName(method2);
    NSLog(@"methodName1 --- %@ //// methodName2 ---- %s",methodName1,methodName2);

可以看到打印的结果为:


image.png
  • types编码他的格式为:


    image.png

    下面我们通过代码看下person中的types的类型

    DGPerson *person = [[DGPerson alloc] init];
    DG_objc_class *personStruct = (__bridge DG_objc_class *)[DGPerson class];
    class_rw_t *rwStruct = personStruct->data();
      NSLog(@"---------");
其中person中的方法为:
      - (void)test;

其中DG_objc_class是c++的一个文件,相关的修改内容如下(就是一个.h文件,使用需要将你的controller改为controller.mm)

//
//  DGPersonInfo.h
//  verification_Isa
//
//  Created by apple on 2018/8/1.
//  Copyright © 2018年 apple. All rights reserved.
//
#import <Foundation/Foundation.h>

#ifndef DGPersonInfo_h
#define DGPersonInfo_h


# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

#else
#error unknown architecture
#endif

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    IMP imp(SEL selector)
    {
        mask_t begin = _mask & (long long)selector;
        mask_t i = begin;
        do {
            if (_buckets[i]._key == 0  ||  _buckets[i]._key == (long long)selector) {
                return _buckets[i]._imp;
            }
        } while ((i = cache_next(i, _mask)) != begin);
        return NULL;
    }
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct method_list_t : entsize_list_tt {
    method_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  // instance对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;  // 类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  // 成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC对象 */
struct DG_objc_object {
    void *isa;
};

/* 类对象 */
struct DG_objc_class : DG_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }
    
    DG_objc_class* metaClass() {
        return (DG_objc_class *)((long long)isa & ISA_MASK);
    }
};
#endif /* DGPersonInfo_h */

通过打印可以看到types为:v16@0:8
解释,其实test的真正为-(void)test:(id)self cmd:(SEL)cmd.
1.v:返回值为(void)
2.整个对象占用16个字节的空间
3.@:id
4.从第0位开始(id )我们知道占用8位
5.:返回值是SEL
6.8从第八位开始,占用8个字节。
假如我们修改test方法改为- (int)test:(NSString *)name,那么我们知道其实他的真正为:- (int)test:(id)self cmd:(SEL)cmd name:(NSString *)name,其中前两个参数是系统默认给我们加上去的。
那么我们看一下types是什么?


image.png

可以看到types为“i24@0:8@16”
解释:
1.i:返回的是int类型
2.24 共占用24个字节
3.@参数id类型
4.0:从第0个字节开始
5.:SEL参数类型
6.8从第8个字节开始(因为self是一个对象,所以占用8个字节)
7.@参数nsstring类型
8.16从第16个字节开始,那么他占用的是8个字节(24-16)
我在网上找了一篇文章,找到了type encoding的对应表


image.png
  • 重点研究一下cache_t 缓存的方法和散列表的算法原理。
    1.散列表的算法的大致说明:
    首先我们将一个方法添加进去缓存的数组,但是他怎么放的呢,也就说这个方法所在的数组的index是多少呢 ,它是@selector(方法名字)&MASK = index,开始的时候他会默认开启一个数组的空间,然后让第一次调用的方法缓存进去我们的数组中,依次类推。比如我们的开始数组的个数是10,但是我们缓存的方法是3个那么还有7个位置是空的 那么他就会将那些位置置为null,他下一个在一次调用同样的方法时候,他就会通过@selector(方法名字)&MASK = index 这样直接去拿数组的index,这样的操作虽然牺牲了一些空间 但是确实提高了很大的效率 不用便利查找了。但是存在
    问题1.有可能我们生成的下标是重复的。
    问题2.如果有一天我们的缓存的方法变成了20 那么我们之前的数组空间不够了怎么办。
    问题1解决:他每一次拿出来的东西会判断是否等于我们的方法名字,如果发现发现等于直接取出,如果不等于直接去前一个以此类推,如果发现第0个还不是 那就取数组最后一个 然后在往前找一次类推。当然存储的时候他会判断当前设置的下标所对应的对象是否有值,有值的话就会往前存储依次类推,如果发现第0个都有值 那么就设置设置最后一个值 依次类推。
    问题2解决:当有一天他发现数组的空间小于要缓存的方法的个数,那么他会重新计算分配数组空间 以及重新缓存方法。
  • 我们具体查看一下缓存的方法 以及验证我以上说的结论
    先说名cache_t的结构如下:
struct cache_t {
    struct bucket_t *_buckets; // 方法的key和imp
    mask_t _mask;// mask做&操作
    mask_t _occupied;//已经缓存的方法的个数
}
struct bucket_t {
    cache_key_t _key;
    IMP _imp;
}

代码中全部都是继承关系,我们看下具体执行的结果

        DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        [chinesePerson personTest];
        NSLog(@"---------------");

我们分别在每一个方法执行的地方打上一个断点,查看效果


image.png

image.png

image.png

image.png

可以看到 开始分配的数组的个数是4(mask+1),mask是3 当到第一个方法的时候occupied为1,因为此时执行了alloc方法,到最后一个方法执行的时候数组重新分配变成了8((mash= k)+1),可以验证我以上的结论了。
下面我们来看下具体的缓存的方法,我循环便利打印出来
打印的代码为:

        DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        cache_t cacheMethods = chinesePersonStruct->cache;
        
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        [chinesePerson personTest];
        NSLog(@"---------------");
        bucket_t *bucketLists = cacheMethods._buckets;
        for (int index = 0; index <= cacheMethods._mask; index++) {
            bucket_t bucket = bucketLists[index];
            NSLog(@"_key : %s --- _imp : %p",bucket._key,bucket._imp);
        }
image.png

可以看到我们缓存中并没有看到persontest这个方法 其实这不是结论错误是因为我们获取的地方不对,假如代码这样修改为:


image.png

可以看到结果为:


image.png

由此可见以上所说的结论是毫无问题的
下面我们执行这段代码:
       DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        cache_t cacheMethods = chinesePersonStruct->cache;
        
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        NSLog(@"---------------");
        bucket_t *bucketLists = cacheMethods._buckets;
        bucket_t testBucket = bucketLists[(long long)@selector(DGChinesePersonTest) & cacheMethods._mask];
        NSLog(@"_key : %s --- _imp : %p",testBucket._key,testBucket._imp);

打印结果是:


image.png

可以看到真是我们的的方法名字(但是这样打印是不准确的,应该明白只不过是赶巧而已)
我们可以将以上程序修改为,这样打印的一定是准确的

        DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        cache_t cacheMethods = chinesePersonStruct->cache;
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        NSLog(@"---------------");
        IMP methodImp = cacheMethods.imp(@selector(DGChinesePersonTest));
        NSLog(@"methodImp -- %p",methodImp);
        NSLog(@"+++++++++++++");
image.png

为什么是准确的,因为我的头文件中这样书写


image.png

相当于按照散列表的算法进行书写,这样拿到的一定不为null

进入正题 消息机制

  • ios的消息机制分为三个主要的步骤:
    1.消息发送
    2.消息方法的动态解析
    3.消息的转发
  • 消息发送:
    他的大致过程为:首先他会判断receiver是否为空,如果为空则直接返回。如果不为空到当前类的方法的缓存中去查找,如果找到了直接返回方法,如果没有找到到当前类的方法中继续查找,如果找到了返回方法并且存储在当前类的缓存方法中。如果没有找到通过superclass指针到当前类的父类中中的缓存方法中去查找,如果找到了方法那么返回方法并且缓存进当前类对象的缓存方法中。如果没有找到找当前类对象的class_rw_t的方法列表中查找,找到了缓存进方法列表中并且返回方法。如果没有找到继续通过superclass的指针到其父类的父类中查找,依次类推。
    用一张图形象的表示为:
    image.png

    其中如果找class_rw_t的方法列表中存在的方法是有顺序的采用的是二分查找方法。如果不是有序的那么采用的是普通的便利查找
  • 如果以上都找不到方法,就会进入动态的方法的解析阶段,我们可以在此阶段动态的添加方法的实现。比如
    代码1:
person中添加如下方法,并没有添加实现
- (void)test;
我在person中添加另一个方法的实现比如
- (void)otherTest{
    
    NSLog(@"------------");
    
}
我想动态的添加方法的实现可以如下操作:
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(test)) {
        Method method = class_getInstanceMethod(self, @selector(otherTest));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
   return [super resolveInstanceMethod:sel];
}

其中resolveInstanceMethod这个方法是在消息发送的过程中找不到当前方法的实现才会调用这个方法,来动态的找方法的实现。
代码二:也可以通过这种方法来实现:

void otherTest(id self,SEL _cmd){
    
    NSLog(@"------------");
    
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(test)) {
        class_addMethod(self, sel, (IMP)otherTest, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

当然如果是类对象,那么就需要实现这个方法

void otherTest(id self,SEL _cmd){
    
    NSLog(@"------------");
    
}
+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(test)) {
        class_addMethod(object_getClass(self), sel, (IMP)(otherTest), "v@:");
    }
    return [super resolveClassMethod:sel];
    
}

需要注意两点:
1.实例方法实现的是实例方法,类方法实现的是类方法。
2.还有其中参数传递的时候是object_getClass(self) 也就是他的原类的对象。

消息的转发

当消息的发送和消息的动态的方法的实现都找不到方法的时候也就是进入到了消息的转发的阶段
他会实现这个方法:

-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
        return [[DGStudent alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
其中DGStudent的内部实现了test方法
@interface DGStudent : NSObject
- (void)test;
@end
@implementation DGStudent
- (void)test{
    NSLog(@"我的打印是 --- %s",__func__);
}
@end

假如这个方法不实现,他会调用哪个方法呢?他会实现这个方法,就是

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

这个方法是返回一个方法的types,要想实现该方法需要结合下面的方法一起实现
所以结合起来的是这样实现的:

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation invokeWithTarget:[[DGStudent alloc] init]];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
   return [super methodSignatureForSelector:aSelector];
}
总结:小的转发的流程是首先进入这个方法:forwardingTargetForSelector:(SEL)aSelector如果这个方法返回了那么就去返回方法的实现,如果返回为nil那么就去找这个方法- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector当然需要-(void)forwardInvocation:(NSInvocation *)anInvocation这个方法的配合实现。如果这个方法- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector也没有返回那么就去找这个方法doesNotRecognizeSelector的这个方法。也就是报错(方法找不到实现的)。

一个简单的流程图是这样的


image.png

也就是说消息的一个发送过程就是以上的步骤。

  • 消息转发的用处:
    我们知道当报方法找不到这个错误的话就会显示错误,也就是程序就会crash,为了降低crash的出错率 ,我简单写一个小的程序,比如person类,当出现了几个方法找不到的时候我们怎样能找到他
    代码如下:
#import "DGPerson.h"
#import <objc/runtime.h>
@implementation DGPerson

- (void)test{
    NSLog(@"%s",__func__);
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([self respondsToSelector:aSelector]) {
        
        return [NSMethodSignature methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%@ -- 哪个方法 %@ 没有实现",anInvocation.target,NSStringFromSelector(anInvocation.selector));
    
}
@end

#import <Foundation/Foundation.h>

@interface DGPerson : NSObject
-(void)test;
-(void)run;
-(void)eat;
@end

当我们调用这三个方法的时候,他会打印如下的方法


image.png

super的理解

看看下面的打印,

#import "DGStudent.h"

@implementation DGStudent

-(void)handleAction{
    [super handleAction];
}
-(instancetype)init{
    if (self = [super init]) {
        
        NSLog(@"[self class] = %@",[self class]);
        NSLog(@"[self superclass] = %@",[self superclass]);
        
        NSLog(@"-------------------------");
        
        NSLog(@"[super class] = %@",[super class]);
        NSLog(@"[super superclass] = %@",[super superclass]);
    }
    return self;
}
@end
其中的继承的关系为 student继承person,person继承object

打印的结果为:


image.png

其中第一和第二都能很简单的知道为什么,但是第三和第四是为什么
按照我的个人的理解应该是person 和object但是为什么student 和person呢
先解释一下super,其中实现这样的方法

#import "DGStudent.h"
@implementation DGStudent
-(void)handleAction{
   [super handleAction];
}
end

看一下他的底层的代码的实现

static void _I_DGStudent_handleAction(DGStudent * self, SEL _cmd)
 {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super)
{      
        (id)self,
        (id)class_getSuperclass(objc_getClass("DGStudent"))
},
        sel_registerName("handleAction"));
}

相当于是一个结构体,我们知道student 那么就相当于这样写

static void _I_DGStudent_handleAction(DGStudent * self, SEL _cmd)
 {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super)
{      
        (id)self,
        DGPerson
},
        handleAction方法
}

我们看下__rw_objc_super这个结构体里面是怎样组成的(到我们的底层代码库中查找发现)


image.png

整理得到:

struct objc_super {
   __unsafe_unretained _Nonnull id receiver;
   __unsafe_unretained _Nonnull Class super_class;
}

在看一下我们的handleAction的底层实现,发现他的receiver我们传递的是self而super_class我们传递的是DGPerson,在看一下底层的这方法的objc_msgSendSuper的实现原理


image.png

通过注释中可以发现,他的super的实现是从当前类中的父类开始查找方法的实现,但是对象传递的的接受者还是传递的当前的对象。
下面回到当前的问题 为什么[super class] 打印的是student 按照刚才的分析不应该是person吗 ?但是我们还是忽略了class的这个方法,因为class的这个方法是nsobject的方法 他的伪代码大致为:

-(Class)class{
    return object_getClass(self);
}

那就是说明返回对象本身
而superclass的伪代码的大致实现为:

-(Class)superClass {
   return class_getSuperclass(object_getClass(self));
}

那么他应该返回的就是person。

  • 继续对super进行深入的探究,刚刚我们探究得到super执行的是objc_msgSendSuper这个方法,但是我们看到我们的汇编执行的代码是


    image.png

    首先我们看下如下的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [DGPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];
    NSLog(@"---------------");
}

我们打印可以看出


image.png

可以看到我们按照8个字节往上找,我们找打了super viewdidload的底层实现,我们知道他的大概实现为:

 struct ss = {
        id self,
        [ViewController class]
    };
    objc_MegSendSuper(ss,sel_registerName("viewDidLoad"));
image.png

可以看到打印,但是为什么第三个就是呢?我大致画一个图分析下


image.png

所以我们获取第三个就是viewcontroller 那么说明super底层调用的就是super2的方法。(lldb中命令 x/4g的意思是16进制,打印4个8个字节的)

  • 了解的内容(llvm)
    我们oc的语言实际上是先转换成中间的语言(llvm)然后在转换成底层的汇编语言/机器语言,下面我们将我们写的代码转化成llvm的语言
    我们的代码为:
void test(int a){
    NSLog(@"%d",a);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        int b = 20;
        int c = a + b;
        test(c);
    }
    return 0;
}

我们通过这个命令进行转化:(切换到我们的main. m所在的上层文件夹)

clang -emit-llvm -S main.m

可以看到如下代码:


image.png

image.png

isMemberOfClass和isKindOfClass的区别(顺便一讲,已经熟悉的请滤过,本人只是为了做笔记)

  • 实力对象开始调用的时候
    比如我们看下这个例子:
        DGPerson *person = [[DGPerson alloc] init];
        NSLog(@"%d",[person isMemberOfClass:[DGPerson class]]);
        NSLog(@"%d",[person isMemberOfClass:[NSObject class]]);
        NSLog(@"%d",[person isKindOfClass:[DGPerson class]]);
        NSLog(@"%d",[person isKindOfClass:[NSObject class]]);

可以看到打印的结果


image.png

为了弄清除这个问题我们需要看下底层代码的实现:

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

因为我们现在调用的是实例对象,所以我们看下减号的方法,可以看到- (BOOL)isMemberOfClass:(Class)cls拿到的是[self class]进行比较,也就是我们实力对象调用isMemberOfClass比较的是当前对象的类对象,如果一致返回的就是yes否则是no,而isKindOfClass通过当前类对象以及当前类对象的父类对象进行比较发现是当前的类对象或者当前类对象的父类的类对象那么就返回yes,否则返回no。
所以以上打印的结果是1 0 1 1
下面我将程序修改为:

        NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]);
        NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]);
        NSLog(@"%d", [[DGPerson class] isKindOfClass:[DGPerson class]]);
        NSLog(@"%d", [[DGPerson class] isMemberOfClass:[DGPerson class]]);

可以看到运行的结果为:


image.png

先看下我们的isKindOfClass的类对象的调用方法,可以看到他拿到的是当前对象的原类对象进行判断的,判断我们传递的对象的原类的对象,已经原类对象的父类的原类对象,如果发现相等就返回yes 否则返回no,而isMemberOfClass是判断当前对象的原类对象是不是和我们传递进来的对象是否相等,由此可以分析出isMemberOfClass必定都是no,而isKindOfClass当我们调用这句代码的时候( [[DGPerson class] isKindOfClass:[DGPerson class]]);)应该也是返回no,因为我们知道他应该等于他的原类对象,但是我们判断[[NSObject class] isKindOfClass:[NSObject class]]);为什么就是yes了 因为isKindOfClass他会去父类中查找一直找找到nsobject的原类的时候他就会去找类对象 类对象就是nsobject,我们以前说过这样的一幅图


image.png

可以看到以上所说,那么现在也就是说任何继承nsobject对象的类对象调用isKindOfClass如果我们传递的是[NSObject class]那么都应该返回的是yes,比如
NSLog(@"%d", [[DGPerson class] isKindOfClass:[NSObject class]]);
image.png

runtime的运用(个人觉得很重要)

    1. 类的相关的api
      1.1 object_setClass :切换isa的指向
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
    // 将person的isa指针指向DGCar
    object_setClass(person, [DGCar class]);
    [person test];
image.png

1.2 object_getClass:获取isa所指向的类
解释:
如果传递的是实例对象那么获取的是类对象。
如果传递的是类对象那么获取的是原类对象。
1.3 object_isClass 判断的是否是一个类对象:

    DGPerson *person = [[DGPerson alloc] init];
    NSLog(@"%d - %d - %d", object_isClass([DGPerson class]),object_isClass(object_getClass([DGPerson class])),object_isClass(person));
image.png

解释:值得说明的是原类对象也是特殊的类对象,所以第二个打印的是1
1.4 class_isMetaClass 判断是不是原类的对象,不再举例子 。
1.5 class_getSuperclass()获取父类,太简单不在举例子。
1.6 动态创建一个类,以及注册一个类 一般的情况下他们是组合一起使用的(其中也包括动态的添加方法和属性等)
代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];  
    //创建一个类
    Class animalClass = objc_allocateClassPair([NSObject class], "DGAnimal", 0);
    // 添加属性(animalClass:类名 4:int占的字节数 1:内存对齐默认为1 "v@:":types)
    class_addIvar(animalClass, "_age", 4, 1, @encode(int));
    // 添加方法
    class_addMethod(animalClass, @selector(test), (IMP)otherTest, "v@:");
    // 注册一个类 ,添加方法等等的 要在注册类的前面执行最好
    objc_registerClassPair(animalClass);
    
    // 开始使用(必须还要alloc,否则失败)
    id animal = [[animalClass alloc] init];
    [animal setValue:@10 forKey:@"_age"];
    
    NSLog(@"age = %@",[animal valueForKey:@"_age"]);
    [animal test];
    
}
void otherTest(){
    
    NSLog(@"老夫打印了哈");
}
// 值得非常注意的是,在这个类不用的时候需要释放因为是c语言的 ,所以必须要释放
// 不在用到这个类的时候需要释放
    objc_disposeClassPair(animalClass);
image.png
  • 2 成员变量相关的信息
    2.1查询成员变量的信息(获取当前类的成员变量)
// 获取成员变量
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([UITextField class], &count);
    for (int index = 0; index < count; index++) {
        Ivar ivar = ivarList[index];
        NSString *name = [NSString stringWithCString: ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        NSLog(@"%@",name);
        
    }
    free(ivarList);
image.png

2.2 获取一个成员变量

 // 获取一个对象的一个成员变量
    Ivar ivar = class_getInstanceVariable([DGPerson class], "_name");
    NSString *nameStr = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
    NSString *typeStr = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
    NSLog(@"nameStr --- %@  type---%@",nameStr,typeStr);
image.png

2.3设置和获取成员变量的值

 // 设置一个ivar
    DGPerson *person = [[DGPerson alloc] init];
    Ivar nameIvar = class_getInstanceVariable([DGPerson class], "_name");
    Ivar ageIvar = class_getInstanceVariable([DGPerson class], "_age");
    object_setIvar(person, nameIvar, @"ahshdahshdahsd");
    object_setIvar(person, ageIvar, (__bridge id)(void *)10);
    NSLog(@"name = %@ --- age = %d",person.name,person.age);
image.png
  • 3 属性相关的
    3.1 property_getName(获得一个类的属性)
    objc_property_t nameProperty = class_getProperty([DGPerson class], "name");
    NSString *nameStr = [NSString stringWithCString:property_getName(nameProperty)   encoding:NSUTF8StringEncoding];
    NSLog(@"nameStr -- %@",nameStr);
image.png

3.2class_copyPropertyList(获取属性列表)
与获取成员变量的方式一样 不在赘述。
3.3 class_addProperty(动态的添加成员变量)

//动态的添加属性
    objc_property_attribute_t type = {"T",[[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; // type 我这里是string类型
    objc_property_attribute_t copyShip = { "C",""}; // C = copy
    objc_property_attribute_t nonatomicAttr = {"N",""}; // N = nonatomic
    objc_property_attribute_t nameIvar = { "V",[[NSString stringWithFormat:@"_%@",@"hand"] UTF8String]};
    objc_property_attribute_t attrs[] = {type,copyShip,nonatomicAttr,nameIvar};
    class_addProperty([DGPerson class], [@"hand" UTF8String], attrs, 4);
    
    // 动态的获取属性
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([DGPerson class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSLog(@"属性 %s ======= 特征 %s\n", property_getName(property), property_getAttributes(property));
    }

3.4 class_replaceProperty(动态的替换成员变量)
与3.3基本类似,不在赘述。
3.5. 获取属性的信息(property_getName(<#objc_property_t _Nonnull property#>) property_getAttributes(<#objc_property_t _Nonnull property#>))
在3.3使用过,不在赘述。
4 方法的相关的操作
4.1class_getClassMethod( 获取一个类方法)

 // 获取一个实例的方法
    Method testMethod = class_getClassMethod([DGPerson class], @selector(eat));
    NSString *methodName = NSStringFromSelector(method_getName(testMethod));
    NSLog(@"methodName -- %@",methodName);
image.png

4.2 class_getInstanceMethod (获取一个实例的对象)
与4.1类似
4.3class_getMethodImplementation(获取一个方法的实现)

 IMP eatImp = class_getMethodImplementation([DGPerson class], @selector(eat));

返回是IMP
4.4 method_setImplementation(设置一个方法的实现)

    // 设置一个方法的实现
    IMP carTestImp = class_getMethodImplementation([DGCar class], @selector(test));
    Method personTestMethod = class_getInstanceMethod([DGPerson class], @selector(test));
    method_setImplementation(personTestMethod, carTestImp);
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
image.png

4.5 替换方法的实现,这个很重要我们经常使用。

  // 替换方法的实现
    
    Method personTestMethod = class_getInstanceMethod([DGPerson class], @selector(test));
    Method carTestMethod = class_getInstanceMethod([DGCar class], @selector(test));
    method_exchangeImplementations(personTestMethod, carTestMethod);
    
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
image.png

4.6 class_copyMethodList (copy方法列表)
她的实现和我们获取成员变量的形式类似
4.7 class_addMethod(动态的添加方法 )和class_replaceMethod(动态的替换方法)
其中class_addMethod 我们在消息动态方法的实现,我们用过,class_replaceMethod与他差不多 只不过一个是添加 一个是替换
其中replace还可以这样用:

class_replaceMethod([DGPerson class], @selector(test), imp_implementationWithBlock(^{
        NSLog(@"123455666");
        
    }), "v@:");
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
image.png

4.8 一些方法的相关的属性


image.png

4.9 琐碎内容 了解即可


image.png

5 几个例子 (工作中用到的runtime的)
  • 1.字典转化模型(我们给nsobject添加一个分类)
    // 简单的实现,要做好了 其实要考虑的类型应该还有很多(参考MJExtension)
+ (instancetype)modelWithJson:(NSDictionary *)dic{
    id obj = [[self alloc] init];
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
     // 开始便利
    for (int index = 0; index < count; index ++) {
        Ivar ivar = ivarList[index];
        NSMutableString *nameStr = [[NSMutableString alloc] initWithString:[NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding]];
        [nameStr deleteCharactersInRange:NSMakeRange(0, 1)];
        if ([dic.allKeys containsObject:nameStr]) {
            
            if (dic[nameStr]) { // 不能设置空的值
             [obj setValue:dic[nameStr] forKey:nameStr];
            }
        }
    }
    // 释放
    free(ivarList);    
    return obj;
}
    1. 方法的替换,比如我们数组中添加nil的值就会crash
      我们可以添加给数组添加一个分类,防止插入空的值出现crash
      例如如下的代码:
    id obj = nil;
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:obj];
image.png

如果我们这样写一个分类

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Class cla = NSClassFromString(@"__NSArrayM"); // 这个要写类簇
        Method method1 = class_getInstanceMethod(cla, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cla, @selector(DG_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
        
    });
    
}
-(void)DG_insertObject:(id)anObject atIndex:(NSUInteger)index{
    
    if (!anObject) return;
    [self DG_insertObject:anObject atIndex:index];

}

看下运行的结果


image.png

可以看到不crash了 说明我们的方法替换成功了
其中有一个块值得说明


image.png

给人的感觉是死循环了 其实不是,我们知道method 其实就是我们的底层的method_t 他的结构包括
image.png

也就是他把imp这个实现给变化了 那么现在的方法指向为:


image.png

所以现在我们在调用-(void)DG_insertObject:(id)anObject atIndex:(NSUInteger)index正好调用的是系统的方法,所以不会出现死循环 反之调用系统的方法才会出现死循环。
  • 3.我们可以设置关联对象 objc_setAssociatedObject
    举个例子比如我们我们一个数组中包含很多的button,然后我们想做的事情是一个button对应一个model,其实有三个办法 ,第一我们打一个tag,第二写一个父类的button,给button增加一个属性,第三就是就是运用运行时动态的设置关联属性。
通过btn传递两个实例对象  firstObject和secondObject
UIButton *btn = // create the button
objc_setAssociatedObject(btn, "firstObject", someObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(btn, "secondObject", otherObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
- (void)click:(UIButton *)sender
{
    id first = objc_getAssociatedObject(btn, "firstObject");
    id second = objc_setAssociatedObject(btn, "secondObject");
    // etc.
}

  • 4.获取所有成员变量或者属性,这样的话我们就可以通过kvc来改变一些属性,比如我们uitextfield的placeholder的颜色,我们就可以通过运行时的方式进行修改。
  • 5 看看那些方法没有实现,(我们都知道如果方法没有实现那就crash),所以我们可以在消息转发的时候进行拦截,打印出来到时候上报我们的服务器
    比如我在上面说到的消息转发的用处。

总结:

我目前了解的runtime就这些,希望对大家有帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容