底层原理-17-oc底层面试题

512 阅读7分钟

主要看下底层面试巩固下所学。

1.+load方法在什么时候调用?

load_images的时候调用,通过prepare_load_methods方法准备,递归的方式schedule_class_load进行添加add_class_to_loadable_list加入实现+load方法的表里。分类要实现+load方法的分类加入add_category_to_loadable_list表里。准备好数据后开始调用call_load_methods()来实现+load方法。该方法分为主类call_class_loads和分类call_category_loads来实现。过程如下图:

image.png

2. 关于+load和+initialize以及C++的静态构造函数调用顺序?

  • +load调用时通过load_images的时候进行调用,通过call_load_methods实现类或者分类的的+load方法。
  • +initialize实现是实现它的类或者分类第一次发送消息的时候调用。
  • C++的静态构造函数通常是类初始化完成后调用,但是在底层源码中objc_init()会先调用static_init自己调用。 +load方法调用顺序先主类后分类,分类之间看编译顺序调用先后;主类和分类方法同名的时候,因为分类方法是在主类实现后加入的,所以优先调用分类的方法。

3.runtime是什么?

Runtime时数据类型的确定由编译时推迟到运行时,runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象,运行时功能
例如:通常正常写的extension拓展就是编译时,编译的时候就确定了方法和属性加入到了类的ro中,而category分类则是通常情况下调用的时候才去添加到类的rw_e的方法列表 里。
平时写的oc代码,在程序运行过程中,最终会转换成Runtime的C语言代码,runtime就是oc的幕后实现者

4.方法的本质,sel,IMP是什么?有什么关系?

  • 方法本质是发送消息objc_msgSend,消息的发送有一下几步:
  1. 快速查找,缓存中查找。
  2. 没找到进入慢速查找lookupImpOrForward,递归自己的父类链。
  3. 还是没找到进入方法动态决议:查看resolveInstanceMethodresolveClassMethod是否实现。
  4. 依然没找到进入消息快速转发,forwardingTargetForSelctor去看看有没有别的类帮他实现。 5.没找到进入慢速转发,实现方法签名methodSignatureForSelectorforward Invocation找人实现。
  • sel是方法编号,在read_images期间编译进内存;imp就是具体实现的指针,找imp就是找函数的过程。
  • 它们关系通常一一对应,method就是由selimp组成。比如我们要读一本书的具体内容,我们查看目录,sel就是目录的标题imp就是具体的页数也就是实现地址。我们先要知道读什么内容(sel就是标题),通过标题找到对应的页数(imp),根据页数去读具体内容(方法实现)

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

不能向编译后的类中增加实例变量;可以向运行时创建的类添加实例变量。
原因:只要还没有注册到内存就可以修改,编译好的实例变量储存在ro,一旦编译完成就不会改变了,但是可以运行时添加属性和方法

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

  • [self class]就是发送消息objc_msgSend,消息的接受者是self,方法编号:class
  • [super class]本质是objc_msgSendSuper,消息的接受者还是self,方法编号:class

image.png 最终都是self的类。就向上面所说的本质都是发送消息objc_msgSendSuper只是去父类中查找class的方法,调用还是self。super只是编译器一个特殊字符,并不代表父类的一个实例化对象。调用的主体还是selfimage.png [super class]在编译的时候是objc_msgSendSuper,在运行的时候是objc_msgSendSuper2(这里可以通过汇编跟踪一下),通过objc_msgSendSuper的汇编查看,在里面有跳转到objc_msgSendSuper2的代码。

image.png objc_msgSendSuper2会去查询当前传入的class,而不是superclass

7.Runtime是如何实现weak的?为神马可以置为nil?

1.通过SideTable找到我们的weak_table
2.weak_table根据referent找到或创建weak_entry_t 3.然后append_refereer(entry, referrer)将我们新的弱引用的对象加进去entry
4.最后weak_entry_insertentry加入我们的weak_table.

8.Runtime Associate方法关联的对象,需要在dealloc中释放吗?

当我们释放对象的时候dealloc
1.c++函数释放:object_cxxDestruct
2.移除关联属性:_object_remove_associations
3.将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this)
4.引用计数处理:table.refcnts.erase(this)
5.销毁对象:free(obj)
因此关联对象不需要我们手动释放,将在dealloc中释放。
源码中dealloc实现进入_objc_rootDealloc,看下rootDealloc

