OC底层面试

关联对象补充

上节课我们在探索关联对象设置流程,在_object_set_associative_reference方法源码中看到析构函数AssociationsManager manager;,这里有疑问?它为什么不是一个单例?下面进行证明它为什么不是单例...

  • 进入_object_set_associative_reference方法,编写如下代码
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
// 这里多创建一个manager2     
AssociationsManager manager2;
AssociationsHashMap &associations2(manager2.get());

直接运行objc4-818.2源码会崩溃,原因是重复加锁AssociationsManager() { AssociationsManagerLock.lock(); }

image.png

解决办法 先把class AssociationsManager里面的加锁、解锁屏蔽掉,如下所示

// 修改前
class AssociationsManager {
    using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
    // 这里的static只是声明init方法是个类方法,并不是单例
    static void init() {
        // 这里面全局存储着3张表,关联对象表  AutoreleasePool表  散列表
        _mapStorage.init();
    }
};
// 修改后
class AssociationsManager {
    using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   {  }
    ~AssociationsManager()  {  }
  • 重新运行工程,进行lldb调试
image.png
(lldb) p &manager
(objc::AssociationsManager *) $0 = 0x00007ffeefbff350
(lldb) p &manager2
(objc::AssociationsManager *) $1 = 0x00007ffeefbff338

由打印信息可知AssociationsManager manager;不是单例

  • _mapStorage.init();全局存储着3张表关联对象表AutoreleasePool表散列表,下面进行断点调试
image.png

void arr_init(void) 
{
    AutoreleasePoolPage::init(); //AutoreleasePool表
    SideTablesMap.init();  //散列表(里面包含两张表,弱引用表 引用计数表)
    _objc_associations_init(); //关联对象表
}

关联对象释放

关联对象是在什么时候释放的?
关联对象也是需要移除的,关联对象的生命周期跟着object一起的。下面反向推导什么时候调用objc_removeAssociatedObjects?

  • 全局搜索什么时候调用objc_removeAssociatedObjects没有找到,那就寻找对象释放函数dealloc
// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}
  • 进入_objc_rootDealloc方法
void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
  • 继续进入rootDealloc方法
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?
    // 判断弱引用表,关联对象表
    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
  • 进入object_dispose方法
id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}
  • 进入objc_destructInstance方法
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;
}

下面查看关联对象整个数据结构?

image.png
(lldb) p refs_result
(std::pair, objc::DenseMap, objc::DenseMapInfo, objc::detail::DenseMapPair >, objc::DenseMapValueInfo, objc::DenseMapInfo, objc::detail::DenseMapPair > >, 
// 查看DenseMapInfo
objc::DenseMapInfo >, objc::detail::DenseMapPair, objc::DenseMap, objc::DenseMapInfo, 
// 查看DenseMapPair
objc::detail::DenseMapPair > >, false>, bool>) $0 = {
  // 第一指针
  first = {
    Ptr = 0x0000000100717900
    End = 0x0000000100717980
  }
  // 第二指针
  second = true
}
  • 下面查看底层源码是怎么包装数据结构?
template <
    typename KeyT, typename ValueT,
    typename ValueInfoT = DenseMapValueInfo,
    typename KeyInfoT = DenseMapInfo,
    typename Bucket = detail::DenseMapPair,
    bool IsConst = false>

OC底层简单面试题

面试题一:load方法在什么时候调用?
load_images 分析

load_images方法的主要作用是加载镜像文件,其中最重要的有两个方法:prepare_load_methods(加载) 和 call_load_methods(调用)

  • 进入load_images源码实现
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();//加载所有分类
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods 发现load方法
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods(); //调用load方法
}
  • 进入prepare_load_methods -> schedule_class_load源码,这里主要是根据类的继承链递归调用获取load,直到cls不存在才结束递归,目的是为了确保父类的load优先加载
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);
    // 添加到表里边
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • 进入add_class_to_loadable_list,主要是将load方法cls类名一起加到loadable_classes表中
  • 进入getLoadMethod,主要是获取方法的sel为load的方法

