OC底层原理探索之相关面试题一

699 阅读5分钟

全局表

三张全局表:关联对象表、 弱引用表 、引用计数表

void arr_init(void) 
{
    AutoreleasePoolPage::init();
    SideTablesMap.init(); //散列表
    _objc_associations_init();// 关联对象表
}

其中散列表里面包含:弱引用和引用计数表

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; //引用计数表
    weak_table_t weak_table;// 弱引用
}

关联对象移除时机

在函数objc_removeAssociatedObjects找到_object_remove_assocations(object, /*deallocating*/false);查找调用的方法链 object_dispose(id obj) ->objc_destructInstance -> _object_remove_assocations 关联对象跟obj绑定

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}

load方法在什么时候调用

load方法调用.png _objc_init里的_dyld_objc_notify_register(&map_images, load_images, unmap_image) load_images的方法里面,进行单个类的收集在一个类的load方法表和一个分类的load方法表 load_images -> prepare_load_methods -> schedule_class_load ->add_class_to_loadable_list -> add_category_to_loadable_list -> call_load_methods -> call_class_loads -> call_category_loads 调用顺序: objc内的C++函数(static_init的时候调用),也就是自启的c++函数 > load() > C++函数 > initialize()(第一次消息发送lookupImp的时候调用) > main 分类load的加载顺序,主要由编译的顺序决定

Runtime

runtime 是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能,运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时。平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代 码,RuntimeObject-C 的幕后⼯作者 。 比如 addMethods、 addPropertys、 rwe ​

⽅法的本质,sel和imp分别是什么

sel 是⽅法编号 ~ 在read_images 期间就编译进⼊了内存 imp 就是我们函数实现指针 ,找imp 就是找函数的过程 sel 就相当于书本的⽬录 tittle imp 就是书本的⻚码 查找具体的函数就是想看这本书⾥⾯具体篇章的内容 1:我们⾸先知道想看什么 (sel) 2:根据⽬录对应的⻚码 (imp) 3:翻到具体的内容 ⽅法的本质:发送消息 , 消息会有以下⼏个流程 1:快速查找 (objc_msgSend)~ cache_t 缓存消息 2:慢速查找~ 递归⾃⼰| ⽗类 ~ lookUpImpOrForward 3:查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod 4:消息快速转发~ forwardingTargetForSelector 5:消息慢速转发~ methodSignatureForSelector & forwardInvocation

能否向编译后的类中增加实例变量

不能,因为我们编译好的实例变量存储的位置在 ro,⼀旦编译完成,内存结构就完全确定就⽆法修改。可以添加属性和方法。

能否向运行时创建的类中添加实例变量

看时机,只要类没有注册到内存还是可以添加的.在方法objc_registerClassPair调用之前都可以。在源码中找到class_addIvar

BOOL class_addIvar(Class cls, const char *name, size_t size, 
                   uint8_t alignment, const char *type)
{
    bool result = YES;

    if (!cls) return NO;
    if (ISMETA(cls)) return NO;
    if (!(cls->info & CLS_CONSTRUCTING)) return NO;
    //...
}

!(cls->info & CLS_CONSTRUCTING)这个是个关键,只要这个成立,就不能添加实例变量,那么我们看下,这个条件什么时候成立。在objc_registerClassPair

void objc_registerClassPair(Class cls)
{
    //...
    // Clear "under construction" bit, set "done constructing" bit
    cls->info &= ~CLS_CONSTRUCTING;
    cls->ISA()->info &= ~CLS_CONSTRUCTING;
    cls->info |= CLS_CONSTRUCTED;
    cls->ISA()->info |= CLS_CONSTRUCTED;

    NXHashInsertIfAbsent(class_hash, cls);
}

[self class][super class]的区别以及原理分析

self是个形参名,而super是个关键字

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

这里的[self class]其实调用的是NSObject的class方法。就是发送消息objc_msgSend(void/* id self, SEL op, ... */ ),消息接受者是 self

- (Class)class {
    return object_getClass(self); // self是指隐藏参数(id self , sel _cmd)
}

super其实是个关键字objc_msgSendSuper(void/* struct objc_super *super, SEL op, ... */ )是个struct objc_super的结构体

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;//第一个去查找的类
};

[super class] 本质就是objc_msgSendSuper, 消息的接受者还是self ⽅法编号:class ,只是objc_msgSendSuper会更快直接跳过 self 的查找 objc_msgSendSuper在汇编的时候自动变成了objc_msgSendSuper2

内存平移问题

Person内有一个对象方法saySomething,下面这种写法是否能调起该方法

- (void)saySomething{
    NSLog(@"%s",__func__);
}
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [Person alloc];
    Class cls = [Person class];
    void  *kc = &cls;
    [person saySomething];
    [(__bridge id)kc saySomething];
}

打印输出,我们发现都能调起image.png

我们知道saySomething在Person类的data中,person可以通过isa找到类然后经过平移就可以得到data中的方法。而kc是一个指针,指向的是Person类的所以也能得到data中的方法。 我们重新修改一下saySomething的实现

@property (nonatomic, copy) NSString *name;
- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.name);
}

image.png

这里的[(__bridge id)kc saySomething];为什么会输出person的地址呢? person vs kc 区别: person是一个已经开辟了内存的对象,而kc只是一个指针而已,没有内存空间。person可以通过内存平移找到name(self+ offset)

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [Person alloc];
    Class cls = [Person class];
    void  *kc = &cls;
    [person saySomething]; // person + 0x8
    [(__bridge id)kc saySomething];// cls + 0x8
}

函数的栈帧是一个栈结构,我们找到cls+0x8的上一个对象是谁就ok了 image.png 所以这也解释出了上面为什么会打印出person。

压栈

在name之前新增一个属性,此时看下栈帧情况

@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, copy) NSString *name;
- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.name);
}
- (void)viewDidLoad {// 参数压栈(id self, sel _cmd)
    [super viewDidLoad];// 结构体压栈(objc, class)
    Person *person = [Person alloc];
    Class cls = [Person class];
    void  *kc = &cls;
    [person saySomething]; // person + 0x10
    [(__bridge id)kc saySomething];// cls + 0x10(16个字节因为新增了一个8字节的属性)
}

image.png 那么这个ox10到底指的哪里,这个viewDidLoad到底哪些东西压栈了 1.参数压栈(隐藏参数) 2.结构体压栈(super关键字)

结构体压栈

struct kc_struct{
    NSNumber *num1;
    NSNumber *num2;
} kc_struct;

image.png 结构体压栈情况,结构体压栈成员变量是是从后向前压栈。 结构体压栈.png

参数压栈

// 高地址 -> 低地址
void kcFunction(id p1, id p2){
    NSLog(@"%p\n",&p1);
    NSLog(@"%p",&p2);
}

image.png 所以参数压栈是从前往后压。

- (void)viewDidLoad {// 参数压栈(id self, sel _cmd)
    [super viewDidLoad];// 结构体压栈(receiver, class)
    Person *person = [Person alloc];
    Class cls = [Person class];
    void  *kc = &cls;
    [person saySomething]; // person + 0x10
    [(__bridge id)kc saySomething];// cls + 0x10(16个字节因为新增了一个8字节的属性)
}

重新回来这个代码,可以得出压栈顺序:

压栈.png

所以[(__bridge id)kc saySomething]打印的是ViewController