iOS Runtime

https://opensource.apple.com/tarballs/objc4/

Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同

什么叫动态性呢?
一般程序的过程是“编写代码——————>编译———————>运行”,运行的结果就是编译的时候的结果。但是oc的动态性体现在能在运行的时候修改编译好的行为。

Objective-C的动态性是由Runtime API来支撑的
Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写

在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址

从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息

从64位开始,isa指针需要一次位运算 &ISA_MASK,才能计算出指向的真实地址

在xcode里直接点击Class可以查看结构是typedef struct objc_class *Class结构体,然后点击objc_class这个结构体,定义如下

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

但是,这个OBJC2_UNAVAILABLE代表已经在oc2.0(2006 年,苹果发布了全新的 Objective-C 2.0)被舍弃了,那么objc_class最新的长什么样子呢?

打开源码,搜索objc_class然后在objc-runtime-new.h里可以看到objc_class定义如下

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() {
        return bits.data();
    }
    
    //还有很多代码,为了便于查看,删除了
}

可以看到最新的objc_class里居然没有isa,其实是有的,因为继承于objc_object,点击objc_object查看,可以看到这个结构体定义为

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    //还有很多代码,为了便于查看,删除了
}

objc_object里有isa,objc_class继承于objc_object,所以也是有isa的。

最新源码isa_t长下面这样

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

然后ISA_BITFIELD宏长下面这样

#   define ISA_BITFIELD    /*注释__arm64__*/                                                  \
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 ISA_BITFIELD    /*注释__x86_64__*/                                                       \
uintptr_t nonpointer        : 1;                                         \
uintptr_t has_assoc         : 1;                                         \
uintptr_t has_cxx_dtor      : 1;                                         \
uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic             : 6;                                         \
uintptr_t weakly_referenced : 1;                                         \
uintptr_t deallocating      : 1;                                         \
uintptr_t has_sidetable_rc  : 1;                                         \
uintptr_t extra_rc          : 8

之前版本是直接定义为下面这样的,其实把宏拷贝进去也是下面这样
union isa_t //共用体虽然是公用一块内存,后来者会覆盖前者,但是下面位数加起来刚好64位,完美错开了每一位覆盖,就算覆盖,也可以避免覆盖其他位的值。

{
    Class cls;
    uintptr_t bits;
    sstruct {  /*注释__arm64__*/
        uintptr_t nonpointer :1;  //0代表普通指针,存储着Class、Meta-Class对象内存地址信息;1代表优化过,使用位域存储更多信息 。这1位在最右边,最低位,下面依次往左排布
        uintptr_t has_assoc :1;  //是否有或者曾有关联对象,如果没有,释放更快
        uintptr_t has_cxx_dtor :1 //是否有C++析构函数(类似OC的dealloc函数),如果没有,释放更快
        uintptr_t shiftcls :33  //存储着Class、Meta-Class对象内存地址信息
        uintptr_t magic :6  //用于在调试时分辨对象是否完成初始化
        uintptr_t weakly_referenced :1  //是否曾有被弱引用指向过
        uintptr_t deallocating :1  //对象是否正在释放
        uintptr_t has_sidetable_rc :1 //引用计数器是否过大无法存储在isa的extra_rc 中,如果为1,那么引用计数会存储在一个叫SideTableS()散列表中
        uintptr_t extra_rc :19  //里面存储的值是当前对象的引用计数器的个数减1
    }
}
//新建一个类,打印下来的isa值放在十六进制计算器里,可以一位位分析上面位域存储信息

话外:
模拟器32位处理器测试需要i386架构,
模拟器64位处理器测试需要x86_64架构,
真机32位处理器需要armv7,或者armv7s架构,
真机64位处理器需要arm64架构。

在开始之前,先看下runtime里类的结构定义

struct objc_class : objc_object {
    //Class ISA;
    Class superclass;
    cache_t cache;  //方法缓存
                 
                      class_data_bits_t bits;  //&FAST_DATA_MASK可以计算出class_rw_t ,获取类信息
                                         |
    class_rw_t *data() {                 |
           return bits.data();           |
       }                                 |
}                                        |
                                         |
                                         |     struct class_data_bits_t { // 源码里是这样取出结构体class_data_bits_t类型的bits里的class_rw_t
                                         |           class_rw_t* data() {
                                         |                       return (class_rw_t *)(bits & FAST_DATA_MASK);
                                         |            }
                                         |     }
                                         |
                                         |
                                        \|/ &FAST_DATA_MASK
struct class_rw_t {  //rw是readwrite
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;
 
                  const class_ro_t *ro;   //ro是readonly
                                      |
    method_array_t methods;           |  //方法列表二维数组里面装着method_list_t(方法列表一维数组) ,method_list_t里面装着method_t(方法),设计为二维便于往里面动态增加分类什么的,比如一个分类的方法包成一个method_list_t放在method_array_t前面。
    property_array_t properties;      |  //属性列表二维数组  ...
    protocol_array_t protocols;       |  //协议列表二维数组  ...
                                      |
    Class firstSubclass;              |
    Class nextSiblingClass;           |
                                      |
    char *demangledName;              |
}                                     |
                                      |
                                     \|/