进入call_load_methods源码,主要有三部分操作

  • 反复调用类的+load,直到不再有
  • 调用一次分类的+load
  • 如果有类或更多未尝试的分类,则运行更多的+load

initialize

  • initialize是在第一次消息发送的时候进行调用,load先于initialize
  • 分类中实现initialize方法会被优先调用,并且本类中的initialize不会被调用,
  • initialize原理是消息发送,所有当子类没有实现时,会调用父类。是会被调用两次
  • 如果子类,父类同时实现,先调用父类,再调用子类

load

  • load方法在应用程序加载过程中(dyld)完成调用,在main之前
  • 在底层进行load_images处理时,维护了两个load的加载表,一个是本类的表,另一个是分类的表,所以说有先对本类的load发起调用
  • 在对类 load方法进行处理时,进行递归处理,以确保父类优先被处理
  • 在load方法的调用顺序是父类、子类、分类
  • 在分类中load调用顺序,是根据编译的顺序为准

c++构造函数

  • 在分析dyld后,可以确定这样个调用流程load->c++->main
  • 但是如果c++写在objc工程中,在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_register向dyld注册load_images之后再调用
  • 同时如果objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况

疑问?如果有LGA LGB LGC三个分类,哪个分类先加载呢?
这个主要看编译顺序,如果同名方法是load方法 -- 先主类load,后分类load(分类之间,看编译顺序

面试题二:Runtime是什么?
  • runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象、以及运行时的功能
  • 运行时是指将数据类型的确定由编译时推迟到了运行时
    举例:extension 和 category 的区别
  • 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtime的C语言代码, runtime是OC的幕后工作者
面试题三:方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?

方法的本质:发送消息,消息会有以下几个流程

  • 快速查找(objc_msgSend) - cache_t缓存消息中查找
  • 慢速查找 - 递归自己|父类 - lookUpImpOrForward
  • 查找不到消息:动态方法解析 - resolveInstanceMethod
  • 消息快速转发 - forwardingTargetForSelector
  • 消息慢速转发 - methodSignatureForSelector & forwardInvocation

sel是方法编号 - 在read_images期间就编译进了内存
imp是函数实现指针 ,找imp就是找函数的过程
sel相当于 一本书的目录title
imp 相当于 书本的页码

查找具体的函数就是想看这本书具体篇章的内容

  • 首先知道想看什么,即目录title - sel
  • 根据目录找到对应的页码 - imp
  • 通过页码去翻到具体的内容
面试题四:能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?

不能向编译后的得到的类中增加实例变量
原因是:编译好的实例变量存储的位置是ro,一旦编译完成,内存结构就完全确定了

可以向运⾏时创建的类中添加实例变量,只要类没有注册到内存,运行时还是可以添加的
可以通过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中的init方法中打印这两种class调用

// LGTeacher继承自LGPerson
#import "LGTeacher.h"

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


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGTeacher *teacher = [[LGTeacher alloc] init];
        NSLog(@"%@",teacher);
    }
    return 0;
}

// 运行工程打印如下
2021-08-30 17:41:17.662930+0800 KCObjc[79229:10118616] -LGTeacher - LGTeacher

首先来分析[self class]打印的为什么是LGTeacher

  • 首先这两个类中都没有实现class方法,那么根据继承关系,他们最终会调用到NSObject中的class方法
- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
  • 由打印可知这两个方法返回的都是self对应的类。关于这个self是谁,这里涉及到消息发送objc_msgSend,有两个隐形参数分别是id selfSEL sel,这里主要来说下id self
  • [self class]输出LGTeacher,这里消息的发送者是LGTeacher对象,通过调用NSObject的class,但是消息的接受者没有发生变化,所以是LGTeacher

