iOS进阶之路 (十)runtime 相关面试题

921 阅读9分钟

一. 什么是runtime

  1. runtime 是由C 和C++ 汇编 实现的一套API,为OC语言加入了面向对象运行时的功能。
  2. runtime是指将数据类型的确定由编译时推迟到了运行时。例如 extension - category 的区别:
  • extension可以添加实例变量,而category是无法添加实例变量。 因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。
  • extension在编译期决议(就是类的一部分),category在运行期决议。 extension在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,extension伴随类的产生而产生,亦随之一起消亡。
  • extension一般用来隐藏类的私有信息,无法直接为系统的类扩展,但可以先创建系统类的子类再添加extension。
  • extension和category都可以添加属性,但是category的属性不能生成成员变量和getter、setter方法的实现
  1. 平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代 码,runtimeObject-C 的幕后工作者

二. ro & rw

问题一:

能否向编译后的得到的类中添加成员变量?能否向运行时创建的类中添加成员变量?

  1. 不能向编译后的得到的类中添加成员变量。
  • 我们编译好的成员变量存储在ro -> ivarList中,一旦编译完成,内存机构就完全确定无法修改。
  1. 运行时创建的类只要还没有注册到内存可以添加成员变量变量

问题二:

为什么运行时创建的类注册到内存中就无法修改了呢?

我们先代码测试下。

  1. 先添加成员变量, 再将类注册到内存

  1. 先将类注册到内存,再添加成员变量

  1. objc_registerClassPairclass_addIvar底层到底干了什么?
/***********************************************************************
* objc_registerClassPair
* fixme
* Locking: acquires runtimeLock
**********************************************************************/
void objc_registerClassPair(Class cls)
{
    ...
    
    // Clear "under construction" bit, set "done constructing" bit
    cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);

    // Add to named class table.
    addNamedClass(cls, cls->data()->ro->name);
}
  • objc_registerClassPair会将flags设置为RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING状态
/***********************************************************************
* class_addIvar
* Adds an ivar to a class.
* Locking: acquires runtimeLock
**********************************************************************/
BOOL 
class_addIvar(Class cls, const char *name, size_t size, 
              uint8_t alignment, const char *type)
{
    ...
    
    // Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }

    ...
       
    return YES;
}
  • flags只有为 in-construction, 才能添加成员变量。

问题3:

如何动态添加属性呢?

定义一个property,在编译期间,编译器会生成实例变量getter方法setter方法,这些方法是通过自动合成(autosynthesize)的方式生成并添加到类中。那么我们动态添加属性就要添加实例变量getter方法setter方法

void akSetter(NSString *value){
    printf("%s/n",__func__);
}

NSString *akName(){
    printf("%s/n",__func__);
    return @"master NB";
}

void ak_class_addProperty(Class targetClass , const char *propertyName){
    
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
    objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String };  //variable name
    objc_property_attribute_t attrs[] = {type, ownership0, ownership,backingivar};

    class_addProperty(targetClass, propertyName, attrs, 4);
}

void ak_printerProperty(Class targetClass){
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 1. 动态创建类
        Class AKPerson = objc_allocateClassPair([NSObject class], "AKPerson", 0);
        
        // 2.1 添加property - rw
        ak_class_addProperty(AKPerson, "subject");
        // 2.2 添加成员变量 1<<aligment ivar - ro - ivarlist
        class_addIvar(AKPerson, "subject", sizeof(NSString *), log2(sizeof(NSString *)), "@");
        // 2.3 添加setter  +  getter 方法
        class_addMethod(AKPerson, @selector(setSubject:), (IMP)akSetter, "v@:@");
        class_addMethod(AKPerson, @selector(subject), (IMP)akName, "@@:");
        
        // 3. 注册到内存
        objc_registerClassPair(AKPerson);
        
        // 4. 开始使用
        id person = [AKPerson alloc];
        [person setValue:@"iOS" forKey:@"subject"];
        NSLog(@"%@",[person valueForKey:@"subject"]);

    }
    return 0;
}
  • 一些相关api的注释:
/**
 * 创建类对
 *superClass: 父类,传Nil会创建一个新的根类
 *name: 类名
 *extraBytes: 0
 *return:返回新类,创建失败返回Nil,如果类名已经存在,则创建失败
  objc_allocateClassPair(<#Class  _Nullable __unsafe_unretained superclass#>, <#const char * _Nonnull name#>, <#size_t extraBytes#>)
 */

/**
 *添加成员变量
 *
 *cls 往哪个类添加
 *name 添加的名字
 *size 大小
 *alignment 对齐处理方式
 *types 签名
 *
 *这个函数只能在objc_allocateClassPair和objc_registerClassPair之前调用。不支持向现有类添加一个实例变量。
 *这个类不能是元类。不支持在元类中添加一个实例变量。
 *实例变量的最小对齐为1 << align。实例变量的最小对齐依赖于ivar的类型和机器架构。对于任何指针类型的变量,请通过log2(sizeof(pointer_type))。
  class_addIvar(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
 */

/**
 *往内存注册类
 *
 * cls 要注册的类
 *
 * objc_registerClassPair(<#Class  _Nonnull __unsafe_unretained cls#>)
 */

/**
 *往类里面添加方法
 *
 *cls 要添加方法的类
 *sel 方法编号
 *imp 函数实现指针
 *types 签名
 *
 *class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
 */

/**
 *往类里面添加属性
 *
 *cls 要添加属性的类
 *name 属性名字
 *attributes 属性的属性数组。
 *attriCount 属性中属性的数量。
 *
 *class_addProperty(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#const objc_property_attribute_t * _Nullable attributes#>, <#unsigned int attributeCount#>)

三. 方法的本质是什么? SEL/IMP分别是什么?两者有什么联系?

  1. 方法的本质:发送消息 , 消息会有以下几个流程
  • 快速查找:objc_msgSend ~ cache_t 缓存消息
  • 慢速查找L:lookUpImpOrForward 递归自己和父类
  • 查找不到消息: resolveInstanceMethod 动态方法解析
  • 消息快速转发:forwardingTargetForSelector
  • 消息慢速转发:methodSignatureForSelector & forwardInvocation
  1. sel 是方法编号 ~ 在read_images 期间就编译进入了内存
  2. imp 是函数实现指针 ,找imp 就是找函数的过程
  3. sel 相当于书本的目录 tittle,imp 相当于书本的⻚码 查找具体的函数就是想看这本书里面具体篇章的内容
  • 我们首先知道想看什么 ~ tittle (sel)
  • 根据目录对应的⻚码 (imp)
  • 翻到具体的内容

四. isKindOfClass & isMemberOfClass

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        // 题目1
        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL re3 = [(id)[AKPerson class] isKindOfClass:[AKPerson class]];
        BOOL re4 = [(id)[AKPerson class] isMemberOfClass:[AKPerson class]];
        NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n", re1, re2, re3, re4);

        // 题目2
        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
        BOOL re7 = [(id)[AKPerson alloc] isKindOfClass:[AKPerson class]];
        BOOL re8 = [(id)[AKPerson alloc] isMemberOfClass:[AKPerson class]];
        NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n", re5, re6, re7, re8);
    }
    return 0;
}

先探索一下isKindOfClassisMemberOfClass 的源码

1. + isKindOfClass

+ (Class)class {
    return self;
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
  1. 这是一个类似于for (int i = 0; i < 3; i ++)的for循环
  • object_getClass得到当前类对象的元类,初始化tcls
  • 只要tcls有值就可以继续循环,当tcls为nil循环结束
  • 取得tcls的父类作为tcls的新值,继续下次循环
  1. isKindOfClass是循环不断获取self的isa指针以及超类的isa指针指向和cls做对比。

结论一:`+isKindOfClass 是元类及其元类的父类 vs cls

2. + isMemberOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
  • object_getClass得到当前类对象的元类,和类本身cls进行比较
  • 相较于+isKindOfClass少了父类的比较,因此+isMemberOfClass为YES时可以得到+isKindOfClass为YES

结论二:+isMemberOfClass是元类 vs cls

3. - isKindOfClass

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

结论3:-isKindOfClass是类本身及其父类 vs cls

4. - isMemberOfClass

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

结论4:-isMemberOfClass是拿实例对象的类(即当前类)vs cls作比较

5. 分析

  • isa 走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
  • 继承关系(实现):子类 -> 父类 -> NSObject -> nil。
  • 根元类的父类为NSObject。

结合 isa走位图 分析题目1:

  • NSObject的元类(根元类)NSObject类不相等,NSObject元类的父类(就是NSObject类)与`NSObject类``相等——YES
  • NSObject的元类(根元类)NSObject类不相等——NO
  • AKPerson的元类AKPerson类不相等,AKPerson元类的父类(根元类)AKPerson类不相等——NO
  • AKPerson的元类AKPerson类不相等——NO

题目二:

  • NSObject类NSObject类相等——YES
  • NSObject类NSObject类相等——YES
  • AKPerson类AKPerson类相等——YES
  • AKPerson类AKPerson类相等——YES

isKindOfClass 侧重于是不是一个类型或者款式;isMemberOfClass 侧重于是不是它的成员,更加固定死了。

五. [self class] & [super class]

AKStudentAKPerson的子类, 主程序初始化AKStudent会打印什么?

#import "AKStudent.h"

@implementation AKStudent

- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", NSStringFromClass([self class]));
        NSLog(@"[super class] = %@", NSStringFromClass([super class]));
    }
    return self;
}

@end

1. [self class]

[self class]就是发送消息objc_msgSend,消息接受者是 self,方法编号:class。

objc_msgSend(void /* id self, SEL op, ... */ )