struct class_ro_t { //类的一些初始信息
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  //instanceSize对象占用内存大小
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name;  //类名
    method_list_t * baseMethodList; //方法列表   里面装着method_t
    protocol_list_t * baseProtocols; //协议列表  ...
    const ivar_list_t * ivars; //成员变量列表     ...

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; //属性列表     ...

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

class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容
类初始化后创建了class_ro_t,把一些信息放进去,然后才出现class_rw_t,这时候可以把一些分类什么的信息合并到rw_t里

method_t是对方法\函数的封装,源码里结构如下

struct method_t {
    SEL name; //选择器,可以简单理解为函数名
    const char *types;  //编码(返回值类型,参数类型)
    MethodListIMP imp; //指向函数的指针(地址 )
}
"SEL"代表方法\函数名,一般叫做选择器,底层结构跟char *类似
可以通过@selector()和 ()获得 // NSLog(@"%@ %@",@selector(test),sel_registerName(@"test"));   ps: sel_registerName是运行时的一个函数
SEL sel2 = @selector(test);
SEL sel1 = sel_registerName("test"); //注意这传入C语言字符串

可以通过sel_getName()和NSStringFromSelector()转成字符串
char *selString1 = sel_getName(sel1); //注意这返回C语言字符串
NSString *selString2 = NSStringFromSelector(sel2);

不同类中相同名字的方法,所对应的方法选择器是相同的

"types"包含了函数返回值,参数编码的字符串
组成顺序: 返回值 参数1 参数2 参数3 ...
比如一个无传参无返回值的-(void)test;函数,在进行编译的时候是转成C函数的,即使无参无返,也会默认传参为void test:(id)self _cmd:(SEL)_cmd;默认传输self和sel作为参数
所以type打印为 v16@0:8  其中,v代表void,@代表id,:代表SEL,16代表所有传参占的字节,0代表id开始的字节,8代表SEL开始的字节
//涉及到 type encoding指令,可以通过@encode查看对应的type
NSLog(@"%s",@encode(int)); //i
NSLog(@"%s",@encode(id));  //@
NSLog(@"%s",@encode(SEL)); //:

比如一个方法

//types为 i24@0:8i16f20
-(int)test:(int)age height:(float)height;

"IMP"在源码里这样定义的

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);  //语法的意思是IMP是一个指向函数的指针,该函数返回id并将id,SEL等作为参数

这样说来,IMP是一个指向函数的指针,这个被指向的函数包括id(“self”指针),调用的SEL(方法名),再加上一些其他参数。

现在来看看类结构里的类型cache_t cache;

Class内部结构中有个方法缓存 cache_t cache; ,用可增量扩展的散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度,源码里结构如下

struct cache_t{
    strcut bucket_t *_buckets; //散列表数组,是bucket_t结构体的数组,里面存着bucket_t,bucket_t结构体里存着cache_key_t和IMP
    mask_t _mask;  //散列表的长度-1,用作掩码,之所以减1刚好保证相与后不超过缓存大小。
    mask_t _occupied;  //已经缓存的方法数量
}

散列表数组里存的是

struct bucket_t{ //旧版源码
    private:
    cache_key_t _key; //SEL作为_key
    IMP _imp; //函数的内存地址
}

散列表数组里存的是

struct bucket_t { //最新源码,没有直接提供好类型,但在public调用时明确了_key和_imp
    private:
        // IMP-first is better for arm64e ptrauth and no worse for arm64.
        // SEL-first is better for armv7* and i386 and x86_64.
    #if __arm64__
        uintptr_t _imp;
        SEL _sel;
    #else
        SEL _sel;
        uintptr_t _imp;
}
   _buckets散列表数组
———————————————————————
| bucket_t(_key,_imp) |   数组0号元素
———————————————————————
| bucket_t(_key,_imp) |   数组1号元素
———————————————————————
| bucket_t(_key,_imp) |   数组2号元素
———————————————————————
| ................... |   数组...号元素
———————————————————————

来剖析下散列表原理
哈希表(hash table,也叫散列表),是根据键(key)直接访问访问在内存储存位置的数据结构。
哈希表本质是一个数组,数组中的每一个元素成为一个箱子,箱子中存放的是键值对。根据下标index从数组中取value。关键是如何获取index,这就需要一个固定的函数(哈希函数),将key转换成index。不论哈希函数设计的如何完美,都可能出现不同的key经过hash处理后得到相同的hash值,这时候就需要处理哈希冲突。

散列表原理如下,跟cache总体流程差不多

