Objc 中类和对象的本质

Objc 中任何对象都可以称之为 id 类型,那么看下在 objc.hid 类型的定义:

1
2
/// A pointer to an instance of a class.
typedef struct objc_object *id;

注释中的描述是 一个指向类的实例的指针,那么是不是意味一个类的实例即对象就是一个 objc_object 结构体呢?再看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_object {
private:
isa_t isa; // 对象的类信息
}

struct objc_class : objc_object {
// Class ISA;
Class superclass; // 父类
cache_t cache; // formerly cache pointer and vtable;方法缓存列表
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags;类信息存储的地方
}

union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}

通过阅读 runtime 的源码可以得出以下结构:

obj-class-isa

注:class_ro_t 中存放的是编译时可以确定的属性、方法和协议等

  1. Objc 中的对象是一个 objc_object 结构体,结构体中第一个变量是 isa_t ,存放着该对象所属类的信息;
  2. 类是一个objc_class 结构体,继承自 objc_object ,所以类也是一个对象,另外还有两个变量进行方法缓存和数据存放,比如变量、方法(实例方法,以 - 开头的方法)和所遵守的协议。
  3. 类的 isa 中存放的类是元类(meta-class),类是一个对象,对象的类型就是元类,元类存放着类的方法(类方法,以 + 开头的方法)。
  4. 元类也是类,所以元类也是对象。元类 isa 变量中存放的类是根元类(一般是 NSObject )。
  5. 根元类 isa 的类信息指向其自身
  6. 类和元类都是单例

那么 isa 到底存放了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
union isa_t {
Class cls;
uintptr_t bits;
uintptr_t nonpointer : 1; // 是否开启指针优化
uintptr_t has_assoc : 1; // 是否关联对象
uintptr_t has_cxx_dtor : 1; // 是否有 C++ 自定义析构函数
uintptr_t shiftcls : 33; // 存放了类的信息
uintptr_t magic : 6; // 验证对象是否初始化完整
uintptr_t weakly_referenced : 1; // 是否被弱引用
uintptr_t deallocating : 1; // 是否正在析构
uintptr_t has_sidetable_rc : 1; // 引用计数是否在 sidetable 中存储,当对象的 extra_rc 字段存储不下时,会把引用计数存放在 sidetable 中
uintptr_t extra_rc : 19 // 引用计数
};

objc_object 中的 isa 是通过位域+掩码来优化存储。isa 共8个字节64位,其中存放类信息的 shiftcls 占去了 33 位,存放引用计数的 extra_rc 占用了 19 位,验证对象是否初始化完整的 magic 占用 6 位,其他都是只占用了 1 位。

再看下 class_data_bits_tclass_rw_t 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro; // 编译期决定的类的信息

method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;
uint32_t index;
};

const class_ro_t *ro 中存放的是类在编译时期就确定的方法、协议等,分类及运行时添加的属性、方法都在 class_rw_t 存储。但是 class_data_bits_t 中的 data(class_rw_t) 一开始是 const class_ro_t *ro;,在初始运行时环境中的 realizeClass 函数中,重新生成 class_rw_t data。并且通过 attachCategories 函数,将分类的方法、协议等添加到 class_rw_t 中。至此,class_rw_t 存放了一个类所需要的完整信息。

objc_msgSend

调用方法(函数)是语言经常使用的功能,在 Objective-C 中专业一点的叫法是 传递消息(pass a message)。Objective-C 的方法调用都是 动态绑定 ,而C语言中函数调用方式是 静态绑定 ( static binding ),也就是说,在编译时期就能决定和知道在运行时所调用的函数。

以下面代码为例:

1
2
3
4
5
6
7
8
9
10
11
void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
if(type == 0){
sayHello();
}else{
sayGoodBye();
}
}

基本上,上面的代码在编译的时候编译器就知道 sayHellosayGoodBye 两个函数的存在,函数地址是硬编码在指令之中的。但是如果换一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
void (*something) ();
if(type == 0){
something = sayHello;
}else{
something = sayGoodBye;
}
something();
}