接下来分析[super class]打印的为什么是LGTeacher?

  • [super class]super 是语法关键字,通过命令clang -rewrite-objc LGTeacher.m -o LGTeacher.cpp查看super的本质。下面是编译时的底层源码,其中第一个参数是消息接收者,是__rw_objc_super结构
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_d7_5qn4fnqn0p197t4lkw1bw1p40000gn_T_LGTeacher_c79449_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),((Class (*)(__rw_objc_super *, SEL))
// objc_msgSendSuper
(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")));
    }
    return self;
}
  • 由上面我们看到[super class]的低层实现是objc_msgSendSuper方法,同时存在id selfSEL sel两个隐形参数

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);


/// 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 */
};
  • 由上面可知id receiverClass super_class两个参数,其中super_class表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiver是LGTeacher对象super_class是LGTeacher的父类,也就是要第一个查找的类。
  • 我们再来看[super class]在运行时是否如上一步的底层编码所示,是objc_msgSendSuper,打开汇编调试发现会调用objc_msgSendSuper2,查看objc_msgSendSuper2汇编源码发现是从superclass中的cache中查找方法

OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);


ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame

ldp p0, p16, [x0]       // p0 = real receiver, p16 = class 取出receiver 和 class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2//cache中查找--快速查找

END_ENTRY _objc_msgSendSuper2

最终回答如下

  • [self class]方法调用的本质是发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_imagesreadClass时已经加入表中,所以打印为LGTeacher
  • [super class]打印的是LGTeacher,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2,其实他的消息接收者[self class]是一模一样的,所以返回的是LGTeacher
面试题六:内存平移问题

调试一
创建一个LGPerson类,并实现实例方法saySomething,下面代码能否正常调用?


#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{ 
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *person = [LGPerson alloc];
        [person saySomething];
        
        Class cls = [LGPerson class];
        void *kc = &cls;
        [(__bridge id)kc saySomething];
    }
    return 0;
}

// 控制台打印信息
2021-08-30 22:26:50.334631+0800 004-内存平移问题[90979:11344738] -[LGPerson saySomething]
2021-08-30 22:26:50.335322+0800 004-内存平移问题[90979:11344738] -[LGPerson saySomething]

分析:

  • person的 isa指向类LGPerson,即person的首地址指向 LGPerson的首地址,我们可以通过LGPerson的内存平移找到cache,在cache中查找方法
  • [(__bridge id)kc saySomething]中的kc是来自于LGPerson这个类,然后有一个指针kc将其指向LGPerson的首地址
  • 所以person是指向LGPerson类的结构,kc也是指向LGPerson类的结构,然后都是在LGPerson中的methodList中查找方法
image.png

调试二
接着上面的案例,我们新增一个属性kc_name进行打印,在return 0;前面添加断点查看控制台打印以及lldb调试打印


@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
- (void)saySomething;
@end


#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{ 
    NSLog(@"%s - %@",__func__,self.kc_name);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *person = [LGPerson alloc];
        [person saySomething];
        
        Class cls = [LGPerson class];
        void *kc = &cls;
        [(__bridge id)kc saySomething];
    }
    return 0;
}

// 控制台打印信息
2021-08-30 23:09:55.448534+0800 004-内存平移问题[91230:11372868] -[LGPerson saySomething] - (null)
2021-08-30 23:09:55.449692+0800 004-内存平移问题[91230:11372868] -[LGPerson saySomething] - 

// lldb调试发现[(__bridge id)kc saySomething]; 打印的self.kc_name的LGPerson内存地址与下面person内存地址相同
(lldb) p person
(LGPerson *) $0 = 0x0000600002cef160
  • 首先我们了解到取出属性的值其实是要先计算出偏移大小,再通过内存平移获取值。其实是Person类内部存储着成员变量,每次偏移8字节进行存取。
  • 至于kc打印的self. kc_name的值,是因为cls只有Person类的内存首地址,但是没有person对象的内存结构,所以kc只能在栈里面进行内存平移

调试三
修改属性kc_name关键字为retain,代码如下


@interface LGPerson : NSObject
@property (nonatomic, retain) NSString *kc_name;
- (void)saySomething;
@end