函数cache总体流程:当第一次调用方法时,就会把方法缓存到类对象的cache里面,而cache里面有一个散列表的结构,这个方法缓存到散列表的哪个位置,由_key&_mask决定,并不是由第一个坐标往下排列,_key&_mask地址值运算出是多少就是多少,存在那里。存值的时候由_key&_mask来找到索引取出方法,如果出现计算地址值对应的方法为空,那么证明这块空间可以用来存储当前方法,如果这块空间被占用了,则会让索引值减1接着计算能存储的地方,减减减后发现全部用光则用_mask作为坐标,再不行就返回空了,如果散列表整体空间不够用了,会清空里面所有缓存然后扩容(原有长度(原有长度一开始就会分配一个值)*2),重新开始方法调用缓存(重新缓存,之前缓存的不要了,开始按新的方法调用缓存,比如之前存了8个,存第九个时候不够了,清空缓存,分配空间,这时候只缓存第九个了,之前的八个只有再次调用才会再次缓存)。取值的时候由_key&_mask来找到索引取出方法,如果出现查找地址值取出的_key不是外界传进来的_key,则会让索引值减1接着查找,减减减后发现全部不对则用_mask作为坐标取出方法。

假设使用"子类"调用对象方法,那么流程如下
先用isa指针找到子类,然后查看子类的cache里面有没有缓存,没有就去子类的rw_t的方法列表里找,找到就调用并且把方法缓存到子类的cache里,下次还是这个流程。假如这个方法是在父类里,调用顺序是先用isa指针找到子类,然后查看子类的cache里面有没有缓存,没有就去子类的rw_t的方法列表里找,还找不到就用superclass指针去找到父类类对象,查看父类的cache里面有没有缓存,如果有,就拿出来调用,并且缓存到子类的cache里,没有就去父类的rw_t的方法列表里找,找到就调用并且把方法缓存到子类cache里。基类的话以此类推,找到最终都是缓存到子类cache里。
//可能会有小疑问,都缓存到子类的cache里了,那父类cache缓存什么,父类cache当然是缓存某些只初始化父类并调用父类方法时的缓存啊

论证过程就是导入之前写的那个类的结构的类文件,然后把自己的类桥接成结构体类,然后观察里面cache的_mask以及_occupied。但是_buckets只会展示第一个元素,想要窥探里面其他元素,bucket_t *buckets = 类->cache._buckets,然后遍历的 i < 类->cache._mask+1 ,就能遍历出里面的bucket_t。

//桥接如下
MJGoodStudent *goodStudent = [[MJGoodStudent alloc] init];
mj_objc_class *goodStudentClass = (__bridge mj_objc_class *)[MJGoodStudent class];

我如果想看看某个方法在散列里的存值,可以先算出索引,然后打印,如下

cache_t cache = goodStudentClass->cache;
bucket_t *buckets = cache._buckets;
bucket_t bucket = buckets[(long long)@selector(studentTest) & cache._mask];
NSLog(@"%s %p", bucket._key, bucket._imp);
//有时候会出现地址被占用的情况的
//控制台通过一个地址想看具体调用的方法是哪个
p (IMP)方法地址
MJPerson *person = [[MJPerson alloc] init];
[person personTest];
这句话[person personTest]在C++底层源码大致是下面这个样子
objc_msgSend(person,sel_registerName("personTest"));
// 消息接收者(receiver):person
// 消息名称:personTest
那么sel_registerName("personTest")是什么呢?
sel_registerName("personTest")等价于@selector(personTest);
[MJPerson initialize];在C++底层源码大致是下面这个样子
objc_msgSend(objc_getClass("MJPerson"),sel_registerName("initialize"));
// 消息接收者(receiver):[MJPerson class] 这个调用的是类方法,所以接收者是类
// 消息名称:initialize

OC的方法调用:也称为消息机制,给方法调用者发送消息

OC中的方法调用,其实都是转换为objc_msgSend函数的调用

objc_msgSend在源码里是以汇编的形式公开的

objc_msgSend的执行流程可以分为3大阶段

消息发送
动态方法解析
消息转发

经过上述三阶段还找不到合适的方法会报错:unrecognized selector sent to instance 地址值

消息发送阶段做的事情

1611645277195.jpg

看截图吧,大致也就是上面的"子类"调用方法流程。注意点:[person personTest];在底层会转成objc_msgSend(person,sel_registerName("personTest"));,所以如果person也就是receiver传的是nil,那么就直接退出流程。不为空才开始流程。

动态方法解析阶段做的事情(在这个阶段可以动态添加方法的实现)

如果一个方法调用通过消息发送阶段还没找到方法,就会判断是否曾经是否有过动态解析;解析过就进入消息转发阶段;没有解析过就调用+resolveInstanceMethod:或者+resolveClassMethod:方法来动态解析方法,如果这两个方法有手动实现,会解析这两个方法里的动态添加方法的函数并添加到rw_t里,然后标记为动态解析过,动态解析过后,会重新走“消息发送”的流程,“从receiverClass的cache中查找方法”这一步开始执行。如果这两个方法没有手动实现,或者实现了这两个方法但是里面没有没有添加方法实现,那么不会也不可能解析这两个方法里的添加方法函数,但是依旧会标记为动态解析过,标记后,会重新走“消息发送”的流程,“从receiverClass的cache中查找方法”这一步开始执行,找不到这次就直接走消息转发阶段。

