美文网首页ios
Object-C ,runtime原理,oc对象的原理,主要记录

Object-C ,runtime原理,oc对象的原理,主要记录

作者: 絮语时光杨 | 来源:发表于2021-09-18 15:49 被阅读0次

    Object-C-runtime

    Object-C ,runtime原理,oc对象的原理,主要记录oc对象在底层的实现原理;
    项目代码地址GitHub
    https://github.com/xuyushiguang/Object-C-runtime

    //
    //  Person.m
    //  Interview001-OC对象的本质
    //
    //  Created by xingye yang on 2021/9/18.
    //  Copyright © 2021 xingye. All rights reserved.
    //
    
    #import "Person.h"
    #import <objc/runtime.h>
    #import <malloc/malloc.h>
    
    /*
     用来计算结构体的属性在内存中的偏移量
     */
    #define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
    
    
    @interface Person()
    {
    @public
        //定义一个私有属性,这个属性系统不会生成set/get方法
        int _age;
    }
    //定义一个property类型的属性,这个属性系统会帮我们生成set/get方法
    @property(nonatomic,assign)int height;
    -(void)run;
    +(void)home;
    @end
    
    @implementation Person
    -(void)run{
    }
    
    +(void)home{
    }
    @end
    
    #pragma mark ########### 以下是把.m文件转成.cpp后的样子,在这里我把关键的部分取出来,写在这里;#####
    #pragma mark###################################  Person start  ##################################################
    #pragma mark##################### 以下是把.m文件转成.cpp后的样子,在这里我把关键的部分取出来,写在这里;#####################
    #pragma mark####################################  Person start  ################################################
    
    
    /*
     Object-C中所有继承NSObject的类 底层就是一个结构体,所以我们把OC类转成.cpp文件后,看到的就是一个结构体
     */
    
    /*
     这个结构体是NSObject类底层的样子,说明NSObject类只有一个isa属性,isa是一个指针类型占8个字节,说明NSObject的一个对象实际内存是8字节;
     但是系统分配内存的时候却分配了16个字节,在下面这个函数中我们可以看到原因,当内存字节小于16字节时返回16字节的长度;
     size_t instanceSize(size_t extraBytes) {
         size_t size = alignedInstanceSize() + extraBytes;
         // CF requires all objects be at least 16 bytes.
         if (size < 16) size = 16;
         return size;
     }
     综上所述:一个NSObject对象的占内存长度是16个字节
     */
    struct NSObject_IMPL {
        Class isa;
    };
    
    /*
     这个就是Person类的底层结构体;我们可以看到结构体中有两个属性:
     1)NSObject_IVARS属性:是NSObject_IMPL类型的属性,也可以理解为isa指针;因为NSObject_IMPL只有一个isa属性
     2)_age 就是我们自定义的属性
     */
    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        int _age;
        int _height;
    };
    /*
     实例方法
     */
    static void _I_Person_run(Person * self, SEL _cmd) {
    }
    /*
     类方法
     */
    static void _C_Person_home(Class self, SEL _cmd) {
    }
    /*
     我们发现无论是对象方法还是类方法都没有在结构体内部,c++的结构体是可以定义方法的,但是为什么不在结构体内部定义;
     这里是把方法实现单独定义成了函数,这些函数会单独存放在一个内存区域,这个函数的内存区域和结构体对象的内存区域是分开的;
     也就是说结构只存储属性值,比如我们初始化一个结构体,系统就会给这个结构体分配内存,内存占用的多少是根据结构体的属性的多少来分配的;
     如果结构体内部有方法的话,也会给方法分配内存,这样就会消耗大量内存;
     如果把结构体的方法单独存放在一个内存区域,当我们取调用结构体对象的方法的时候,去这个内存区域找到对应的函数方法,并把结构体对象也传给这个方法,结构体对象相当于上下文功能,
     这样就不用给每个结构体实例的方法都分配内存,结构体所有对象共用一个方法内存,结构体对象只需要保存好属性的值的内存就可以了;
     下面会讲方法存储在什么地方
     */
    
    /*
     这个就是@property(nonatomic,assign)int height;由系统生成的set/get方法
     */
    static int _I_Person_height(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)); }
    static void _I_Person_setHeight_(Person * self, SEL _cmd, int height) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height; }
    
    #pragma mark####################################  Person end  ########################################################
    #pragma mark#############################到这就是Person类被转成底层时的样子##################################
    #pragma mark####################################  Person end  ########################################################
    
    /*
     下面就是对Person的属性的配置
     */
    
    /*
     这个定义是用来记录Person_IMPL结构体中的_age属性的内存偏移量;
     因为当我们给Person对象的_age属性设置值或者获取_age的值,都需要知道_age属性在内存中的位置,
     然后找到_age内存,把值写入到_age内存中,或者从_age内存中读取值
     */
    extern "C" unsigned long OBJC_IVAR_$_Person$_age;
    extern "C" unsigned long OBJC_IVAR_$_Person$_height;
    
    /*
     用来计算OBJC_IVAR_$_Person$_age的值
     */
    extern "C" unsigned long int OBJC_IVAR_$_Person$_age __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Person_IMPL, _age);
    
    extern "C" unsigned long int OBJC_IVAR_$_Person$_height __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Person_IMPL, _height);
    
    
    int main(int argc, const char * argv[]) {
        
        /*
         这里是初始化一个Person对象;
         Person *p = [[Person alloc] init];
         */
        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        
        /*
         p->_age = 20;
         
         这里是给Person对象的_age属性赋值,我们来分解这个式子到底是什么意思;
         0)其中p是Person类的一个对象,实际就是一个指针;是指针就好办了,我们可以转换成其他任意类型的指针;注意在ARC中编译器不让转化,可以在MRC中转换,把xcode修改成MRC;
         1)(char *)p 的意思是把p转换成char *类型的指针;
         2)((char *)p + OBJC_IVAR_$_Person$_age) 这个就是指针的移动,由于先前已经转换长char*类型的,所以指针移动的步长是一个字节,
            总共移动OBJC_IVAR_$_Person$_age长度的字节,OBJC_IVAR_$_Person$_age就是_age属性在Person_IMPL结构体中的内存偏移量;
            OBJC_IVAR_$_Person$_age的长度是这样计算的:__OFFSETOFIVAR__(struct Person_IMPL, _age)
         3)(int *)((char *)p + OBJC_IVAR_$_Person$_age)这一步是指针移动完成后,把指针转成int *类型的;这是因为_age属性是int类型的;
            转成int *类型是为了方便赋值或者取值;
         4)*(int *)((char *)p + OBJC_IVAR_$_Person$_age)这一步我们看到在3)的基础上最左边添加了一个*号,由于式子是在等号=左边,
            所以加*号是赋值的意思,
         */
        (*(int *)((char *)p + OBJC_IVAR_$_Person$_age)) = 20;
    
        /*
         int a = p->_age;
         
         这里是取值,取Person对象的_age属性值;这里就不做过多介绍了
         */
        int a = (*(int *)((char *)p + OBJC_IVAR_$_Person$_age));
        
        /*
         下面是调用@proper描述的属性,我们可以发现和调用_age属性不同;
         这里调用了set/get方法;而不是直接给属性赋值或者直接取属性的值;
         其实是在set/get方法内部实现了取值和赋值操作;
         不同点就是一个直接操作内存,另一个通过消息发送的方式调用set/get方法取操作内存;
         效率的话,肯定是直接操作内存快;
         
         */
    
        /*
         p.height = 100;
         
         这是height的set方法,其方法内部实现和_age属性一样
         _I_Person_setHeight_(Person * self, SEL _cmd, int height) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height; }
         */
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)p, sel_registerName("setHeight:"), 100);
    
        /*
         int h = p.height;
         
         这是height属性的get方法,我们可以看到方法内部的实现和_age属性一样
         _I_Person_height(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)); }
         */
        int h = ((int (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("height"));
    
        
        /*
         调用对象方法
         
         [p run];
         */
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
    
        /*
         调用类方法
         
         [Person home];
         */
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("home"));
        /*
         调用对象方法和类方法不同点在于:
         调用对象方法是直接传p对象;
         调用类方法是获取类对象objc_getClass("Person")
         */
        
        return 0;
    }
    
    #pragma mark ########### 以下是 Person的属性和方法是怎么保存的 #####
    #pragma mark #######################################################
    
    /*
     从转换后的.cpp文件中可以看到;Person类的变量,属性,方法,isa,父类的isa,以及缓存都可以从这里找到;
     _class_ro_t结构体非常重要,里面保存的都是Person类的所有的数据结构;
     
     也就是说我们这样Person *p = [[Person alloc] init];创建的p对象;p的isa指针指向的就是_class_t结构体对象;
     也即是Person *p = [[Person alloc] init];
     可以把p->isa转成:
     struct _class_t *c = (struct _class_t *)(p->isa);
     
     对于Person类来说,全局只有一个Person类对象,记住是类对象,不是Person对象,类对象全局只有一个,保存的是这个类的所有数据结构;
     这个类对象就是_class_t对象,我们创建p对象的时候,会把类对象(也就是_class_t对象)的指针地址保存一份到p对象的isa上;
     也就是我们每创建一个p对象,p对象都会在isa上保存一份类对象的指针地址
     
     这样有什么用呢?非常重要,比如:
     我们想要获取_age的值,平常我们是直接p._age就可以获取到,但是系统在底层帮我们做了好多事;
     1)首先我们要知道Person类有没有这个属性?
     2)我们还要知道_age属性在内存中的位置吧,不然怎么获取值呢?
     3)找到这个属性后才能对这个属性操作;
     所以Person的所有数据结构都保存在_class_t这个结构体中;
     下面一步步讲解
     */
    struct _class_t {
        struct _class_t *isa;
        struct _class_t *superclass;
        void *cache;
        void *vtable;
        struct _class_ro_t *ro;
    };
    
    /*
     这个结构体体保存的都是Person类的具体内容了;一个一个看:
     flags:不清楚,不用关心
     instanceStart:这个就是Person对象在内存地址中开始的位置,如果我们要获取某个对象,就需要先拿到这个属性,通过指针移动,获取到这个对象内存的起始位置
     instanceSize :这个就是对象的内存长度,也就是这个对象在内存中占用的字节数
     reserved:不清楚
     ivarLayout:不清楚
     name:类名
     baseMethods:保存方法的地方,类的方法都保存在这里
     baseProtocols:保存协议的地方
     ivars:保存对象变量的地方,就是在{int _age;}定义的变量;
     weakIvarLayout:不清楚
     properties:保存属性的地方,就是@proper()int height;定义的属性;
     */
    struct _class_ro_t {
        unsigned int flags;
        unsigned int instanceStart;
        unsigned int instanceSize;
        unsigned int reserved;
        const unsigned char *ivarLayout;
        const char *name;
        const struct _method_list_t *baseMethods;
        const struct _objc_protocol_list *baseProtocols;
        const struct _ivar_list_t *ivars;
        const unsigned char *weakIvarLayout;
        const struct _prop_list_t *properties;
    };
    
    /*
     先看变量列表;在.cpp文件中可以找到这个;这个就是保存变量的列表
     entsize:是_ivar_t结构体的内存长度
     count:保存变量的数量,
     ivar_list:保存真的变量的具体内容,看下面的_ivar_t介绍
     */
    static struct _ivar_list_t {
        unsigned int entsize;  // sizeof(struct _prop_t)
        unsigned int count;
        struct _ivar_t ivar_list[2];
    }
    /*
     这个是描述变量的,也就是这个变量的名字,类型,占用的内存字节数,内存偏移量
     offset:保存这个变量在结构体内存中的内存偏移量;方便找到这个变量的位置;类似_class_ro_t结构体中的instanceStart属性
     name:变量名字
     type:变量的类型,比如int,float,String等,是简写的类型;
     alignment:字节对齐
     size:这个变量占的内存长度;类似_class_ro_t结构体中的instanceSize属性
     */
    struct _ivar_t {
        unsigned long int *offset;  // pointer to ivar offset location
        const char *name;
        const char *type;
        unsigned int alignment;
        unsigned int  size;
    };
    
    /*
     这个是方法列表;类的所有方法都保存在这个地方;
     entsize:保存的是_objc_method结构体的占内存的大小;
     method_count:保存的是方法数量
     method_list:所有的方法都保存在这个数组中;
     一个方法的具体内容保存在_objc_method结构体中;
     继续往下看
     */
    static struct _method_list_t {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[1];
    }
    /*
     这个是描述一个方法;也就是这个方法具有哪些内容;
     _cmd:方法的名字
     method_type:方法的类型,像这样"v16@0:8",描述这个方法的返回值类型,参数类型等
     _imp:函数指针,可以理解为方法的实现的地址,通过这个指针就能拿到这个方法;从而调用这个方法;
          runtime里面的黑魔法,方法交换;其实就是修改_imp指针,让它指针另一个函数的地址;
     */
    struct _objc_method {
        struct objc_selector * _cmd;
        const char *method_type;
        void  *_imp;
    };
    
    /*
     _objc_protocol_list  协议列表
     _prop_list_t  属性列表
     有时间在讲解
     */
    
    #pragma mark #########################  类对象 到底是怎么产生的? ##############################
    #pragma mark #########################  类对象 属性和方法是怎么存储的?  ##############################
    
    /*
     这里是类对象实现的地方
     
     在.cpp文件我们看到,这里初始化了一个_class_t类型的变量 OBJC_CLASS_$_Person
     OBJC_CLASS_$_Person就是 Person 类对象,全局只有这一个;
     我们发现初始化的时候:
     struct _class_t *isa; 这个是0,也就是说类对象初始化的时候,他的isa指针式空的,别担心,后面会对他从新赋值
     struct _class_t *superclass; 这个也是0,也就是说类对象初始化的时候,他的指向父类的superclass指针式空的,别担心,后面会对他从新赋值
     void *cache;  缓存也是空的
     void *vtable; 也是空的
     struct _class_ro_t *ro; 这个有值_OBJC_CLASS_RO_$_Person,这个值保存的就是Person类的数据结构,保存的属性,方法,协议等
     我们全局找到_OBJC_CLASS_RO_$_Person这个在那实现的;请继续看下面
     */
    extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
        0, // &OBJC_METACLASS_$_Person,
        0, // &OBJC_CLASS_$_NSObject,
        0, // (void *)&_objc_empty_cache,
        0, // unused, was (void *)&_objc_empty_vtable,
        &_OBJC_CLASS_RO_$_Person,
    };
    
    /*
     我们发现_OBJC_CLASS_RO_$_Person是_class_ro_t类型的结构体对象;
     可以看到
     unsigned int flags; 值是0
     unsigned int instanceStart;可以看到内存初始位置就是竟然是Person类的第一个自定义属性_age的指针位置;竟然不是isa的指针位置,那是因为isa保存的是类对象的位置,
     如果从isa开始取值,就取不到_age,和其他属性的值了;
     unsigned int instanceSize;可以看到内存长度是Person_IMPL结构体的长度
     unsigned int reserved; 值是0
     const unsigned char *ivarLayout;也是0
     const char *name; 类名"Person"
     const struct _method_list_t *baseMethods; 这个有值,_OBJC_$_INSTANCE_METHODS_Person,这个保存的是方法列表
     const struct _objc_protocol_list *baseProtocols;也是0,因为我们没有定义协议,所以没有
     const struct _ivar_list_t *ivars;保存的是变量列表
     const unsigned char *weakIvarLayout;应该是存的弱引用
     const struct _prop_list_t *properties;属性列表
     
     下面主要看方法列表_method_list_t和_ivar_list_t变量列表
     
     */
    static struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        0,
        __OFFSETOFIVAR__(struct Person, _age),
        sizeof(struct Person_IMPL),
        (unsigned int)0,
        0,
        "Person",
        (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
        0,
        (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
        0,
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
    };
    
    /*
     这里是方法列表。初始化方法列表
     我们可以看到run,这个是我们自己定义的方法
     还有height和setHeight,这个是@property()定义的属性,系统帮我们实现了set/get方法
     下面我们看看变量列表是怎么实现的
     
     _I_Person_run
     _I_Person_height
     _I_Person_setHeight_
     这三个就是函数指针,保存的是函数实现的位置
     当我的Person对象调用run方法的时候,其实就是根据“run”字符串去找到_I_Person_run这个函数指针;调用这个函数;
     我们发现_I_Person_run函数的第一个参数就是Person类型的,
     */
    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 *)"run", "v16@0:8", (void *)_I_Person_run},
        {(struct objc_selector *)"height", "i16@0:8", (void *)_I_Person_height},
        {(struct objc_selector *)"setHeight:", "v20@0:8i16", (void *)_I_Person_setHeight_},
        }
    };
    
    
    /*
     这个就是变量列表,初始化变量列表
     在.cpp文件中我们可以找到这个,这个就是把Person类的所有属性保存在 _OBJC_$_INSTANCE_VARIABLES_Person 这个变量里;
     _INSTANCE_VARIABLES_Person 是_ivar_list_t类型的,上面有介绍;
     这个式子的意思是定义一个_ivar_list_t类型的结构体,并且创建一个结构体对象_OBJC_$_INSTANCE_VARIABLES_Person 并给这个对象初始化;初始化的时候把Persion类的属性都放进去了;
     
     */
    static struct _ivar_list_t {
        unsigned int entsize;  // sizeof(struct _prop_t)
        unsigned int count;
        struct _ivar_t ivar_list[2];
    } _OBJC_$_INSTANCE_VARIABLES_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_ivar_t),
        2,
        {
            /*在这里说一下:
             OBJC_IVAR_$_Person$_age :这个在上面有定义过,就是_age属性在结构体内存中的偏移量;
             "_age":是属性的名字
             "i":是属性的类型;
             2:未知,可能是属性的数量;
             4:就是这个属性的内存长度因为是int类型,所以长度是4
             */
            {(unsigned long int *)&OBJC_IVAR_$_Person$_age, "_age", "i", 2, 4},
            {(unsigned long int *)&OBJC_IVAR_$_Person$_height, "_height", "i", 2, 4}
        }
    };
    
    
    #pragma mark #############  重要  isa指针什么时候赋值的?  ############
    /*
     重要
     其实在这里赋值的,
     首先是NSObject的元类对象给Person的元类isa赋值;
     最后Person的元类对象赋值给Person类对象的isa
     */
    static void OBJC_CLASS_SETUP_$_Person(void ) {
        OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
        OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
        OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
        OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
        OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
        OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
    }
    
    #pragma section(".objc_inithooks$B", long, read, write)
    __declspec(allocate(".objc_inithooks$B")) static void *OBJC_CLASS_SETUP[] = {
        (void *)&OBJC_CLASS_SETUP_$_Person,
    };
    
    
    
    #pragma mark #########################   元类对象 到底是怎么产生的?  ##############################
    
    
    /*
     这里是元类对象实现的地方
     
     在.cpp文件我们看到,这里初始化了一个_class_t类型的变量 OBJC_METACLASS_$_Person
     OBJC_METACLASS_$_Person就是 Person 元类对象,全局只有这一个;
     我们发现初始化的时候:
     struct _class_t *isa; 这个是0,也就是说元类对象初始化的时候,他的isa指针式空的,别担心,后面会对他从新赋值
     struct _class_t *superclass; 这个也是0,也就是说元类对象初始化的时候,他的指向父类的superclass指针式空的,别担心,后面会对他从新赋值
     void *cache;  缓存也是空的
     void *vtable; 也是空的
     struct _class_ro_t *ro; 这个有值,这个值保存的就是Person元类的数据结构, 基本上保存的是类方法,+加号方法
     我们全局找到_OBJC_METACLASS_RO_$_Person这个在那实现的;请继续看下面
     */
    extern "C" __declspec(dllexport) struct _class_t OBJC_METACLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
        0, // &OBJC_METACLASS_$_NSObject,
        0, // &OBJC_METACLASS_$_NSObject,
        0, // (void *)&_objc_empty_cache,
        0, // unused, was (void *)&_objc_empty_vtable,
        &_OBJC_METACLASS_RO_$_Person,
    };
    
    /*
     我们发现上面的_OBJC_METACLASS_RO_$_Person是在这里实现的;
     可以看到
     unsigned int flags; 值是1
     unsigned int instanceStart;可以看到内存初始位置就是_class_t结构体的长度
     unsigned int instanceSize;可以看到内存长度
     unsigned int reserved; 值是0
     const unsigned char *ivarLayout;也是0
     const char *name; 类名"Person"
     const struct _method_list_t *baseMethods; 这个有值,_OBJC_$_CLASS_METHODS_Person,我们可以看到初始化的时候保存的是类方法,也就是+加号方法
     const struct _objc_protocol_list *baseProtocols;也是0
     const struct _ivar_list_t *ivars;也是0
     const unsigned char *weakIvarLayout;也是0
     const struct _prop_list_t *properties;也是0
     
     我们取看看_method_list_t的值是在哪初始化的,这个一开始保存的是类方法,也就是+加号方法
     去找找类方法在哪实现的
     
     */
    static struct _class_ro_t _OBJC_METACLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        1,
        sizeof(struct _class_t),
        sizeof(struct _class_t),
        (unsigned int)0,
        0,
        "Person",
        (const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_Person,
        0,
        0,
        0,
        0,
    };
    
    /*
     类方法在这里实现的;我们发现类方法实现的方式和对象方法实现的是一样的,
     那就不多介绍了
     */
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[1];
    } _OBJC_$_CLASS_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"home", "v16@0:8", (void *)_C_Person_home}}
    };
    
    

    相关文章

      网友评论

        本文标题:Object-C ,runtime原理,oc对象的原理,主要记录

        本文链接:https://www.haomeiwen.com/subject/zucygltx.html