iOS - 20.类的加载(3)+ OC底层面试解析

1,009 阅读16分钟

ios底层文章汇总

1. class_ro_t的llvm层分析

  • 下载llvm源码

  • 在llvm源码中搜索class_ro_t,定位class_ro_t结构体

image.png

  • 找到Read函数的实现

image.png image.png image.png

  • 什么时候调用Read函数 ClassDescriptorV2::Describe --> Read_class_row --> class_ro->Read

image.png

image.png

image.png

2. 类别(category)

类别(category)

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员属性,但可以通过runtime用关联对象的方式添加属性
  • 分类中用@property定义变量,只会生成变量的getter,setter方法的声明,不能生成方法实现和带下划线的成员变量

给类别添加属性,需要重写getter,setter方法


@interface LGPerson (LG)
@property (nonatomic, copy) NSString *cate_name;
@end

@implementation LGPerson (LG)
- (void)setCate_name:(NSString *)cate_name{
    /**
     1: 对象
     2: 标识符
     3: value
     4: 策略
     */
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_name{
    return  objc_getAssociatedObject(self, "cate_name");
}

@end

关联对象

AssociationsManager 不是唯一的, AssociationsHashMap 才是唯一的 image.png

关联对象:设值流程

  • objc_setAssociatedObject --> SetAssocHook --> _base_objc_setAssociatedObject --> _object_set_associative_reference
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

{
    SetAssocHook.get()(object, key, value, policy);
}

//SetAssocHook
static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};


//_base_objc_setAssociatedObject
static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

//_object_set_associative_reference
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    // 包装了一下 对象
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 包装一下 policy - value
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    {
        AssociationsManager manager;
    
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second; // 
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

//try_emplace
 std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))  // 找桶子
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{})中refs_result的结构: image.png

auto &refs = refs_result.first->second中refs的结构 image.png

AssociationsManager 不是单例,可以创建多个,公用AssociationsHashMap所以必须加锁使用保证安全性 image.png AssociationsManager::init不是初始化是类的静态函数,在load_image中调用

请问关联对象需要移除么? 不需要。在对象销毁的时候统一移除

image.png

关联对象流程

  • 1:创建一个 AssociationsManager 管理类

  • 2:获取唯一的全局静态哈希Map:AssociationsHashMap

  • 3:判断是否插入的关联值value是否存在

    • 3.1:存在走第4步
    • 3.2:不存在就走 : 关联对象-插入空流程
  • 4:通过try_emplace方法,并创建一个空的 ObjectAssociationMap 去取查询的键值对:

  • 5:如果发现没有这个 key 就插入一个 空的 BucketT进去并返回true

  • 6:通过setHasAssociatedObjects方法标记对象存在关联对象即置isa指针has_assoc属性为true

  • 7:用当前 policy 和 value 组成了一个 ObjcAssociation 替换原来 BucketT 中的空

  • 8:标记一下 ObjectAssociationMap 的第一次为 false

关联对象插入空流程

  • 根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器

  • 清理迭代器

  • 如果插入空值,相当于清处

关联对象:取值流程

  • 创建一个AssociationsManager管理类

  • 获取唯一的全局静态哈希Map

  • 根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器

  • 如果这个迭代查询器不是最后一个 获取:ObjectAssociationMap(这里有策略和Value)

  • 找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value

  • 返回value

image.png

3. 类扩展(extension)

类扩展(extension)

  • 可以说是特殊的分类,也称作匿名分类
  • 类扩展(extension)的位置:在类的声明之后,实现之前,习惯放置在类实现.m文件的顶部
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

main.mm

#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Teacher : NSObject
@end

@interface Teacher()
@property(nonatomic,copy) NSString *ext_name;

- (void)ext_instanceMethod;
- (void)ext_classMethod;

@end
@implementation Teacher

- (void)ext_instanceMethod{
  
}
- (void)ext_classMethod{
  
}


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      NSLog(@"Hello world")
    }
    return 0;
}

重新预编译为cpp文件

clang -rewrite-objc main.mm -o main2.cpp

属性直接写入类,并有setter 和 getter方法:

extern "C" unsigned long OBJC_IVAR_$_Teacher$_ext_name;
struct Teacher_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_ext_name;
};

static NSString * _I_Teacher_ext_name(Teacher * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Teacher$_ext_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Teacher_setExt_name_(Teacher * self, SEL _cmd, NSString *ext_name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Teacher, _ext_name), (id)ext_name, 0, 1); }