疑问解析:上述流程中的动态解析,如果+resolveInstanceMethod:或者+resolveClassMethod:方法有手动实现,比如下面示例,就是把没有实现的test方法的实现换成了other,所以重新走“消息发送”的流程是找到的test方法的实现就变成了other,所以程序就不会蹦。

MJPerson *person = [[MJPerson alloc] init];
[person test];
结果test只有声明,没有实现,那么在MJPerson.m文件中实现下面方法,也可保证代码执行成功
void c_other(id self, SEL _cmd)  //c语言定义的c_other方法
{
    NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
- (void)other
{
    NSLog(@"%s", __func__);
}

//方案一

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 动态添加test方法的实现
        class_addMethod(self, sel, (IMP)c_other, "v16@0:8");  //(IMP)c_other能这样写是因为C语言的函数名就是函数地址

        // 返回YES代表有动态添加方法。其实这个YES或者NO返回值并没有被使用到。
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//方案二

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
       
        // 获取对象方法 ,Method结构构造跟method_t一样,都是三元素SEL、types、IMP
        Method method = class_getInstanceMethod(self, @selector(other));
        
       
         //Method在OC头文件里定义为下面这样
        /* typedef struct objc_method *Method;*/
        
        //objc_method在源码里长下面这样
        /*
        struct objc_method {
            SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
            char * _Nullable method_types                            OBJC2_UNAVAILABLE;
            IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
        }
         */
        
        // 动态添加test方法的实现
        class_addMethod(self, sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));

        // 按照示例规范返回YES,代表有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//方案三

struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他方法
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));

        // 动态添加test方法的实现
        class_addMethod(self, sel, method->imp, method->types);

        // 返回YES代表有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

上述三个方案是针对调用对象方法的动态添加方法,如果外界调用的是[MJPerson test];那么要实现resolveClassMethod方法

+(void)haha{
    NSLog(@"哈哈");
}
+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        //获取类方法
        Method method = class_getClassMethod(object_getClass(self), @selector(haha));
        // 第一个参数是object_getClass(self)
        class_addMethod(object_getClass(self), sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveClassMethod:sel];
}

消息转发阶段做的事情

消息转发:将消息发送给别人

调用forwardingTargetForSelector:方法 (没写+或减-方法开头,证明是因为这个方法有类方法和对象方法,对象方法实现-,类方法实现+),返回值不为nil时调用成功,返回值为空时,调用methodSignatureForSelector:方法;methodSignatureForSelector方法返回值不为空时,调用forwardInvocation:方法;methodSignatureForSelector方法返回值为空时,调用doesNotRecognizeSelector:方法并抛出异常unrecognized selector

- (id)forwardingTargetForSelector:(SEL)aSelector    //这个方法干的事情在源码里是以汇编的形式展现的,而且实现也未公开
{
    if (aSelector == @selector(test)) {
        // objc_msgSend([[MJCat alloc] init], aSelector),即自己的类没有这个test方法,那么转发给MJCat类,调用MJCat类的test对象方法
        return [[MJCat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果上步没有转发,那么顺序执行下面俩方法,当然方法签名如果返回nil,是会直接崩溃的。下面两个方法也是有类方法和对象方法,对象方法实现-,类方法实现+

// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) { //这里是给test生成一个方法签名
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 针对的是上面的方法签名,NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument:NULL atIndex:0]  //取出传参,如果有的话
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 上面的方法签名里已经指定了方法名、方法参数,所以这里重新赋值下新的方法调用者就能去新的方法调用者里去查找这个方法进行调用(比如上面注册的是test方法签名,那么这个也会会执行MJCat的test)
    //把调用者给MJCat,此时会执行MJCat的test
    [anInvocation invokeWithTarget:[[MJCat alloc] init]];

    // 上面invokeWithTarget等同于下面这两句
//    anInvocation.target = [[MJStudent alloc]init];
//    [anInvocation invoke]; //主动调用
}

只要执行到forwardInvocation这个方法,程序就不会因unrecognized selector崩溃,无论上一步注册了什么方法,forwardInvocation方法里可以写自己想要实现的方法,也可以调用别的方法。
比如

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"哈哈");
}
如果方法有传参,可通过getArgument取出。

@synthesize 和 @dynamic

@interface MJPerson : NSObject
@property (assign, nonatomic) int age;
@end

@implementation MJPerson
// 提醒编译器自动生成setter和getter的实现、并生成__age成员变量,现在xcode升级了,@synthesize都被默认书写了。
@synthesize age = _age;
// 提醒编译器不要自动生成setter和getter的实现、不要自动生成成员变量
@dynamic age;
@end