2. [super class]

通过汇编和源码发现:[super class] 本质就是使用objc_msgSendSuperobjc_super发送消息,消息接受者是 self,方法编号:class。

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

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

我们可以用伪代码重写[super class]

objc_msgSend走的是消息查找的流程,会递归查找class方法;objc_msgSendSuper直接跳过 self 的查找,从objc_super结构体开始查找,更加效率。

六. 内存偏移

题目1

程序能否运行吗?是否正常输出?为什么?

@interface AKPerson : NSObject

@property (nonatomic, copy) NSString *name;

- (void)doSomething;

@end

@implementation AKPerson

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

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    id cls = [AKPerson class];
    void *obj= &cls;
    [(__bridge id)obj doSomething];
    
    AKPerson *p = [AKPerson alloc];
    [p doSomething];
}

  • 指针1:指针p指向实例对象的首地址(实例对象的首地址是isa
  • 指针2: 实例对象的isa指向类对象cls
  • 指针3:cls类对象指向AKPerson
  • 指针4: obj指向类对象cls

题目2

修改doSomething方法实现,会打印什么?

- (void)doSomething {
    NSLog(@"%s——————%@", __func__, self.name);
}

为什么会打印viewController呢?

那么我们就来看一下viewDidLoad里面总共有哪些局部变量,再贴一下代码

- (void)viewDidLoad {
    //    [super viewDidLoad];
    struct __rw_objc_super arg = {
        (id)self,
        (id)class_getSuperclass(objc_getClass("ViewController"))
    };
    objc_msgSendSuper(arg, @selector(viewDidLoad));
    
    id cls = [AKPerson class];
    void *obj= &cls;
    [(__bridge id)obj doSomething]; 
}

self.name相当于self->_name,因为_name是isa后面紧接着的成员变量,而_name是一个指针,占8个字节大小,因此self->_name实际上得到的就是从self所指向的内存地址往高地址偏移8个字节(跨过isa的大小)后的内存地址,指向一段8字节大小的内存空间,从而获得person对象的成员变量_name。

self.name所拿到的变量,就是图中cls下面的那8个字节,也就是当前方法的消息接受者self(ViewController实例对象),因此打印的结果是<ViewController: 0x7fce43e08aa0>。

函数的栈空间简介

对于arm64架构来说,栈空间的作用:

  • 存放被调用函数其内部所定义的局部变量的。
  • 局部变量的存放顺序,是根据定义的先后顺序,从函数栈底开始,一个一个排列,最先定义的局部变量位于栈底(高地址)。
int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    NSLog(@"\na = %p\nb = %p\nc = %p\nd = %p\n",&a,&b,&c,&d);
a = 0x7ffeefbfea8c
b = 0x7ffeefbfea88
c = 0x7ffeefbfea84
d = 0x7ffeefbfea80。

参考资料

RUNNING_NIUER - 面试题中的Runtime