这就得使用 动态绑定 ,待调用的函数地址需要到运行时才能读取出来。
在 Objective-C 中,对某一个对象传递消息,会用动态绑定机制来决定到底是调用哪个方法。而Objective-C是 C 的超集,底层是由 C语言实现,但是对象接收消息后会调用哪个方法都是在运行期决定。

1
id object = [list objectAtIndex:1];

在这行代码中, list 称为 接收者objectAtIndex 叫做 选择器, 选择器和参数合起来称为消息。当编译器看到这行代码的时候,会换成标准的C语言函数调用:

1
2
void objc_msgSend(id self, SEL cmd, ...);
id lastObject = objc_msgSend(list, @selector(objectAtIndex:), parameter);

objc_msgSend 这个函数可以接收两个及两个以上的参数,第一个参数是接收者,第二个参数是选择器,后面的参数是保持顺序的原来消息传递的参数,objc_msgSend 会依据接收者和选择器来决定调用哪个方法,首先在接收者的方法列表缓存(objc_class/cache_t cache)中寻找,如果方法缓存中找不到就类信息存储的方法列表(objc_class/class_data_bits_t/class_rw_t/methods)中找,如果再找不到就会沿着继承体系(objc_class/superclass/class_data_bits_t/class_rw_t/methods)去向上一层一层的寻找,如果仍旧找不到就会执行 消息转发(message forwarding)
当消息第一次传递之后,objc_msgSend 会将匹配结果进行缓存(objc_class/cache_t cache),下次会直接调用方法。消息传递除了objc_msgSend之外在特殊情况下还会有其他的方法来处理:

  • objc_msgSend_stret 如果待发送的消息返回一个结构体,就会调用这个函数来处理。
  • objc_msgSend_fpret 如果消息返回的是浮点数,就会调用这个函数进行处理。
  • objc_msgSendSuper 如果要传递消息给父类。

总结:

  • 消息由 接收者、选择器及参数构成,给某对象 发送消息( invoke a message ) 也就相当于在该对象上调用方法。
  • 发送给某对象的全部消息都要有动态消息派发系统( dynamic message dispatch system ) 来处理。

Objective-C 的消息转发机制与动态添加方法

刚才说了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。这里就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )

开发可能经常会遇到这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff9f2d94f2 __exceptionPreprocess + 178
1 libobjc.A.dylib 0x00007fff90db3f7e objc_exception_throw + 48
2 CoreFoundation 0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x00007fff9f249571 ___forwarding___ + 1009
4 CoreFoundation 0x00007fff9f2490f8 _CF_forwarding_prep_0 + 120
5 runtime 0x0000000100001c1c main + 124
6 libdyld.dylib 0x00007fff91df85ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

这个异常信息是由 NSObjectdoesNotRecognizeSelector: 方法抛出来的,本来是给 AutoDictionary 的一个实例对象发送消息,但是该对象并没有 setDate: 方法,所以消息转发给了 NSObject ,最后抛出异常。

先看下消息处理机制流程图:

消息处理机制流程图

消息转发分为两阶段三步,第一阶段先看接受消息的对象能不能自己处理这个无法解读的消息,这一步可以动态的添加方法去解读接受这个消息;第二阶段是先看看对象自己不能处理这个消息,能不能交给其他对象来进行处理,在这一步如果仍然无法解读消息,那么就会走最后一步:把和消息有关的所有细节封装到一个 NSInvocation 中,再询问一次对象是否能解决。
看下三个方法:

1
2
3
4
5
6
7
8
// 询问对象是否自己处理,是返回YES,一般会在这个方法里面动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;

// 这一步询问对象把消息交给哪个对象来进行处理
- (id)forwardingTargetForSelector:(SEL)aSelector;