super

如果MJStudent继承MJPerson,MJPerson继承NSObject,然后MJStudent重写父类MJPerson的test方法。如下

- (void)test{
   [super test];
}

那么MJStudent的test方法在cpp文件里是怎么展示的呢?

转cpp文件后看到如下

static void _I_MJStudent_test(MJStudent * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))}, sel_registerName("test"));
}

可以看到[super test]这句话展示为objc_msgSendSuper()方法接收两个参数,一个是__rw_objc_super,一个是sel

在源码里搜索objc_msgSendSuper,可以看到定义如下,接收两个以上参数,第一个为objc_super结构体,第二个为SEL

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

在源码里搜索objc_super,可以看到这个结构体定义为如下

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;  // 消息接收者。表明当前类仍然是消息接受者
    __unsafe_unretained _Nonnull Class super_class;  // 消息接收者的父类。
};

所以[super test]转成的cpp就可以看懂了,objc_msgSendSuper接收两个以上参数,第一个参数为一个objc_super结构体,结构体里定义了receiver,以及super_class;第二个参数为SEL。

objc_msgSendSuper方法在源码里面定义时,有句注释是这样说的:“参数objc_super结构体里的receiver为消息接收者,super_class是开始搜索方法实现的类”。所以我们可以理解objc_msgSendSuper方法实现原理是消息接收者仍然是子类对象,从父类开始查找SEL的实现。

如果MJStudent继承MJPerson,MJPerson继承NSObject
那么

@implementation MJStudent
- (instancetype)init
{
    if (self = [super init]) {
        
        NSLog(@"[self class] = %@", [self class]); // MJStudent
        NSLog(@"[self superclass] = %@", [self superclass]); // MJPerson
        
        NSLog(@"[super class] = %@", [super class]); // MJStudent
        NSLog(@"[super superclass] = %@", [super superclass]); // MJPerson
    }
    return self;
}
@end

前两个的打印还好理解,为什么[super class]是MJStudent,[super superclass]是MJPerson呢?
首先来看下super本质
/*
[super XXXFuction]的底层实现
1.消息接收者仍然是子类对象
2.从父类开始查找方法的实现
*/

class方法的实现是在NSObject里,大致实现是这样

- (Class)class
{
    return object_getClass(self); //返回传进来的接收者的类
}

superclass方法的实现是在NSObject里,大致实现是这样

- (Class)superclass
{
    return class_getSuperclass(object_getClass(self)); //返回传进来的接收者的父类
}

所以一步步往上找,找到后返回接收者。所以上面打印就好理解了。

//object_getClass 传参是instance对象(即实例对象)运行时获取类对象
Class objectClass4 = object_getClass(object1);

//获取元类对象
Class objectMetaClass = object_getClass([NSObject class]);
//传参是类对象时获取的是meta-class(元类对象)

== 、isEqual、hash

对于基本类型, ==运算符比较的是值;
对于对象类型, ==运算符比较的是对象的地址

isEqual方法就是用来判断两个对象是否相等,更像是比较对象里所有元素均相等才相等。

UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
NSArray *a1 = @[@1,@2,@3];
NSArray *a2 = @[@2,@1,@3];

NSLog(@"color1 == color2 = %@", color1 == color2 ? @"YES" : @"NO");
NSLog(@"[color1 isEqual:color2] = %@", [color1 isEqual:color2] ? @"YES" : @"NO");
NSLog(@"%d",[a1 isEqual:a2]);
打印:
2019-06-05 15:49:42.219681+0800 Test[6542:625122] color1 == color2 = NO
2019-06-05 15:49:42.219795+0800 Test[6542:625122] [color1 isEqual:color2] = YES
2019-06-05 15:49:42.219936+0800 Test[6542:625122] 0

实例对象的类对象是类对象,类对象的类对象是元类对象

isKindOfClass
isMemberOfClass
二者都有+方法和—方法

看下底层实现,实现直接在源码公开的

- (BOOL)isMemberOfClass:(Class)cls { //方法调用的左边这个类的类对象是不是刚好等于右边的类对象
    return [self class] == cls;
}

+ (BOOL)isMemberOfClass:(Class)cls { //方法调用的左边这个类对象的元类是不是刚好等于右边的元类
    return object_getClass((id)self) == cls;   //+号开头的方法,self就是类对象,不是实例对象了
}