#import "LGPerson.h"
@implementation LGPerson
- (void)saySomething{ 
    NSLog(@"%s - %@",__func__,self.kc_name);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    LGPerson *person = [LGPerson alloc];
    person.kc_name = @"cooci";
    [person saySomething];
    
    Class cls = [LGPerson class];
    void *kc = &cls;
    [(__bridge id)kc saySomething];
}

kc表示8字节指针,self.kc_name的获取相当于 kc首地址的指针平移8字节找kc_name,那么此时的kc的指针地址是多少?平移8字节获取的是什么?

  • kc是一个指针,是存在栈中的,栈是一个先进后出的结构,参数传入就是一个不断压栈的过程。其中隐藏参数也会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),可以通过clang -rewrite-objc ViewController.m -o ViewController.cpp查看底层编译
  • 隐藏参数压栈其地址是递减的,而栈是从高地址->低地址分配的,即在栈中参数会从前往后一直压

super通过clang查看底层的编译是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass),那么结构体中的属性是如何压栈的?

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

- (void)viewDidLoad {
    [super viewDidLoad];
    struct kc_struct kcs = {@(10), @(20)};
    LGPerson *person = [LGPerson alloc];
}

// LGPerson *person = [LGPerson alloc]; 下一行添加断点,lldb调试
(lldb) p &person 
(LGPerson **) $0 = 0x00007ffee303b008
(lldb) p *(NSNumber **)0x00007ffee303b010
(__NSCFNumber *) $1 = 0x9f631adbc08bee8a (int)10
(lldb) p *(NSNumber **)0x00007ffee303b018
(__NSCFNumber *) $2 = 0x9f631adbc08bef6a (int)20

lldb调试得出20先加入,10后加入,因此结构体内部的压栈情况是 低地址->高地址递增的,即栈中结构体内部的成员是反向压入栈,而对象属性参数压栈是高地址->低地址正向压入栈

调试三
通过下面这段代码打印栈的存储

- (void)viewDidLoad {
    [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;
    
    for (long i = 0; i
2021-08-31 22:25:31.815326+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e030 : viewDidLoad
// viewDidLoad方法里面 [super viewDidLoad]; 压栈的为什么不是UIViewController
2021-08-31 22:25:31.815461+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e028 : ViewController
2021-08-31 22:25:31.815608+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e020 : 
2021-08-31 22:25:31.815744+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e018 : LGPerson
2021-08-31 22:25:31.815897+0800 004-内存平移问题[94665:11885963] 0x7ffee1e4e010 : 
image.png

由此可知栈中从高地址到低地址的顺序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - personself和_cmdviewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈的。class_getSuperClass 和 selfobjc_msgSendSuper2中的结构体成员,是从最后一个成员变量,即低地址->高地址反向压栈的

注意

  • 函数隐藏参数会从前往后一直压,即从高地址->低地址开始入栈
  • 结构体内部的成员是从低地址->高地址

通过上面调试,这里有几个疑问?结构体压栈的原理?什么东西才会压栈?上面打印栈的存储viewDidLoad方法里面[super viewDidLoad];压栈的为什么不是UIViewController?

  • 临时变量(传入函数栈帧里面的参数)才会压栈,其中viewDidLoad方法里面的隐藏参数self_cmd压入LGPerson的class的函数栈帧中,跟viewDidLoad的函数栈帧没有任何关系
  • viewDidLoad方法的结构体{objc class}为什么会压栈进来呢?[super viewDidLoad];潜在含义是objc_msgSend发送消息,等同与objc_msgSendSuper(&kc_objc_super, @selector(viewDidLoad))

压栈的为什么不是UIViewController? 真机运行工程,通过汇编代码查看[super viewDidLoad];的底层含义

image.png
  • 结构体第一个参数receiverself,第二个参数是superclass还是currentclass? 如果当前传入的是currentclass意味着压栈的是ViewController,反之压栈的是UIViewController
  • 现在查看objc_msgSendSuper底层源码实现
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
  • 进行lldb调试
image.png
image.png

当前的结构体里面super_class传入的是ViewController,即当前类类名

你可能感兴趣的:(OC底层面试)