// 如果走到这一步的话,就把消息的所有信息封装成 NSInvocation 对象进行 "最后通牒"
- (void)forwardInvocation:(NSInvocation *)anInvocation;

来一段代码示例:
新建一个 AutoDictionary 类,添加一个 NSDate 类型的 date 属性,在实现文件里面用 @dynamic date; 禁止自动生成存取方法,这样当代码中给 AutoDictionary 实例对象的 date属性赋值时就会出现消息无法解读的现象。
.h 文件:

1
2
3
4
5
@interface AutoDictionary : NSObject

@property (nonatomic, strong) NSDate *date;

@end

.m 实现文件代码内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@interface AutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;

/**
* 该类仅在实现文件 实现了
* - (NSDate *)date
* - (void)setDate:(NSDate *)date
* 两个方法,用于处理 AutoDictionary 无法解读的消息
*/
@property (nonatomic, strong) MethodCreator *methodCreator;
@end
@implementation AutoDictionary
//
@dynamic date;
//
- (instancetype)init{
if (self = [super init]) {
self.backingStore = [NSMutableDictionary dictionary];
self.methodCreator = [MethodCreator new];
}
return self;
}

#pragma mark - 消息转发机制 :1.动态添加方法 2.后备消息接收者 3.封装NSInvocation,最后通牒
// 3. 封装NSInvocation,最后通牒
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//
}
// 2. 无法接受消息,选择由谁来接受
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.methodCreator;
}
// 1. 动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selString = NSStringFromSelector(sel);
if ([selString hasPrefix:@"set"]) {
class_addMethod(self, sel, (IMP)autoDictSetter, "");
}else{
class_addMethod(self, sel, (IMP)autoDictGetter, "");
}
return YES;
}
id autoDictGetter (id self, SEL _cmd){
AutoDictionary *dict = self;
NSString *key = NSStringFromSelector(_cmd);
return [dict.backingStore objectForKey:key];
}
void autoDictSetter (id self, SEL _cmd, id value){
AutoDictionary *dict = self;
NSString *selString = NSStringFromSelector(_cmd);
NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)];
key = [key lowercaseStringWithLocale:[NSLocale currentLocale]];
if (value) {
[dict.backingStore setObject:value forKey:key];
}else{
[dict.backingStore removeObjectForKey:key];
}
}

@end

测试代码:

1
2
3
AutoDictionary *dict = [AutoDictionary new];
dict.date = [NSDate date];
NSLog(@"dict.date = %@",dict.date);

method swizzling 与AOP编程

什么是 AOP : (site: baike.baidu.com),引用百度百科中的解释就是:

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

主要功能:
日志记录,性能统计,安全控制,事务处理,异常处理等等

主要意图:
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

iOS 开发中的 AOP
Objective-C 中,类的方法列表会把选择器的名称映射到方法的实现上,这样 动态消息转发系统 就可以以此找到需要调用的方法。这些方法是以函数指针的形式来表示,这种指针叫做 IMP
如下:

c1f0660ejw1f51w4zipmhj20c5052glu

1
id (*IMP) (id, SEL, ...)

Objective-Cruntime 机制以此提供了获取和交换映射IMP的的接口:

1
2
3
4
// 获取方法
Method class_getInstanceMethod(Class cls, SEL name);
// 交换两个方法
void method_exchangeImplementations(Method m1, Method m2)

我们可以通过上面两个方法来进行选择器和所映射的IMP进行交换:

c1f0660ejw1f51w5m2wipj20c008874r

来,直接上代码示例,比如我们的要实现功能是在每个控制器的- viewDidLoad方法里面log一下,一般有三种实现方式:

  1. 直接修改每个页面的 view controller 代码,简单粗暴;
  2. 子类化 view controller ,并让我们的 view controller 都继承这些子类;
  3. 使用 Method Swizzling 进行 hook,以达到 AOP 编程的思想

第一种实现的代码是在每个类的里面都这么写:

1
2
3
4
- (void)viewDidLoad {
[super viewDidLoad];
DDLog();
}