- (BOOL)isKindOfClass:(Class)cls {  //方法调用的左边这个类的类对象是不是等于右边的类对象,或者是右边类对象的子类,满足返回yes
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isKindOfClass:(Class)cls { //方法调用的左边这个类对象的元类是不是等于右边的元类,或者是右边元类的子元类,满足返回yes
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

ps:观看上面源码可知,上面这四个方法,如果是对象方法,左边参数会直接拿来用;如果是类方法,会获取左边参数的元类拿来使用。但是注意,比对的右边参数是被直接拿来用的,所以,要注意:对象方法里,右边传参是类对象;类方法里,右边传参是元类对象。

简单应用:

#import 

NSObject *obj = [[NSObject alloc]init];
NSLog(@"%d",[obj isMemberOfClass:[NSObject class]]);  //1
NSLog(@"%d",[NSObject isMemberOfClass:object_getClass([NSObject class])]);  //1
NSLog(@"%d",[obj isKindOfClass:[NSObject class]]);   //1
NSLog(@"%d",[NSObject isKindOfClass:object_getClass([NSObject class])]);    //1

结合上面所学知识,看下面两个打印,会发现第三个很奇怪为什么会是1,,不是该传元类吗。因为基类元类对象的superclass指向基类类对象,在遍历到基类时的superclass已经变成类对象了,类对象于是乎等于右边传进来的类对象。

NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); //0
NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); //0   MJPerson的superClass是NSObject类对象
NSLog(@"%d", [MJPerson isKindOfClass:[NSObject class]]); //1

针对为什么第三个是1,结果源码开始分析

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

首先cls在这里面传进来的是[NSObject class],tcls首先是MJPerson的元类对象,判断不等于,开始去找MJPerson的元类对象的superclass,找到了NSObject的元类对象,还不等于,再找superclass,找成了[NSObject class],此时tcls变成了[NSObject class],于是tcls == cls了,所以打印为1

看下面四道题

NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]); //1
NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]); //0
NSLog(@"%d", [[MJPerson class] isKindOfClass:[MJPerson class]]); //0
NSLog(@"%d", [[MJPerson class] isMemberOfClass:[MJPerson class]]); //0

看另外一道题

#import 

@interface MJPerson : NSObject
@property (copy, nonatomic) NSString *name;

- (void)print;
@end

#import "MJPerson.h"

@implementation MJPerson

- (void)print
{
    NSLog(@"my name is %@", self->_name);
}

@end

在ViewController控制器里执行下面代码,能执行成功吗,如何可以,会打印什么?

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

答案:能执行成功,而且打印 my name is

/*
1.print为什么能够调用成功?

2.为什么self.name变成了ViewController等其他内容
*/

1.id cls = [MJPerson class];代码表达的是cls指针指向MJPerson类
  void *obj = &cls;代码表达的是obj指针指向cls指针的地址
所以关系图就是
    指向       指向
obj ----> cls ----> MJPerson

我们平时写
MJPerson *mj = [[MJPerson alloc] init];
[mj print];
指针情况是什么样的呢?
    指向      指向
mj ----> isa ----> MJPerson
因为isa指针处于结构体的第一个位置,所以mj指针的地址可以等于isa的指针地址,然后isa去找类对象调用方法。

由此,二者很类似,所以能调用print。

2.
栈空间分配,是由高地址到低地址

我们在ViewController里的viewDidLoad在增加一句话
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *test = @"123";
    id cls = [MJPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];
}
//此时会打印出 my name is 123

那么,test、cls、obj在里栈分配情况如下

低地址
  |
  |obj指针 :指向cls
  |cls指针 :指向MJPerson
  |test指针:@"123"
 \|/
高地址

如果是由[mj print];执行里面self->_name是由mj找到实例对象,然后在实例对象结构体里跳过isa指针向下移8个字节取出这个地址值存的东西。同理,如果此时[(__bridge id)obj print]调用,想找到obj的_name,先找obj的指向,找到了cls,然后cls的存储空间向下移8位 ,就找到了test,于是会打印出my name is 123。如果我们把NSString *test = @"123";删了,为什么会打印my name is 呢,是因为我们调用了[super viewDidLoad],这个super底层是一个结构体,含有self,以及父类class。所以,上述栈结构其实如下
低地址
 |
 |obj指针 :指向cls
 |cls指针 :指向MJPerson
 |test指针:@"123"
 |self
 |ViewController class    //不是ViewController的superclass
\|/
高地址

我们说super底层时这样的

struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者。表明当前类仍然是消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类。表明只是说从父类开始查找方法的实现
};

其实这只是转成c++文件时给用户参考的,我们在运行的时候打断点然后转汇编,会发现super执行的是objc_super2,结构体传的是self以及当前类,不过在使用得时候在多执行一步先找当前类得superclass,效果跟objc_super一样。

Runtime 应用

什么是Runtime?平时项目中有用过么?
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用

利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
......

具体应用实例

//获取isa指向的Class
Class object_getClass(id obj)

eg:
MJPerson *person = [[MJPerson alloc]init];
Class personClass = object_getClass(person); // 类
Class personMetaClass = object_getClass(personClass);  //元类
//设置isa指向的Class
Class object_setClass(id obj, Class cls)

//判断一个OC对象是否为Class
BOOL object_isClass(id obj)

//判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

//获取父类
Class class_getSuperclass(Class cls)

