1. class_ro_t的llvm层分析
-
下载llvm源码
-
在llvm源码中搜索class_ro_t,定位class_ro_t结构体
- 找到Read函数的实现
- 什么时候调用Read函数 ClassDescriptorV2::Describe --> Read_class_row --> class_ro->Read
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 才是唯一的
关联对象:设值流程
- 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的结构:
auto &refs = refs_result.first->second中refs的结构
AssociationsManager 不是单例,可以创建多个,公用AssociationsHashMap所以必须加锁使用保证安全性
AssociationsManager::init不是初始化是类的静态函数,在load_image中调用
请问关联对象需要移除么? 不需要。在对象销毁的时候统一移除
关联对象流程
-
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
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设置关联对象value为nil。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
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(分类之间,看编译的顺序)- 原因:参考iOS-底层原理 18:类的加载(上)中的
load_images解析
- 原因:参考iOS-底层原理 18:类的加载(上)中的
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的查找
- 进入
[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方法
- 底层源码中搜索
__rw_objc_super,是一个中间结构体
- objc中搜索
objc_msgSendSuper,查看其隐藏参数
- 找不到定义,但是找到了声明,通过注释了解到是struct objc_super类型,那么我们更换搜索目标,搜索
struct objc_super的结构
一共有两个参数:分别是id类型的receiver(消息接收者)变量和Class类型的父类变量,此时receiver的值是self(LGTeacher的对象),self是init后的实例对象,实例对象的isa指向的是本类,即消息接收者是LGTeacher本类
objc_msgSendSuper,打开汇编调试,objc_msgSendSuper转为了objc_msgSendSuper2
- 查看 objc_msgSendSuper2的函数原型,
从类开始查找,而不是父类
- 查看
objc_msgSendSuper2的汇编源码,是从superclass中的cache中查找方法-class,底层是在函数内部调用的class->superclass获取父类,并不是我们上面分析的直接传入的就是父类对象。查找顺序是:LGPerson->NSObject找到NSObject中的-class方法返回object_getClass(self);,此时self仍是LGTeacher
其实
_objc_msgSendSuper2内传入的结构体为objc_super2
struct objc_super2 {
id receiver;
Class current_class;
};
我们可以发现objc_super2中除了消息接受者receiver,另一个成员变量current_class也就是当前类对象。
与我们上面分析的不同_objc_msgSendSuper2函数内其实传入的是当前类对象,然后在函数内部获取当前类对象的父类,并且从父类开始查找方法。
总结:
-
[self class]方法调用的本质是发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_images的readClass时已经加入表中,所以打印为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
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];
通过运行发现,是可以执行的
[person saySomething]的本质是对象发送消息,关键是找到类结构中存储的methods
person的isa指向类LGPerson即通过person的首地址 可找到 LGPerson的首地址,我们可以通过LGPerson的内存平移找到cache,在cache中查找方法
[(__bridge id)kc saySomething]中的kc是来自于LGPerson这个类,然后有一个指针kckc中存储的是isa,而isa将其指向LGPerson的首地址
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];
打印结果如下:
其中person方式的kc_name 是由于 self指向person的内存结构,然后通过内存平移8字节,取出去kc_name,即self指针首地址平移8字节获得
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和_cmd是viewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈的 -
class_getSuperClass和self为objc_msgSendSuper2中的结构体成员,是从最后一个成员变量,开始入栈,即反向压栈
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);
}
哪些东西在栈里 哪些在堆里
alloc的对象 都在堆中指针、对象在栈中,例如person指向的空间在堆中,person所在的空间在栈中临时变量在栈中属性值在堆,属性随对象是在栈中
堆是从小到大,即低地址->高地址栈是从大到小,即从高地址->低地址分配
- 函数隐藏参数会
从前往后开始入栈`,- 结构体内部的成员是
最后一个成员开始入栈一般情况下,内存地址有如下规则
0x60开头表示在堆中0x70开头的地址表示在栈中0x10开头的地址表示在全局区域中