扩展的方法查看,在编译时期就直接添加到类的方法列表中,作为了类的一部分

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[6];
} _OBJC_$_INSTANCE_METHODS_Teacher __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	6,
	{{(struct objc_selector *)"ext_instanceMethod", "v16@0:8", (void *)_I_Teacher_ext_instanceMethod},
	{(struct objc_selector *)"ext_classMethod", "v16@0:8", (void *)_I_Teacher_ext_classMethod},
	{(struct objc_selector *)"ext_name", "@16@0:8", (void *)_I_Teacher_ext_name},
	{(struct objc_selector *)"setExt_name:", "v24@0:8@16", (void *)_I_Teacher_setExt_name_},
	{(struct objc_selector *)"ext_name", "@16@0:8", (void *)_I_Teacher_ext_name},
	{(struct objc_selector *)"setExt_name:", "v24@0:8@16", (void *)_I_Teacher_setExt_name_}}
};

总结: 类的扩展在编译器就已经作为类的一部分编译进来,和分类不同,不会动态加入

4. 相关面试题

Q:如何移除关联对象?

  • 移除一个object的某个key的关联对象:调用objc_setAssociatedObject设置关联对象valuenil

    objc_setAssociatedObject函数会调用_object_set_associative_reference函数,并在该函数中判断传进来的value是否为nil,是的话会调用erase(j)擦除函数,将j变量擦除。j即为ObjectAssociationMap对象里的一对【key: key value: ObjcAssociation(_policy、_value)】。

  • 移除一个object的所有关联对象:调用函数objc_removeAssociatedObjects

    objc_removeAssociatedObjects函数会调用_object_remove_assocations函数,并在该函数中调用对象的erase(i)擦除函数,将i变量擦除。i即为AssociationsHashMap对象中的一对【key: object value: ObjectAssociationMap】。

Q:如果 object 被销毁,那它所对应的 ObjectAssociationMap 是否也会自动销毁?

答案是肯定的。 当我们对象释放时,会调用dealloc

  • C++函数释放 :objc_cxxDestruct
  • 移除关联属性:_object_remove_assocations
  • 将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this);
  • 引用计数处理:table.refcnts.erase(this)
  • 销毁对象:free(obj)

所以,关联对象不需要我们手动移除,会在对象析构即dealloc时释放

dealloc的源码查找路径为:dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose(释放对象)-> objc_destructInstance -> _object_remove_assocations

image.png

Q:如果没有关联对象,怎么实现 Category 有成员变量的效果?

使用字典。创建一个全局的字典,将self对象在内存中的地址作为key

缺点: 内存泄漏问题:全局变量会一直存储在内存中;

线程安全问题:可能会有多个对象同时访问字典,加锁可以解决;

每添加一个成员变量就要创建一个字典,很麻烦。

#import "Person.h"
@interface Person (Test)
@property (nonatomic, assign) int height;
@end

#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)
NSMutableDictionary *heights_;
+ (void)load {
    heights_ = [NSMutableDictionary dictionary];
}
- (void)setHeight:(int)height {
    NSString *key = [NSString stringWithFormat:@"%@",self];
    heights_[key] = @(height);
}
- (int)height {
    NSString *key = [NSString stringWithFormat:@"%@",self];
    return [heights_[key] intValue];
}

Q: 类的方法 和 分类方法 重名,如果调用,是什么情

  • 如果同名方法是普通方法,包括initialize -- 先调用分类方法

    • 因为分类的方法是在类realize之后 attach进去的,插在类的方法的前面,所以优先调用分类的方法(注意:不是分类覆盖主类!!)
    • initialize方法什么时候调用? initialize方法也是主动调用,即第一次消息时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize
  • 如果同名方法是load方法 -- 先 主类load,后分类load(分类之间,看编译的顺序)

Q: Runtime是什么?

  • runtime是由C和C++汇编实现的一套API,为OC语言加入了 面向对象、以及运行时的功能

  • 运行时是指将数据类型的确定由编译时 推迟到了 运行时

    • 举例:extension 和 category 的区别
  • 平时编写的OC代码,在程序运行的过程中,其实最终会转换成runtime的C语言代码, runtime是OC的幕后工作者