image.png 里面有关联属性设置的bool值,当有这些条件的时候进入else流程。

image.png object_dispose主要是销毁实例对象

image.pngobjc_destructInstance中就有进行关联属性的移除操作

image.png 具体的移除操作,和我们之前探索的关联对象属性类似,只是过程是相反的。

9.内存平移问题

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

通过上面的方法是否可以调用实例方法?为什么?

image.png 结果是可以调用的,我们知道方法的本质是消息发送objc_msgSend。我们之前探究知道实例对象的isa指向该对象的,实例方法就是先进行在当前类的cache_t中快速查找,没找到进行慢速查找,也就是我们方法查找流程。
而我们通过 &cls获取当前类的地址,相当于实例对象根据它的isa获取类的地址,之后方法查找一样的流程,找到方法的imp,进行消息发送

方法saySomething里面有属性 self.kc_hobby 的打印

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString * kc_name;

@property (nonatomic, copy) NSString *kc_hobby;  // 12

- (void)saySomething;

@end
- (void)saySomething{ 
    NSLog(@"%s - %@",__func__,self.kc_hobby);
}
    Class cls = [LGPerson class];
    void  *kc = &cls;
    [(__bridge id)kc saySomething]; 
    LGPerson *person = [LGPerson alloc];
    [person saySomething];
   

打印结果 image.png 并没有给kc_hobby赋值?为什么实例调用有值,但是却是viewController,而类方法中没有?
我们知道self.kc_hobby,通过getter方法获取,实例对象是由isa和成员变量组成,获取属性的值的时候是进行实例对象进行偏移得到的,我们clang下自定义的文件得到:

image.png 我们在源码中看下objc_getProperty的实现,和我们clang编译的一样的。 image.png 因此我们在[person saySomething]中调用self.kc_hobby是LGPerson实例对象开辟的内存进行偏移获取值的。而[(__bridge id)kc saySomething] 中kc只是伪装的对象,并没有实际开辟内存空间,因此获取不是当前对象的属性。
kc是一个指针,存在栈中的,栈遵循先进先出,因此参数传入就是一个不断压栈的过程。
1.其中隐藏参数会压入栈,每个函数都有2个隐藏参数(id self,sel _cmd)
2.栈中,地址是衰减的,从高地址到底地址进行分配。因此在栈中,参数会从前往后进行压栈
3.super我们知道消息发送是通过objc_msgSendSuper或者objc_msgSendSuper2其中第一个结构体,结构体是怎么压栈的

image.png 结构体objc_super: image.png 我们写一个结构体,打印一下里面地址

image.png 在结构体内部在堆中,地址是从低到高,0x00007ffee3635438<--0x00007ffee363544010<-20;而外部地址是高到低:0x00007ffee3635448->0x00007ffee3635438即person<——>str
因此栈中顺序self-->_cmd-->class_getSuperclass-->self-->cls-->kc-->person
我们把它打印出来看下

   [super viewDidLoad];

    Class cls = [LGPerson class];

    void  *kc = &cls;

    LGPerson *person = [LGPerson alloc];

    NSLog(@"%p - %p",&person,kc);

    // 隐藏参数 会压入栈帧

    void *sp  = (void *)&self;//取当前开始的栈地址

    void *end = (void *)&person;//结束

    long count = (sp - end) / 0x8;//指针8字节压栈

    for (long i = 0; i<count; i++) {

        void *address = sp - 0x8 * i;

        if ( i == 1) {

            NSLog(@"%p : %s",address, *(char **)address);//cmd

        }else{

            NSLog(@"%p : %@",address, *(void **)address);

        }

    }

    [(__bridge id)kc saySomething];

    [person saySomething];

打印一下看下结果,因为kc是伪装的实例对象,因此系统也将它平移0x8获取属性,结果获取的是self(viewcontroller)

image.png 大致流程

未命名文件-3.jpg

10. Method-Swizzling方法交换的应用和坑点

method-swizzling是方法交换,主要作用是运行时替换方法,把一个方法换成另一个方法实现。通常所知的iOS黑魔法

  • oc中就是利用方法交换实现AOP面向切面编程。AOP时进行方法封装,提取公共部分,提高复用率。比如我们方法崩溃处理,埋点处理等都是AOP的体现。
  • OOP倾向业务逻辑的封装,面向对象编程 方法Method时由selimp组成,方法交换就是把2个方法原本对应的sel-imp断开重新组合正常情况:

image.png 交换后

image.png

坑点1:method-swizzling使用过程中的一次性问题

method-swizzling方法交换写在+load中,但是+load方法可能会主动调用多次,就会重复交换,可能会使方法还原,导致交换失败。
解决方法:使用单例模式,只走一次 dispatch_once_t实现

+ (void)load{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];

    });

}

坑点2:子类没有实现,父类实现了

定义父类LGPseosn

@interface LGPerson : NSObject

- (void)personInstanceMethod;

+ (void)personClassMethod;

@end
@implementation LGPerson

- (void)personInstanceMethod{

    NSLog(@"person对象方法:%s",__func__);

}

+ (void)personClassMethod{

    NSLog(@"person类方法:%s",__func__);

}

@end

定义子类LGStudent继承LGPerson,并在它的分类中实现方法交换

@implementation LGStudent (LG)

+ (void)load{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];

    });

}


// personInstanceMethod 我需要父类的这个方法的一些东西

// 给你加一个personInstanceMethod 方法

// imp

- (void)lg_studentInstanceMethod{

    [self lg_studentInstanceMethod];

    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);

}
@end
******************************************
@implementation LGRuntimeTool

+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{

    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);

    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

    method_exchangeImplementations(oriMethod, swiMethod);

}
@end

在ViewController中调用

- (void)viewDidLoad {

    [super viewDidLoad];

    // 黑魔法坑点二: 子类没有实现 - 父类实现

    LGStudent *s = [[LGStudent alloc] init];

    [s personInstanceMethod];


    LGPerson *p = [[LGPerson alloc] init];

    [p personInstanceMethod];

}

运行报错,方法没找到

image.png

  • student调用父类方法没问题,但是在分类+load中进行了方法交换替换成了lg_studentInstanceMethod,所以实际上调用的lg_studentInstanceMethod而在该方法中我们又调用了[self lg_studentInstanceMethod],但是没有递归。因为此时lg_studentInstanceMethod方法的imp交换后指向了personInstanceMethod的实现。
  • 当我们父类调用它自己的方法personInstanceMethod由于方法交换imp实现实际上是lg_studentInstanceMethod的imp,父类没法继承子类的方法,因此imp找不到就方法报错。
  • 解决:我们子类替换了父类的方法的imp,所以想不影响父类的情况又想子类方法交换到父类。我们可以先判断子类是否有父类的方法,没有的话我自己把父类方法添加一份到自己的方法列表,这个时候在进行交换,就不会影响父类了。
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{

    

    if (!cls) NSLog(@"传入的交换类不能为空");

    // oriSEL       personInstanceMethod

    // swizzledSEL  lg_studentInstanceMethod

    

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);

    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

   

    // 尝试添加。相当于当前的添Student添加lg_studentInstanceMethod

    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)

        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

    }else{ // 自己有

        method_exchangeImplementations(oriMethod, swiMethod);

    }
}

坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?

- (void)viewDidLoad {

    [super viewDidLoad];

    LGStudent *s = [[LGStudent alloc] init];

    [s helloword];
    ******************分类****************
    @implementation LGStudent (LG)

+ (void)load{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];

    });

}

- (void)lg_studentInstanceMethod{

    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);

    [self lg_studentInstanceMethod];

}

@end

我们运行会进行死循环,最终会导致内存溢出。

image.png helloword我们没有实现因此没有imp,交换的时候始终找不到oriMethod,交换了寂寞。当我们把hellowordimp换成了lg_studentInstanceMethod的,lg_studentInstanceMethod自己的imp却是空的了。当进入lg_studentInstanceMethod中没有指向oriMethod,所以就会自己掉自己造成死循环

  • 解决:如果方法没有实现就没有交换的必要了,我们可以自己指向一个imp就行操作。

+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{    

    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);

    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

    

    if (!oriMethod) {

        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:

        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));

    }
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败

    // 交换自己没有实现的方法:

    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)

    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL

    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

    if (didAddMethod) {

        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

    }else{

        method_exchangeImplementations(oriMethod, swiMethod);

    }

}