第二种是只在基类里面写。然后所有的控制器都继承这个基类。
最后一种是最佳的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@implementation UIViewController (Log)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(log_viewDidLoad);
//
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling

- (void)log_viewDidLoad{
[self log_viewDidLoad];
DDLog(...);
}
@end

注意:

  • 为什么使用 + (void)load ?因为父类、子类和分类的该方法是分别调用,互不影响,而且是在类被加载的时候必定会调用的方法。

那么为什么说“父类、子类和分类的该方法是分别调用”且”类被加载的时候必定会调用的方法“呢?

在 runtime 源码中的 _objc_init() -> load_images() -> call_load_methods() 函数里,可以发现load方法不是走的消息发送,而是直接使用函数地址指针直接调用,且保证了类的load方法在分类之前调用,父类的load方法再子类之前调用。具体逻辑见伪代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void call_load_methods(void)
{
...
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();
...
}

static void call_class_loads(void)
{
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}

}

给对象、分类添加实例变量

在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)

关联对象 可以给某个对象关联其他对象并用key来区分其他对象。需要注意的是,存储对象的时候要指明 存储策略,用来维护对象的内存管理语义。存储策略是 objc_AssociationPolicy 枚举定义,以下是存储策略对应的 @property属性:

存储策略类型 对应的@property属性
OBJC_ASSOCIATION_ASSIGN weak
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong
OBJC_ASSOCIATION_COPY copy

用下面的方法可以管理关联对象:

1
2
3
4
5
6
7
8
// 这个方法可以根据指定策略给对象关联对象值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

// 这个方法可以获取对象关联对象值
id objc_getAssociatedObject(id object, const void *key)

// 这个方法可以删除指定对象的全部关联对象值
void objc_removeAssociatedObjects(id object)

注意:

  • 定义关联对象时需要指定内存管理语义,用来模拟对象对变量的拥有关系
  • 尽量避免使用关联对象,因为如果出现bug不易于问题排查

Associated Object的实现方式

associations.png

通过源码发现,存储使用的数据结构如上图所示。

1
2
3
void objc_setAssociatedObject(id object, const void *key,
id value,
objc_AssociationPolicy policy)
  1. AssociationsManager 类中有个静态变量 AssociationsHashMap *_map
  2. AssociationsHashMap 中以 object 为 key,value 是 ObjectAssociationMap
  3. ObjectAssociationMap 中 key 就是入参 key,value 就是入参 value

后话

做个试验,新建一个 BBObject 类,添加一个属性 bb_name,头文件如下:

1
2
3
4
5
@interface BBObject : NSObject

@property (nonatomic, strong) NSString *bb_name;

@end

那么以下代码会输出什么呢:

1
2
3
4
NSString *name = @"这是 bb_name";
void *cls = (__bridge void *)([BBObject class]);
void *bb_obj = &cls;
NSLog(@"%@",[(__bridge BBObject*)bb_obj bb_name]);

输出的是:2017-12-16 19:55:33.477716+0800 runtime[45876:7116185] 这是 bb_name

因为 Objc 中一个完整的对象就是 首地址指向一个类的连续空间,为什么是连续空间?那是因为对象还有自己属性变量的值要存储,这也是为什么没有给 bb_objbb_name 属性赋值,却打印出 name 值的原因,在 iOS 中,栈的地址是由高到低,堆的地址是由低到高,在这段代码中栈中依次压入了 namebb_obj,而 bb_obj 对象自身的属性是根据自身首地址进行偏移去获取,所以会取到 name 的值。

objc_iva

使用 clang -rewrite-objc BBObject.m 可以把得到重写后的 C++ 文件,在其中也可以看到其中获取属性就是自身地址加偏移量:

1
2
3
static NSString * _I_BBObject_bb_name(BBObject * self, SEL _cmd) { 
return (*(NSString **)((char *)self + OBJC_IVAR_$_BBObject$_bb_name));
}