美文网首页interview
OC底层原理探索—经典面试题原理

OC底层原理探索—经典面试题原理

作者: 十年开发初学者 | 来源:发表于2021-07-26 17:01 被阅读0次

    1.loadinitialize方法的调用原则和调用顺序?

    load
    • load方法在应用程序加载过程中(dyld)完成调用,在main之前
    • 在底层进行load_images处理时,维护了两个load的加载表,一个是本类的表,另一个是分类的表,所以说有先对本类的load发起调用
    • 在对类 load方法进行处理时,进行递归处理,以确保父类优先被处理
    • load方法的调用顺序是父类、子类、分类
    • 在分类中load调用顺序,是根据编译的顺序为准
    initialize
    • initialize是在第一次消息发送的时候进行调用,load先于initialize
    • 分类中实现initialize方法会被优先调用,并且本类中的initialize不会被调用,
    • initialize原理是消息发送,所有当子类没有实现时,会调用父类还会被调用两次
    • 如果子类,父类同时实现,先调用父类,在调用子类
    c++构造函数
    • 在分析dyld后,可以确定这样个调用流程load->c++->main
    • 但是如果c++写在objc工程中,在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_register向dyld注册load_images之后再调用
    • 同时,如果objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况

    方法的本质

    • 方法的本质:发送消息

    消息发送的流程

    • 首先进入快速查找也就是通过objc_msgSend去缓存(cache_t)中查找
    • 慢速查找:通过递归自己或者父类查找,也就是lookupImporForward方法
    • 动态方法解析:resolveInstanceMethod
    • 消息快速转发:forwardingTargetForSelector
    • 消息慢速转发:methodSignatureForSelectorforwardInvocation

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

    1.不能向编译后得到的类增加实例变量

    • 首先编译好的实例变量存储在ro中,一旦完成编译,内存结构确定
    • 可以通过分类以关联对象形式向类中添加分类和属性
    1. 可以向运⾏时创建的类中添加实例变量
    • 可以通过objc_allocateClassPair运行时创建类,并添加属性、实例变量、方法等
    `        const char *className = "SHObject";
            Class objc_class = objc_getClass(className);
            if (!objc_class) {
                Class superClass = [NSObject class];
                objc_class = objc_allocateClassPair(superClass, className, 0);
            }
            class_addIvar(objc_class, "name", sizeof(NSString *), log2(_Alignof(NSString *)),  @encode(NSString *));
            class_addMethod(objc_class, @selector(addName:), (IMP)addName, "V@:");
    

    [self class]和[super class]区别

    来看下下面案例,LGTeacher类继承自LGPerson,在LGTeacher的init初始化方法中,调用了[self class]和[super class],结果会是什么

        // LGPerson
        @interface LGPerson : NSObject
        @end
    
        @implementation LGPerson
        @end
        
        // LGTeacher
        @interface LGTeacher : LGPerson
        @end
    
        @implementation LGTeacher
        - (instancetype)init{
            self = [super init];
            if (self) {
               NSLog(@"%@ - %@", [self class], [super class]);
            }
            return self;
        }
        @end
    

    首先这两个类中都没有实现class方法,那么根据继承关系,他们最终会调用到NSObject中的class方法

    - (Class)class {
        return object_getClass(self);
    }
    

    由上图可知这两个方法返回的self对应的类。关于这个self是谁,这里涉及到消息发送objc_msgSend,有两个隐形参数,分别是id self 和 SEL sel,这里SEL sel没啥好说的,主要来说下id self

    • [self class]输出LGTeacher,这里消息的发送者是LGTeacher对象,通过调用NSObject 的 class,但是消息的接受者没有发生变化,所以是LGTeacher
    • [super class]这里的输出仍然是LGteacher,至于为什么,我们来clang一下,查看下cpp文件
      image.png
      通过上图我们看到[super class]的低层实现时objc_msgSendSuper方法,同时存在存在id self 和 SEL sel两个隐形参数
    /// Specifies the superclass of an instance. 
    struct objc_super {
        /// Specifies an instance of a class.
        __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
        /* super_class is the first class to search */
    };
    

    我们来查看下objc_super结构体

    由上图知:id receiverClass super_class两个参数,其中super_class表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiver是LGTeacher对象,super_class是LGTeacher的父类,也就是要第一个查找的类。

    内存偏移

    案例1

    创建一个person类

    @interface Person : NSObject
    @property (nonatomic,copy)NSString *name;
    
    
    - (void)say1;
    @end
    
    @implementation Person
    
    - (void)say1{
        NSLog(@"%s",__func__);
    }
    
    @end
    
    

    viewDidload中添加以下代码

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        Person *person = [[Person alloc] init];
        [person say1];
        
        Class cls = [Person class];
        void *sh  =&cls;
        [(__bridge  id)sh say1];
        
    }
    

    查看下打印


    image.png

    分析:

    • 这两处调用的本质objc_msgSend的调用,在汇编源码中进行方法的快速查找
    • [person say1];通过person对象的isa指针找到对应的类,在类中进行地址平移,首先在cache_t中快速查找,如果找不到,则在类或者父类的方法列表遍历查找
    • [(__bridge id)sh say1],这里能够调用成功的原因是,Class cls = [Person class];cls是一个指针,指向一个objc_class指针,这里是指向Person类,将cls地址赋给sh,shcls的地址,也是指向类
    image.png
    由上图知,sh是指向Person类的 image.png

    总结:

    • person对象里面的isa指向Person类,通过内存平移的方式找到say1方法
    • sh指向clscls指向Person类,同样通过首地址平移找到say1
    案例2

    接着上面的案例,我们新增一个属性打印

    @implementation Person
    
    - (void)say1{
        NSLog(@"%s,%@",__func__,self.name);
    }
    
    @end
    

    查看打印


    image.png

    首先我们先要了解,取出属性的值,其实是要先计算出偏移大小,在通过内存平移获取值。其实是Person类内部存储着成员变量,每次偏移8字节进行存取

    至于sh打印的self.name的值是Person;0X600...,是因为cls只有Person类的内存首地址,但是没有person对象的内存结构,所以sh只能在栈里面进行内存平移。

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        Class cls = [Person class] ;
        
        void *sh  =&cls;
        [(__bridge  id)sh say1];
        Person *person = [[Person alloc] init];
        [person say1];
        
        
        // 下面代码为打印栈结构
        void *sp = (void *)&self;
        void *end = (void *)&person;
        long count = (sp - end) / 0x8;
        
        for (long i = 0; i < count; i++) {
            void *address = sp - 0x8 * I;
            if (i == 1) {
                NSLog(@"%p : %s",address, *(char **)address);
            } else {
                NSLog(@"%p : %@",address, *(void **)address);
            }
        }
    }
    
    

    看下栈结构打印


    image.png
    • 0x16f35ffb8 : <ViewController: 0x136906d30>viewDidload第一个隐形参数id self
    • 0x16f35ffb0 : viewDidLoad``是viewDidload第二个隐形参数SEL _cmd`
    • 0x16f35ffa8 : ViewController,这个是结构体class压栈
    • 0x16f35ffa0 : <ViewController: 0x136906d30> 为结构体receiver压栈
    • Personsh压栈
    • 0x16f35ff90 : <Person: 0x16f35ff98>person压栈

    什么可以压栈

    • 方法的参数viewDidLoad的(id self, SEL _cmd)。
    • 结构体参数objc_super,相当于下边这块代码创建了一个sh_objc_super的临时变量,所以也可以压栈。
    struct objc_super sh_objc_super;
    sh_objc_super.super_class = class;
    sh_objc_super.receiver = receiver;
    
    • 临时变量即:sh、person

    相关文章

      网友评论

        本文标题:OC底层原理探索—经典面试题原理

        本文链接:https://www.haomeiwen.com/subject/gefimltx.html