iOS runtime—看这一篇就够了

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

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

image

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相关逻辑图
image

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运行之前:
image

然后在加载 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 方法执行过后的类所占用内存的布局:
image

细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于: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相关的知识点全部总结完毕,该文章将会持续更新迭代!!
看到就是缘分😁,如发现任何有误之处,肯请留言纠正,谢谢。

posted @ 2022-01-22 15:16  Julday  阅读(203)  评论(0编辑  收藏  举报