Q:分类和扩展的对比

  • category 类别、分类

    • 专门用来给类添加新的方法
    • 不能给类添加成员属性,添加了成员属性,也无法取到
    • 注意:其实可以通过runtime 给分类添加属性,即属性关联,重写setter、getter方法
    • 分类中用@property 定义变量,只会生成变量的setter、getter方法的声明不能生成方法实现 和 带下划线的成员变量
  • extension 类扩展

    • 可以说成是特殊的分类 ,也可称作 匿名分类
    • 可以给类添加成员属性,但是是私有变量
    • 可以给类添加方法,也是私有方法

Q:方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?

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

    • 快速查找(objc_msgSend) - cache_t缓存消息中查找
    • 慢速查找 - 递归自己|父类 - lookUpImpOrForward
    • 查找不到消息:动态方法解析 - resolveInstanceMethod
    • 消息快速转发 - forwardingTargetForSelector
    • 消息慢速转发 - methodSignatureForSelector & forwardInvocation
  • sel方法编号 - 在read_images期间就编译进了内存

  • imp函数实现指针 ,找imp就是找函数的过程

  • sel 相当于 一本书的目录title

  • imp 相当于 书本的页码

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

    • 1、首先知道想看什么,即目录 title - sel
    • 2、根据目录找到对应的页码 - imp
    • 3、通过页码去翻到具体的内容

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

  • 1、不能向编译后的得到的类中增加实例变量
  • 2、只要类没有注册到内存还是可以添加的,原因是:编译好的实例变量存储的位置是ro,一旦编译完成,内存的结构完全确定了就无法修改
  • 3、可以添加属性+方法

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

  • [self class]就是发送消息 objc_msgSend,消息接收者是self,方法编号 class
  • [super class] 本质就是objc_msgSendSuper,消息的接收者还是 self,方法编号 class,在运行时,底层调用的是_objc_msgSendSuper2【重点!!!】
  • 只是 objc_msgSendSuper2 会更快,直接跳过self的查找

image.png

  • 进入[self class]中的class源码

**

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


Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

class方法查找顺序是:LGTeacher->LGPerson->NSObject,找到NSObject中的class方法,返回object_getClass(self) ,其底层是获取对象的isa,当前的self是LGTeacher对象,其isa是同名的LGTeacher,所以[self class]打印的是LGTeacher

  • [super class]中,其中super 是语法的 关键字,可以通过clang 看super的本质objc_msgSendSuper(__rw_objc_super,sel)其中第一个参数是消息接收者,是一个__rw_objc_super结构; [super class]就是拿到__rw_objc_super这个消息接收者的isa
clang -rewrite-objc LGTeacher.m -o LGTeacher.cpp
  • 查看init方法

image.png

  • 底层源码中搜索__rw_objc_super,是一个中间结构体

image.png

  • objc中搜索objc_msgSendSuper,查看其隐藏参数

image.png

  • 找不到定义,但是找到了声明,通过注释了解到是struct objc_super类型,那么我们更换搜索目标,搜索struct objc_super的结构 image.png

一共有两个参数:分别是id类型的receiver(消息接收者)变量和Class类型的父类变量,此时receiver的值是self(LGTeacher的对象),self是init后的实例对象,实例对象的isa指向的是本类,即消息接收者是LGTeacher本类

  • objc_msgSendSuper,打开汇编调试,objc_msgSendSuper转为了objc_msgSendSuper2

image.png

  • 查看 objc_msgSendSuper2的函数原型,从类开始查找,而不是父类

image.png

  • 查看objc_msgSendSuper2的汇编源码,是从superclass中的cache中查找方法-class,底层是在函数内部调用的class->superclass获取父类,并不是我们上面分析的直接传入的就是父类对象。查找顺序是:LGPerson->NSObject 找到NSObject中的-class方法返回object_getClass(self);,此时self仍是LGTeacher

image.png 其实_objc_msgSendSuper2内传入的结构体为objc_super2

struct objc_super2 {
    id receiver;
    Class current_class;
};

我们可以发现objc_super2中除了消息接受者receiver,另一个成员变量current_class也就是当前类对象。

与我们上面分析的不同_objc_msgSendSuper2函数内其实传入的是当前类对象,然后在函数内部获取当前类对象的父类,并且从父类开始查找方法。

总结:

  • [self class]方法调用的本质是 发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_imagesreadClass时已经加入表中,所以打印为LGTeacher

  • [super class]打印的是LGTeacher,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2,其实他的消息接收者和[self class]是一模一样的,所以返回的是LGTeacher

Q: Runtime是如何实现weak的,为什么可以自动置nil

  • 通过SideTable 找到我们的 weak_table
  • weak_table 根据 referent找到或者创建 weak_entry_t
  • 然后append_referrer(entry,referrer)将我的新弱引用的对象加进去entry
  • 最后 weak_entry_insert,把entry加入到我们的weak_table

