iOS 底层学习18

前言

iOS 底层第18天的学习。今天主要分享的是对上一次分类关联对象的一些补充以及一些 runtime 比较重要知识点复习从而给这一个篇章画下一个完美的句号。

分类关联对象补充

objc_getAssociatedObject

  • 进入 objc_getAssociatedObject
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}
  • 再次进入 _object_get_associative_reference
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        // 析构函数 创建一个 AssociationsManager 管理类
        AssociationsManager manager;
        // 获取全局唯一静态哈希map
        AssociationsHashMap &associations(manager.get());
        // 根据 object(key) 找到 iterator
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        // 如果 iterator 不是最后一个
        if (i != associations.end()) {
            // 获取 ObjectAssociationMap
            ObjectAssociationMap &refs = i->second;
            // 根据 value_key 再次找到 iterator
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                // 获取一个经过属性修饰符修饰的value
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }
    // 返回 value
    return association.autoreleaseReturnedValue();
}
  • 分析 _object_get_associative_reference 代码,其实是 2层哈希,根据keyvalue 的过程👇

_object_remove_assocations

  • 关联对象后的属性 何时销毁呢?
  • 全局搜索 _object_remove_assocations
 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;
}
  • 发现在 objc_destructInstance 有对其进行调研,全局搜索 objc_destructInstance
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
  • 发现在 object_dispose 有对其进行调研,全局搜索 object_dispose
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;
    object_dispose((id)this);
}
  • 找到了 rootDealloc
// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

  • 小结:管理对象的生成的属性 也会在dealloc 时进行销毁

知识点复习

- load 方法调用流程

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

分析前准备

@interface XKStudent : XKPerson
@end

@implementation XKStudent
- (instancetype)init{
    if (self == [super init]) {
        NSLog(@" \n self class  %@ \n super class %@",[self class],[super class]);
        return self;
    }
    return nil;
}
  • 输出打印结果

为什么 [self class] 会打印 XKStudent

  • 从底层原理开始分析 进入 self class 源码
  • 进入 object_getClass
  • 小结: [self class] 其实在底层调用了 objc_msgSend(id receive,sel _cmd),在调用方法时,系统把参数 self 给隐藏了,这里的self = objcobjc->isa => class。故输入结果为 XKStudent

那为什么 [super class] 也会打印 XKStudent

  • 动态运行程序 当调用 [super class] 会来到 objc_msgSendSuper2👇
  • 全局搜 objc_msgSendSuper2
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

//  objc_super 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;    /* super_class is the first class to search */
};
  • 可知 objc_msgSendSuper2 参数 id receiver 就是 [super class] 的隐藏参数 self ,
    而这里的 self 也是 objcobjc->isa => class。故输入结果为 XKStudent
    • 而这里的 super 是编译器的关键字,不是参数别被误导了。
  • 小结:[self class], [super class] 输出都是,在底层的本质都是一样的
    发送消息self = objc 传进去用来获取 isa

- 内存偏移面试题分析

代码👇

@implementation ViewController

// viewDidLoad
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    XKStudent *student = [XKStudent alloc];
    [student doSomething];

   // 核心代码
    Class cls = [XKStudent class];
    void  *kc = &cls;
    [(__bridge id)kc doSomething];
}
@end

//  XKStudent 
@implementation XKStudent
- (void) doSomething {
    NSLog(@"%s ",__func__);
}

@end
  • 打印输出👇

为什么第二段代码 - [(__bridge id)kc doSomething]也会输出 doSomething

  • 在开始分析前,先要知道为什么实例对象 student 能调用到 类(XKStudent)里面 doSomething这个方法👇
  • 由👆可知,实例对象student 和 指针地址 kc 都指向了同一个class故可输出一样的方法 doSomething

代码改进👇

@interface XKStudent : NSObject
// 添加了一个实例变量 name
@property (nonatomic, copy) NSString *name; 
- (void) doSomething;

@end

@implementation XKStudent

- (void) doSomething {
    NSLog(@"%s name is %@",__func__,self.name);
}
@end

  • 打印输出👇
  • 输出 null 是因为没有对 name 进行赋值。

那为什么 [(__bridge id)kc doSomething] 会输出 SKStudent 对象
要明白为什么你就要先明白 给name 赋值在底层到底做了❓

  • 开始分析先给 name 赋值
    XKStudent *s = [XKStudent alloc];
    s.name = @"小看";
  • 打印输出👇
  • 进入 lldb 调试
  • 我们发现对象首地址内存平移0x8再取地址 0x0000000131b207f0s.name 取地址0x0000000131b207f0 是相同的,由此得知对象的赋值其实在底层就是一个内存平移的过程。
  • 继续调式 &cls
  • 👆得知cls 取地址后平移0x8得到的地址 0x00007ffeebfe20e8 也与 实例对象取地址 0x00007ffeebfe20e8 相同。这下就能解释 [(__bridge id)kc doSomething] 会输出 SKStudent 对象
  • 因为 cls 不是实例对象,当实例对象+0x8后,cls没有分配内存空间,它只能在函数栈里进行0x8,所以就来到了 SKStudent 实例对象

代码再次改进👇

@interface XKStudent : NSObject
// 新增了一个 age
@property (nonatomic, copy) NSString *age; 
@property (nonatomic, copy) NSString *name; 
- (void) doSomething;

@end
  • 打印输出👇

这是我们发现多了一个 age,输出的结果也不一样了,那这又是为何会输出 <ViewController: 0x7fbe4d406bd0>

  • 其实这里在内存平移时要访问name 时,就必须要跨过 age,因此也就是平移了0x10

那为何平移了0x10,就会输出<ViewController: 0x7fbe4d406bd0> ?

  • 要知道为何必须先要知道函数栈到底是如何压栈
结构体入栈分析
struct xk_struct {
    NSNumber *num1;
    NSNumber *num2;
};
- (void)viewDidLoad {    // id self, SEL _cmd
    [super viewDidLoad]; // 底层结构体 id objc,class
    // Do any additional setup after loading the view.
    XKStudent *student1 = [XKStudent alloc];
    struct xk_struct xk_s = {@10,@11};
    XKStudent *student2 = [XKStudent alloc];
}
  • lldb输出 👇

  • 从输出得知 student1 先入栈,地址0x00007ffee6ed00e8,后入栈student2 ,地址0x00007ffee6ed00d0。结论:内存地址 由高到低 ↑

  • lldb 输出结构体内部👇

  • 从输出得知 num2 地址 > num1 地址
  • 小结: 结构体是倒着入栈的,后面的先入栈
参数入栈分析
void xk_Function(id s1, id s2){
    NSLog(@"s1 = %p",&s1);
    NSLog(@"s2 = %p",&s2);
}
@implementation ViewController
- (void)viewDidLoad {    // id self, SEL _cmd
    [super viewDidLoad]; // 底层结构体 id objc,class
    // Do any additional setup after loading the view.
    XKStudent *student1 = [XKStudent alloc];
    XKStudent *student2 = [XKStudent alloc];
    xk_Function(student1, student2);
}
  • 打印输出👇
  • 从输出得知 参数s1 地址 > 参数 s2 地址
  • 小结: 参数传递是顺着入栈的,第一个参数先入栈

内存偏移总结

  • 解释为何平移0x10输出 <ViewController: 0x7fbe4d406bd0>
  • [super viewDidLoad] 在底层就是一个 struct { id objc , Class superclass}
  • superclass = <ViewController:: 0x7fbe4d406bd0> 先入栈,objc = ViewController 后入栈 ,故平移0x10会输出 <ViewController:: 0x7fbe4d406bd0>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容