eg:

//假设MJPerson里有run方法,MJCar里也有run方法
MJPerson *person = [[MJPerson alloc] init];
[person run]; //此时打印MJPerson里的run

object_setClass(person, [MJCar class]);//修改person的ISA指向MJCar
[person run];//此时打印MJCar里的run

NSLog(@"%d %d %d",
      object_isClass(person), //0
      object_isClass([MJPerson class]),  //1
      object_isClass(object_getClass([MJPerson class])),  //1 ,此时为1是因为元类类型也是Class的
      class_isMetaClass(object_getClass([MJPerson class]))  //1
      );

Class superClass = class_getSuperclass([MJPerson class]);
NSLog(@"%@",superClass); //NSObject
//动态创建一个类(参数:要继承的父类名字,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

//动态添加成员变量(已经注册的类是不能动态添加成员变量的,因为类结构里的ro_t是只读的,所以没法在注册类之后再增加成员变量)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

//注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)

//销毁一个类
void objc_disposeClassPair(Class cls)


eg:
void run(id self,SEL _cmd){ //先创建一个函数,一会绑给新建的类
    NSLog(@"------");
}
// 创建类
Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);  //这样创建类的方式很强大,类名也可以由自己随意控制
class_addIvar(newClass, "_age", 4, 1, @encode(int)); //添加int类型的_age成员变量  ,4是int占的字节数,1是对齐参数
class_addIvar(newClass, "_weight", 4, 1, @encode(int));//添加int类型的_weight成员变量
class_addMethod(newClass, @selector(run), (IMP)run, "v@:");//添加run方法
// 注册类
objc_registerClassPair(newClass); //创建类以及添加完变量后需要注册一下类才能生效。方法倒是没有限制在哪儿添加,因为rw_t的方法列表是可读可写的

MJPerson *person = [[MJPerson alloc] init];
object_setClass(person, newClass);
[person run]; //此时调用了自己创建的MJDog类的run

id dog = [[newClass alloc] init];
[dog setValue:@10 forKey:@"_age"]; 可以通过KVC去访问给这个类创建过的属性
[dog setValue:@20 forKey:@"_weight"];
[dog run];

NSLog(@"%@ %@", [dog valueForKey:@"_age"], [dog valueForKey:@"_weight"]);

// 在不需要这个类时释放
objc_disposeClassPair(newClass);
//获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

//获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

//设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

eg:
先在MJPerson里用@property声明age和name属性,age为Int,name为NSString
 //获取成员变量描述信息
Ivar ageIvar = class_getInstanceVariable([MJPerson class], "_age");
NSLog(@"%s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));

 //设置和获取成员变量的值
Ivar nameIvar = class_getInstanceVariable([MJPerson class], "_name");

MJPerson *person = [[MJPerson alloc] init];
object_setIvar(person, nameIvar, @"123");
object_setIvar(person, ageIvar, (__bridge id)(void *)10); //将10直接存到ageIvar里,但是报错,所以将10先转成指针(void *),为什么可以转成(void *),因为指针就是存值的,先转成指针,然后再转成需要接收的id型。因为这个参数接收类型为id
NSLog(@"%@ %d", person.name, person.age);

// 成员变量的数量
unsigned int count;
Ivar *ivars = class_copyIvarList([MJPerson class], &count);
for (int i = 0; i < count; i++) {
    // 取出i位置的成员变量
    Ivar ivar = ivars[i];
    NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars); //runtime通过copy创建的需要释放
//获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

//拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

//动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                       unsigned int attributeCount)

//动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                           unsigned int attributeCount)

//获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
//获得一个实例方法
Method class_getInstanceMethod(Class cls, SEL name)
//获得一个类方法
Method class_getClassMethod(Class cls, SEL name)

//方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

//拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

//动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

//动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

//获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

//选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

//用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)


eg:
void myrun()
{
    NSLog(@"---myrun");
}

void test()
{
    MJPerson *person = [[MJPerson alloc] init];
    
//替换MJPerson的run方法为自己写的myrun
//    class_replaceMethod([MJPerson class], @selector(run), (IMP)myrun, "V@:");
//    [person run];
 
//替换MJPerson的run方法用block作为方法实现
    class_replaceMethod([MJPerson class], @selector(run), imp_implementationWithBlock(^{
        NSLog(@"123123");
    }), "V@:");
    [person run];
}

假设MJPerson里有run和test对象方法

MJPerson *person = [[MJPerson alloc] init];

Method runMethod = class_getInstanceMethod([MJPerson class], @selector(run));
Method testMethod = class_getInstanceMethod([MJPerson class], @selector(test));
//交换test和run方法
method_exchangeImplementations(runMethod, testMethod);

[person test];//运行run方法
[person run];//运行test方法
//写一个运行时,用来打印一个类的对象方法
- (void)printMethods:(Class)cls
{
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        
        NSString *methodName = NSStringFromSelector(method_getName(method));
        [methodNames appendString:methodName];
        [methodNames appendString:@" "];
    }
    
    NSLog(@"%@", methodNames);
    
    free(methods);
}