image.png

Q:内存平移问题

LGPerson类定义 LGPerson.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
@property (nonatomic, assign) int kc_name;
@property (nonatomic, copy) NSString *kc_hobby;  // 12
- (void)saySomething;
@end

NS_ASSUME_NONNULL_END

LGPerson.m

#import "LGPerson.h"

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

调用

Class cls = [LGPerson class];
void  *kc = &cls; 
[( __bridge id)kc saySomething];


LGPerson *person = [LGPerson alloc];
[person saySomething];

通过运行发现,是可以执行的

image.png

[person saySomething]的本质是对象发送消息,关键是找到类结构中存储的methods

  • person的 isa指向类LGPerson 即通过person的首地址 可找到 LGPerson的首地址,我们可以通过LGPerson的内存平移找到cache,在cache中查找方法

image.png

  • [(__bridge id)kc saySomething]中的kc是来自于LGPerson 这个类,然后有一个指针kc kc中存储的是isa,而isa将其指向LGPerson的首地址

截屏2021-09-23 下午8.03.32.png

saySomething里面有属性 self.kc_name 的打印


- (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

其中person方式的kc_name 是由于 self指向person的内存结构,然后通过内存平移8字节,取出去kc_name,即self指针首地址平移8字节获得

image.png

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

  • kc是一个指针,是存在中的,栈是一个先进后出的结构,参数传入就是一个不断压栈的过程,

  • 其中隐藏参数会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),可以通过clang查看底层编译

  • 隐藏参数压栈的过程,其地址是递减的,即在栈中,参数会从前往后一直压入栈

  • super通过clang查看底层的编译,是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass)

  • 结构体内部的压栈情况是 低地址->高地址递增的,栈中结构体内部的成员是从`最后一个成员变量开始入栈``,

  • 栈中从高地址到低地址的顺序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person

  • self_cmdviewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈

  • class_getSuperClass 和 selfobjc_msgSendSuper2中的结构体成员,是从最后一个成员变量,开始入栈,即反向压栈

image.png

image.png kc是指向LGPerson的关系,kc中存储的是person<LGPerson: 0x7ffee808c1e8>, 编译器会认为 kc也是LGPerson的一个实例化对象,即kc中存储的是isa,isa指向LGPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有kc_hobby。由于person查找kc_name是通过内存平移8字节,即找到0x7ffee808c1e8+0x8 = 0x7ffee808c1f0,所以kc也是通过内存平移8字节去查找kc_hobby 就找到了ViewController

修改类结构和代码

@interface LGPerson : NSObject
@property (nonatomic, assign) int kc_name;
@property (nonatomic, copy) NSString *kc_hobby;  // 12
- (void)saySomething;
@end

NS_ASSUME_NONNULL_END



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


- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ViewController 当前的类
    // self cmd (id)class_getSuperclass(objc_getClass("LGTeacher")) self cls kc person
    
    Class cls = [LGPerson class];
    void  *kc = &cls;  // 
    LGPerson *person = [LGPerson alloc];
    // LGPerson  - 0x7ffeea0c50f8 --->  0x7ffeea0c50f8平移12个字节取kc_name会崩溃
    [(__bridge id)kc saySomething]; //崩溃
    
    [person saySomething]; // self.kc_name = nil - (null)
    
}

[(__bridge id)kc saySomething]; //崩溃

kc中存储的是LGPerson - 0x7ffeea0c50f8 ,进入-saySomething方法中,读取self.kc_name,即是0x7ffeea0c50f8地址往后平移12个字节读取,不能读取到,所以崩溃

修改代码saySomething

- (void)saySomething{
    NSLog(@"%s - %x",__func__,self.kc_name);
}

image.png

哪些东西在栈里 哪些在堆里

  • alloc的对象 都在
  • 指针、对象 在中,例如person指向的空间中,person所在的空间在栈中
  • 临时变量
  • 属性值 在,属性随对象是在
  • 是从小到大,即低地址->高地址

  • 栈是从大到小,即从高地址->低地址分配

    • 函数隐藏参数会从前往后开始入栈`,
    • 结构体内部的成员是最后一个成员开始入栈
  • 一般情况下,内存地址有如下规则

    • 0x60 开头表示在 
    • 0x70 开头的地址表示在 
    • 0x10 开头的地址表示在全局区域