美文网首页
iOS runtime——看这一篇就够了

iOS runtime——看这一篇就够了

作者: iOS丶lant | 来源:发表于2022-01-05 15:38 被阅读0次

    本文篇幅比较长,创作的目的为了自己日后温习知识所用,希望这篇文章能对你有所帮助。
    如发现任何有误之处,肯请留言纠正,谢谢。

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

    1、instance对象实例

    我们经常使用id来声明一个对象,那id的本质又是什么呢?查看objc/objc.h文件

    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    
    

    我们创建的一个对象或实例其实就是一个struct objc_object结构体,而我们常用的id也就是这个结构体的指针。

    这个结构体只有一个成员变量,这是一个Class类型的变量isa,也是一个结构体指针,isa指针就指向对象所属的类

    一个 NSObject 对象占用多少内存空间?
    一个NSObject实例对象只有一个isa指针,所以一个isa指针的大小,他在64位的环境下占8个字节,在32位环境上占4个字节。

     NSObject *obj = [[NSObject alloc] init];
     NSLog(@"class_getInstanceSize--%zd", class_getInstanceSize([NSObject class]));
    
    

    输出结果:

    class_getInstanceSize--8
    
    

    2、class object(类对象)/metaclass(元类)

    看结构体objc_class的定义

    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    
    
    • Class superclass;——用于获取父类,也就是元类对象,它也是一个Class类型
    • cache_t cache;——是方法缓存
    • class_data_bits_t bits;——用于获取类的具体信息,看到bits
    • class_rw_t data()函数,该函数的作用就是获取该类的可读写信息,通过class_data_bits_t的bits.data()方法获得,class_rw_t后面会介绍*
    class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    

    该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,我们称之为类对象。类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),元类中保存了创建类对象以及类方法所需的所有信息。

    3、isa指针与superclass相关逻辑图

    isa逻辑图

    4、总结 + 代码校验

    • 对象 的类(Superclass)是 类(对象) ;
    • 类(对象) 的类(Superclass)是 元类,和类同名;
    • 元类 的类(Superclass)是 根元类 NSObject;
    • 根元类 的类(Superclass)是 自己 ,还是NSObject;
    • 对象的isa指针指向类(对象) ;
    • 类对象的isa指针指向元类,和类同名;
    • 元类的isa指针指向跟根元类 NSObject;
    • 根元类 NSObject的isa指针指向自己。

    isa验证

        NSString *string = @"字符串";
        Class class1 = object_getClass(string);//NSString类对象
        Class metaClass = object_getClass(class1);//NSString元类
        Class rootMetaClass = object_getClass(metaClass);//根元类
        Class rootRootMetaClass = object_getClass(rootMetaClass);//根元类
        NSLog(@"%p 实例对象 ",string);
        NSLog(@"%p 类 %@",class1,NSStringFromClass(class1));
        NSLog(@"%p 元类 %@",metaClass,NSStringFromClass(metaClass));
        NSLog(@"%p 根元类 %@",rootMetaClass,NSStringFromClass(rootMetaClass));
        NSLog(@"%p 根根元类 %@",rootRootMetaClass,NSStringFromClass(rootRootMetaClass));
    
        Class rootMetaClass_superclass = rootMetaClass.superclass;//根元类的superclass
        NSLog(@"根根元类的superclass:%@",NSStringFromClass(rootMetaClass_superclass));
    
    

    输出结果:

    0x102d48078 实例对象 
    0x1d80e3d10 类 __NSCFConstantString
    0x1d80e3cc0 元类 __NSCFConstantString
    0x1d80c66c0 根元类 NSObject
    0x1d80c66c0 根根元类 NSObject
    根根元类的superclass:NSObject
    
    

    superclass验证

        NSString *string = @"字符串";
        Class class1 = object_getClass(string);//NSString类对象
        Class class2 = class1.superclass;
        NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class1),NSStringFromClass(class2));
        Class class3 = class2.superclass;
        NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class2),NSStringFromClass(class3));
        Class class4 = class3.superclass;
        NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class3),NSStringFromClass(class4));
        Class class5 = class4.superclass;
        NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class4),NSStringFromClass(class5));
        Class class6 = class5.superclass;
        NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class5),NSStringFromClass(class6));
    
    

    输出结果:

     __NSCFConstantString 的superclass是 __NSCFString
     __NSCFString 的superclass是 NSMutableString
    NSMutableString 的superclass是 NSString
    NSString 的superclass是 NSObject
    NSObject 的superclass是 (null)
    
    

    如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

    二、class_rw_t 与 class_ro_t

    1、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;
        }
    };
    
    
    • uint32_t instanceSize;——instance对象占用的内存空间
    • const char * name;——类名
    • const ivar_list_t * ivars;——类的成员变量列表

    class_ro_t存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,里面是没有分类的方法的。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。
    ro即表示read only,是无法进行修改的。

    2、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; // 指向只读的结构体,存放类初始信息
    
        /*
         这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
         这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
         */
        method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
        property_array_t properties; // 属性列表
        protocol_array_t protocols; //协议列表
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        //...
        }
    
    

    3、class_rw_t生成时机

    class_rw_t生成在运行时,在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

    类的realizeClass运行之前:


    然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:

    • 从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
    • 初始化一个 class_rw_t 结构体
    • 设置结构体 ro 的值以及 flag
    • 最后设置正确的 data。
    const class_ro_t *ro = (const class_ro_t *)cls->data();
    class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
    
    

    但是,在这段代码运行之后 class_rw_t 中的方法,属性以及协议列表均为空。这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。

    realizeClass 方法执行过后的类所占用内存的布局:


    细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。

    4、method_t

    上面我们剖析了class_rw_t、class_ro_t这两个重要部分的结构,并且主要关注了其中的方法列表部分,而从上面的分析,可发现里面最基本也是重要的单位是method_t,这个结构体包含了描述一个方法所需要的各种信息。

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

    变量介绍可以参考之前文章:iOS 代码注入—— hook 实践

    三、Runtime 初始化函数

    1、一"码"当先

    /***********************************************************************
    * _objc_init
    * Bootstrap initialization. Registers our image notifier with dyld.
    * Called by libSystem BEFORE library initialization time
    **********************************************************************/
    
    void _objc_init(void)
    {
        static bool initialized = false;
        if (initialized) return;
        initialized = true;
    
        // fixme defer initialization until an objc-using image is found?
        environ_init();
        tls_init();
        static_init();
        lock_init();
        exception_init();
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    }
    
    

    _dyld_objc_notify_register(&map_images, load_images, unmap_image)。这个函数里面的三个参数分别是另外三个函数:

    • map_images -- Process the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件)
    • load_images -- Process +load in the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件中的+load方法)
    • unmap_image -- Process the given image which is about to be unmapped by dyld.(处理那些将要被dyld进行去映射操作的镜像文件)

    我们查看一下map_images方法,点进去:

    /***********************************************************************
    * map_images
    * Process the given images which are being mapped in by dyld.
    * Calls ABI-agnostic code after taking ABI-specific locks.
    *
    * Locking: write-locks runtimeLock
    **********************************************************************/
    void
    map_images(unsigned count, const char * const paths[],
               const struct mach_header * const mhdrs[])
    {
        mutex_locker_t lock(runtimeLock);
        return map_images_nolock(count, paths, mhdrs);
    }
    
    

    四、分类底层原理

    根据map_images函数,继续点进去看,可以看到如下代码:

    // Discover categories. 
        for (EACH_HEADER) {
            category_t **catlist = 
                _getObjc2CategoryList(hi, &count);
            bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    
            for (i = 0; i < count; i++) {
                category_t *cat = catlist[i];
                Class cls = remapClass(cat->cls);
    
                if (!cls) {
                    // Category's target class is missing (probably weak-linked).
                    // Disavow any knowledge of this category.
                    catlist[i] = nil;
                    if (PrintConnecting) {
                        _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                     "missing weak-linked target class", 
                                     cat->name, cat);
                    }
                    continue;
                }
    
                // Process this category. 
                // First, register the category with its target class. 
                // Then, rebuild the class's method lists (etc) if 
                // the class is realized. 
                bool classExists = NO;
                if (cat->instanceMethods ||  cat->protocols  
                    ||  cat->instanceProperties) 
                {
                    addUnattachedCategoryForClass(cat, cls, hi);
                    if (cls->isRealized()) {
                        remethodizeClass(cls);
                        classExists = YES;
                    }
                    if (PrintConnecting) {
                        _objc_inform("CLASS: found category -%s(%s) %s", 
                                     cls->nameForLogging(), cat->name, 
                                     classExists ? "on existing class" : "");
                    }
                }
    
                if (cat->classMethods  ||  cat->protocols  
                    ||  (hasClassProperties && cat->_classProperties)) 
                {
                    addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                    if (cls->ISA()->isRealized()) {
                        remethodizeClass(cls->ISA());
                    }
                    if (PrintConnecting) {
                        _objc_inform("CLASS: found category +%s(%s)", 
                                     cls->nameForLogging(), cat->name);
                    }
                }
            }
        }
    
    

    根据代码:

    category_t *cat = catlist[i];
    
    

    一开始的那个catlist是一个二维数组,里面的成员也是一个一个的数组,也就是代码里面的cat所指向的数组,它的类型是category_t *,说明cat数组里面装的就是category_t,一个cat里面装的就是某个class所对应的所有category。

    那么什么决定了这些category_t在cat数组中的顺序呢?
    答案是category文件的编译顺序决定的。先参与编译的,就放在数组的前面,后参与编译的,就放在数组后面。我们可以在xcode-->target-->Build Phases-->Compile Sources列表查看和调整category文件的编译顺序

    加载分类的最后,执行方法:remethodizeClass(cls->ISA());

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
    
        runtimeLock.assertLocked();
    
        isMeta = cls->isMetaClass();
    
        // Re-methodizing: check for more categories
        if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
            if (PrintConnecting) {
                _objc_inform("CLASS: attaching categories to class '%s' %s", 
                             cls->nameForLogging(), isMeta ? "(meta)" : "");
            }
    
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    
    

    然后在这里面找到一个方法attachCategories,看名字就知道,附着分类,也就是把分类的内容添加/合并到class里面,感兴趣的可以自己查看一下这个方法,这个理就不做解释了。

    五、方法缓存

    1、数据结构

    它的底层是通过散列表(哈希表)的数据结构来实现的,用于缓存曾经调用过的方法,可以提高方法的查找速度。
    首先,回顾一下正常情况下方法调用的流程。假设我们调用一个实例方法[obj XXXX];

    • obj -> isa -> obj的Class对象 -> method_array_t methods -> 对该表进行遍历查找,找到就调用,没找到继续往下走
    • obj -> superclass -> obj的父类 -> isa -> method_array_t methods -> 对父类的方法列表进行遍历查找,找到就调用,没找到就重复本步骤
    • 直到NSObject -> isa -> NSObject的Class对象 -> method_array_t,如果还是没有找到就会crash

    如果XXXX方法在程序内会被频繁的调用,那么这种逐层便利查找的方式肯定是效率低下的,因此苹果设计了cache_t cache,当XXXX第一次被调用的时候,会按照常规流程查找,找到之后,就会被加入到cache_t cache中,当再次被调用的时候,系统就会直接现到cache_t cache来查找,找到就直接调用,这样便大大提升了查找的效率。

    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    }
    
    
    • struct bucket_t *_buckets; —— 用来缓存方法的散列/哈希表
    • mask_t _mask; —— 这个值 = 散列表长度 - 1
    • mask_t _occupied; —— 表示已经缓存的方法的数量

    _buckets散列表里面的存储单元是bucket_t,

    struct bucket_t {
    private:
        cache_key_t _key;
        IMP _imp;
    }
    
    
    • cache_key_t _key; —— 这个key实际上就是方法的SEL,也就是方法名
    • IMP _imp; —— 这个就是方法对应的函数的内存地址

    2、缓存逻辑

    • (1) 当一个对象接收到消息时[obj message];,首先根据obj的isa指针进入它的类对象class里面。
    • (2) 在obj的class里面,首先到缓存cache_t里面查询方法message的函数实现,如果找到,就直接调用该函数。
    • (3) 如果上一步没有找到对应函数,在对该class的方法列表进行二分/遍历查找,如果找到了对应函数,首先会将该方法缓存到obj的类对象class的cache_t里面,然后对函数进行调用。
    • (4) 在每次进行缓存操作之前,首先需要检查缓存容量,如果缓存内的方法数量超过规定的临界值(设定容量的3/4),需要先对缓存进行2倍扩容,原先缓存过的方法全部丢弃,然后将当前方法存入扩容后的新缓存内。
    • (5) 如果在obj的class对象里面,发现缓存和方法列表都找不到mssage方法,则通过class的superclass指针进入它的父类对象father_class里面
    • (6) 进入father_class后,首先在它的cache_t里面查找mssage,如果找到了该方法,那么会首先将方法缓存到消息接受者obj的类对象class的cache_t里面,然后调用方法对应的函数。
    • (7) 如果上一步没有找到方法,将会对father_class的方法列表进行遍历二分/遍历查找,如果找到了mssage方法,那么同样,会首先将方法缓存到消息接受者obj的类对象class的cache_t里面,然后调用方法对应的函数。需要注意的是,这里并不会将方法缓存到当前父类对象father_class的cache_t里面
    • (8) 如果还没找到方法,则会通过father_class的superclass进入更上层的父类对象里面,按照(6)->(7)->(8)步骤流程重复。如果此时已经到了基类对象NSObject,仍没有找到mssage,则进入步骤(9)

    如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

    六、消息转发

    第一步:Method resolution 方法解析处理阶段
    如果调用了对象方法首先会进行+(BOOL)resolveInstanceMethod:(SEL)sel判断
    如果调用了类方法 首先会进行 +(BOOL)resolveClassMethod:(SEL)sel判断
    两个方法都为类方法;

    + (BOOL)resolveClassMethod:(SEL)sel {
        ///这里动态添加方法
        return YES;
    }
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        ///这里动态添加方法
        return YES;
    }
    
    

    _class_resolveInstanceMethod源码解析

    /***********************************************************************
    * _class_resolveInstanceMethod
    * Call +resolveInstanceMethod, looking for a method to be added to class cls.
    * cls may be a metaclass or a non-meta class.
    * Does not check if the method already exists.
    **********************************************************************/
    static void _class_resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        SEL resolve_sel = @selector(resolveInstanceMethod:);
    
        if (! lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA())) {
            // Resolver not implemented.
            return;
        }
    
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(cls, resolve_sel, sel);
    
        // Cache the result (good or bad) so the resolver doesn't fire next time.
        // +resolveInstanceMethod adds to self a.k.a. cls
        IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    
        if (resolved  &&  PrintResolving) {
            if (imp) {
                _objc_inform("RESOLVE: method %c[%s %s] "
                             "dynamically resolved to %p", 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel), imp);
            }
            else {
                // Method resolver didn't add anything?
                _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                             ", but no new implementation of %c[%s %s] was found",
                             cls->nameForLogging(), sel_getName(sel), 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel));
            }
        }
    }
    
    

    从runtime的源码,resolveInstanceMethod的返回值对于消息转发流程没有任何意义,这个返回值只和debug的信息相关。
    这两个方法是最先走到的方法,可以在这两个方法中动态的添加方法,进行消息转发。这里有一个需要特别注意的地方,类方法需要添加到元类里面,原因这里就不赘述了。

    **第二步:Fast forwarding 快速转发阶段 **

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return [xxx new];
    }
    
    

    这个里可以快速重定向成其他对象,已经让备用的对象去响应了该对象本身无法响应的一个SEL

    第三步:Normal forwarding 常规转发阶段

    //返回方法签名
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        if ([NSStringFromSelector(aSelector) isEqualToString:@"xxx"]) {
            return [[xxx new] methodSignatureForSelector:aSelector];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    //处理返回的方法签名
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
        if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"xxx"]) {
            [anInvocation invokeWithTarget:[xxx new]];
        }else{
            [super forwardInvocation:anInvocation];
        }
    }
    
    

    自动签名

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        //如果返回为nil则进行自动签名
       if ([super methodSignatureForSelector:aSelector]==nil) {
            NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
            return sign;
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
        //创建备用对象
        xxx * backUp = [xxx new];
        SEL sel = anInvocation.selector;
        //判断备用对象是否可以响应传递进来等待响应的SEL
        if ([backUp respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:backUp];
        }else{
           // 如果备用对象不能响应 则抛出异常
            [self doesNotRecognizeSelector:sel];
        }
    }
    
    ////触发崩溃
    - (void)doesNotRecognizeSelector:(SEL)aSelector {
    
    }
    
    

    七、super的本质

    1、定义

    • super—— 是一个指向结构体指针struct objc_super *,它里面的内容是{消息接受者 recv, 消息接受者的父类类对象 [[recv superclass] class]},objc_msgSendSuper会将消息接受者的父类类对象作为消息查找的起点。

    2、流程
    [obj message] -> 在obj的类对象cls查找方法 -> 在cls的父类对象[cls superclass]查找方法 -> 在更上层的父类对象查找方法 -> ... -> 在根类类对象 NSObject里查找方法

    [super message] -> 在obj的类对象cls查找方法(跳过此步骤) -> (直接从这一步开始)在cls的父类对象[cls superclass]查找方法 -> 在更上层的父类对象查找方法 -> ... -> 在根类类对象 NSObject里查找方法

    3、实例

     NSLog(@"[self class] = %@",[self class]);
    
    
    • 接受者 当前class实例对象
    • 最终调用的方法:基类NSObject的-(Class)class方法
     NSLog(@"[super class] = %@",[super class]);
    
    
    • 接受者 当前class实例对象
    • 最终调用的方法:基类NSObject的-(Class)class方法
     NSLog(@"[self superclass] = %@",[self superclass]);
    
    
    • 接受者 当前class实例对象
    • 最终调用的方法:基类NSObject的-(Class) superclass方法
     NSLog(@"[super superclass] = %@",[super superclass]);
    
    
    • 接受者 当前class实例对象
    • 最终调用的方法:基类NSObject的-(Class) superclass方法

    因此 [self class] [super class] [self superclass] [super superclass] 的值都相等

    至此,runtime相关的知识点全部总结完毕,该文章将会持续更新迭代!!
    看到就是缘分😁,如发现任何有误之处,肯请留言纠正,谢谢。

    相关文章

      网友评论

          本文标题:iOS runtime——看这一篇就够了

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