交换方法

假设想要拦截系统的按钮点击需要怎么做呢

//因为UIButton继承UIControl,所以给UIControl新建一个分类,重写load,在load里交换系统点击的底层方法为自己的方法

#import "UIControl+Extension.h"
#import 

@implementation UIControl (Extension)

+ (void)load
{
   //因为- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;实际是会触发- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;所以交换sendAction:to:forEvent:)就行
    // hook:钩子函数,也为交换方法
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));//拿到系统方法
    Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));//拿到自己方法
    method_exchangeImplementations(method1, method2);
}
 
- (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    
    // 调用系统原来的实现
    [self mj_sendAction:action to:target forEvent:event];

    //    if ([self isKindOfClass:[UIButton class]]) {
    //        // 拦截了所有按钮的事件
    //
    //    }
}

@end

上述即实现了拦截点击,同时在mj_sendAction方法里调用[self mj_sendAction:action to:target forEvent:event]又会触发原来的按钮点击效果。
有几个疑问
1.方法交换原理?
2.为什么mj_sendAction调 【self mj_sendAction】不会死循环?

1.方法交换原理?

已知类的rw_t的方法列表里存的方法数组里的method_t如下

struct method_t {
    SEL name; //函数名
    const char *types;  //编码(返回值类型,参数类型)
    MethodListIMP imp; //指向函数的指针(地址 ),imp里存着函数的实现
}

执行method_exchangeImplementations交换函数做了两件事,第一件清除这个方法的缓存,第二件重新缓存,不过这次缓存更新了IMP指向,比如交换方法A和方法B,那么交换后方法A的imp存的是方法B的方法实现,方法B的imp存的是方法A的方法实现。

2.为什么mj_sendAction调 【self mj_sendAction】不会死循环?

已知当前方法里再次调用当前方法, 执行的时候会出现死循环,但是这个交换方法里却不会。
解惑:
比如交换方法拦截了系统按钮点击,那么我们拦截后想让之前的按钮点击事件生效怎么办,常规想法是在mj_sendAction:to:forEvent:里调用【self sendAction:to:forEvent:】,这时候会发现死循环了,因为sendAction:to:forEvent:里方法实现指向的是mj_sendAction:to:forEvent:,自己调自己,死循环。但是想要继续有按钮点击效果,必须要执行sendAction:to:forEvent:啊,其实sendAction:to:forEvent:方法的实现已经被替换到mj_sendAction:to:forEvent:的方法实现里了,所以在mj_sendAction:to:forEvent:里调用mj_sendAction:to:forEvent:

话外:

在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable()中,SideTableS()是散列表结构,其中SideTableS()中有很多SideTable结构,实例对象的地址作为key,SideTable结构作为value。非嵌入式系统中,使用64个SideTable

struct SideTable {
    spinlock_t slock; //自旋锁,是一种“忙等”的锁
    RefcountMap refcnts; //引用计数表,哈希表
    weak_table_t weak_table; //弱引用表,实际上也是哈希表
}

"为什么不用一个SideTable呢?
假如说只有一个SideTable,那么所有实例对象的引用计数和弱引用什么的都放在一个结构里。这时候如果我们要操作某一个对象的引用计数值进行修改,由于多个对象可能是在不同线程进行分配和销毁的,那么我们想正确的修改某一个对象就要进行加锁才能够保证资源访问正确。这时候大大减弱了效率。系统为了解决这种效率问题,采用了分离锁技术。将8个SideTable共用一把锁,共8把锁(N为64)。

"那么系统如何快速的实现SideTableS()分流呢?
SideTableS()本质是哈希表。通过对象地址经过hash函数计算后得出SideTable地址。

"RefcountMap"结构
引用计数表也就是RefcountMap是一个哈希表,通过一个指针快速找到对象的引用计数进行操作,避免循环遍历。
RefcountMap结构的key是指针(大牛没说是否是对象地址),value为size_t,size_t是一个无符号的long型的变量。
size_t在这里面是64位来表示,最低的一位(也就是第一个二进制位)用来表示是否有弱引用;第二位用来表示当前对象是否正在释放;其他62位用来存储这个对象的引用计数值,当我们想要获得这个对象的引用计数值时需要将这个值向右偏移两位才能取到真实的值。

"weak_table_t"弱引用表,实际上也是哈希表
weak_table_t的key为对象指针,通过hash运算,可以获得一个结构体为weak_entry_t,weak_entry_t为一个结构体数组,weak_entry_t这个结构体数组里面存的就是一个个的WeakPtr(对象),即弱引用指针地址。

weak 实现原理的概括

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

weak 的实现原理可以概括一下三步:

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

day17天第一节课讲了一点交换方法

你可能感兴趣的:(iOS Runtime)