ios 底层面试一

225 阅读5分钟

前言

通过面试题检验下自己对底层掌握的怎么样吧,顺便也做个记录。

1. load的调用顺序?

父类>子类>分类

在博客dyld加载流程,我们详细分析过load_images,会先递归获取父类的load()方法存进列表,然后再获取子类的load()方法存进列表,最后获取分类的load()方法存进列表。所以调用顺序父类>子类>分类

2.能否向编译后的类中添加实例变量?能否向运行时创建的类中添加实例变量?

  • 不能向编译后的类中添加实例变量,因为编译后类的内存结构已经确定无法修改
  • 运行时动态创建的类还没有进入内存,可以添加实例变量

例如:

Class newperson=  objc_allocateClassPair(class_getSuperclass(objc_getClass("newPerson")), "newPerson", 0);
class_addIvar(newperson, "name", sizeof(NSString*),log2(alignof(NSString*)), @encode(NSString*));

3. [self class]和[super class]的区别

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGTeacher* lgter=[[LGTeacher alloc]init];
    }
    return 0;
}

输出结果:

**LGTeacher - LGTeacher**

分析:[self class] 打印LGTeacher能理解,但是[super class]怎么也是打印的LGTeacher呢?我们看源码class源码返回的是什么

- (Class)class {
    return object_getClass(self);
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

源码看出class方法返回的是对象self->isa,方法调用默认有两个隐藏参数idsel,id为消息接收者,此处的self应该就是消息接收者,只要知道消息接受者是谁,返回的就是谁。clang编译下LGTeacher.m文件看下底层编译实现

static instancetype _I_LGTeacher_init(LGTeacher * self, SEL _cmd) {

    self = ((LGTeacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("init"));

    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_40_l7v4nh0543xbbd0lfg8dy5g00000gn_T_LGTeacher_81f7d8_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));

    }
    return self;

}

[self class]在底层是通过objc_msgSend消息转发的,有id和SEL两个参数,id为消息接收者即为LGTeacher;[super class]在底层是通过objc_msgSendSuper消息转发的,有__rw_objc_super和SEL两个参数,看下oc源码rw_objc_super结构

struct objc_super {
    /// 消息接收者
    __unsafe_unretained _Nonnull id receiver;
    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    //父类
    __unsafe_unretained _Nonnull Class super_class;
#endif
};

objc_super结构体中receiver还是LGTeacher,还有一个父类super_class。objc_msgSendSuper的作用就是去父类查找方法仅此而已,并不是返回父类。所以初始化类的时候使用[super init],返回的并不是父类而是自己,只是调用父类的方法初始化自己,如果不用父类方法初始化自己就不能继承父类的属性

4. 内存平移和压栈

先看demo

- (void)viewDidLoad {
    [super viewDidLoad];
    LGPerson* p=[[LGPerson alloc]init];
    p.kc_name=@"kc";
    [p saySomething];

    
    Class cls=[LGPerson class];
    void* gy_p=&cls;
    [(__bridge  id)gy_p saySomething];
  }
  
@implementation LGPerson

- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.kc_name);

}

输出:

**-[LGPerson saySomething] - kc**

**-[LGPerson saySomething] - <LGPerson: 0x282075400>**

分析:

  • 为什么gy_p可以调用到saySomething方法呢?首先对象方法saySomething在类LGPerson中,对象p通过isa关联到类LGPerson,只要gy_p和p指向的是同一个类那么就可以调用类中方法,p是一个指向堆区LGPerson对象的指针,p本身作为变量是在栈区的。gy_p是一个指向cls的栈区变量,也就是说gy_p也指向LGPerson类。方法调用的本质是objc_msgSend消息发送,此处的接收者都是LGPerson类,所以gy_p可以调用到saySomething。
  • 为什么p调用saySomething,self.kc_name打印的是kc,但是gy_p调用self.kc_name打印的是LGPerson: 0x282075400>对象呢?因为p是对象,对象是需要开辟内存的,而gy_p只是栈区的指针,在栈区调用self.kc_name相当于gy_p指针平移,指针的大小是8个字节,栈区压栈的规则是先入后出,地址方向是高地址到低地址,gy_p指针的下一个地址就是p的地址,所以打印的是LGPerson对象。lldb调试如下:

image.png p的地址就是gy_p地址指针加8字节。

结构体压栈顺序

  • 结构体存在于栈区,结构体中后面的变量先入栈
struct kc_struct{

    NSNumber *num1;

    NSNumber *num2;

} kc_struct;

- (void)viewDidLoad {
    [super viewDidLoad];
    struct kc_struct kc={@(10),@(20)};
 }

lldb看下入栈情况 image.png num2的地址0x000000016b0ddb28大于num1的地址0x000000016b0ddb20,栈区是高地址到地址,地址高的先入栈,所以num2先入栈。

函数参数压栈顺序

  • 函数参数入栈顺序按照参数顺序入栈 image.png a、b、c在栈区中地址由高到低,a最先入栈,然后b,最后c。

6. 类方法动态方法决议为什么在后面还要实现 resolveInstanceMethod?

类方法存在元类(以对象方法形式存在), 元类的父类最终是 NSObject 所以我们可以通过resolveInstanceMethod 防止 NSObject 中实现了对象方法!

7.类方法存在哪里?为什么要这么设计

对象方法存储在类中,类方法存储在元类中; 对象方法是由类实例化出来的,类是由元类实例化出来的;

  • 底层不用对类方法和对象方法做区分,本质上都是对象方法
  • 这样设计更加的基于对象

8.一个类的类方法没有实现为什么可以调用 NSObject 同名对象方法

在方法快速查找时,会根据类的isa找到元类, 元类中没有改方法就会进入lookUpImpOrForward慢速查找。 在慢速查找流程中,会进行for递归查找,根据superclass查找到元类的父类,也就是根源类,而根元类的superclass指向类NSObject,所以会调用到 NSObject的同名对象方法。