小马同学的技术博客

Stay hungry. Stay Foolish. - Steve Jobs. 求知若渴,虚心若愚!


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

Category

发表于 2018-09-18 | 更新于 2018-09-24 | 分类于 iOS底层原理

iOS | Category | Objective-C

Category的实现原理?Category和Class Extension的区别是什么?Category中的load方法时什么时候调用的?如何给分类添加属性?


通过前面章节,我们有学习到OC实例方法存储在class对象里面,类方法存储在meta-class里面,那么分类里面方法呢,难道是一个新的对象来进行存储么?

答案肯定是否,class对象、meta-class对象在内存中只会存在一份,分类里面的方法会合并到class对象、meta-class中。

1.Category的实现原理

Category内方法是什么时候合并到class、meta-class对象中的,是在编译阶段么?

并不是,是在程序运行时通过runtime机制动态将分类的方法合并到class、meta-class对象中。下面来通过探索源码来进行分析验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface Person (Test_A)
+ (void)testA;
- (void)testA;
@end

@implementation Person (Test_A)
+ (void)testA {
NSLog(@"%s", __func__);
}
- (void)testA {
NSLog(@"%s", __func__);
}
@end

通过clang编译器将 Person+Test_A.m 文件转成cpp文件,搜索 struct _category_t {

1
2
3
4
5
6
7
8
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

Person+Test_A这个分类在编译完,转成上面这种结构,Test_A分类里面的方法、协议、属性信息也是存储在这个结构体中。每编写一个分类,就相当于一个 struct _category_t 类型的结构体,在程序运行时,通过runtime机制动态的将这个结构体内部的方法、协议等合并到类对象中。

KVC

发表于 2018-09-11 | 更新于 2018-09-24 | 分类于 iOS底层原理

iOS | KVC | Objective-C

通过KVC修改属性会不会触发KVO,KVC底层实现是什么样子?

1.KVC

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性

常见API:

1
2
3
4
5
// 赋值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

2.通过KVC修改属性会触发KVO么

1
2
3
4
5
@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
@end
1
2
3
4
5
6
7
8
Observer *observer = [[Observer alloc] init];
Person *person = [[Person alloc] init];

[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

[person setValue:@20 forKey:@"age"];

[person removeObserver:observer forKeyPath:@"age"];

通过控制台输出,发现是可以触发KVO的

3.setValue:forKey

setValue:forKey:

1
[person setValue:@20 forKey:@"age"];

上面这行代码相当于下面这几行,在对属性赋值的时候系统内部会自动调用 willChangeValueForKey 和 didChangeValueForKey,所以会触发 KVO,可以重写这两个方法进行验证。

1
2
3
[person willChangeValueForKey:@"age"];
person->_age = 20;
[person didChangeValueForKey:@"key"];

4.valueForKey

valueForKey:

iOS中的线程同步方案(锁)、读写安全方案

发表于 2018-09-06 | 更新于 2018-09-11 | 分类于 iOS底层原理

iOS中的线程同步方案

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

1.OSSpinLock

1> OSSpinLock叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源;High-level lock,高级锁,等不到锁时忙等,不会休眠
2> 目前已经不再安全,可能会出现优先级反转问题
3> 如果等待锁的优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
4> 需要导入头文件 #import <libkern/OSAtomic.h>

自旋锁优先级反转:有两个线程,thread1(高优先级)、thread2(低优先级),thread2先进行加锁操作,CPU切换调度,thread1进入,发现已经被锁,进入自旋状态。由于thread1优先级比thread2高出很多,CPU接下来可能一直调度thread1,处于自旋状态,相当于一直执行不到thread2的解锁操作,造成优先级反转现象

1
2
3
4
5
6
7
// 初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

OSSpinLock是性能最高的锁,因为加锁操作耗时较短的话,忙等一会儿直接继续执行了;反之休眠的话唤醒也是需要耗性能的。但是现在已经不再安全了,所以苹果不建议我们再继续使用OSSpinLock了

验证OSSpinLock自旋锁忙等:
Xcode -> Debug Workflow -> Always show Disassembly

step::代码级别一行一行走
stepi:汇编指令一行一行走,简称 si
nexti:汇编指令一行一行走,但是如果遇到函数调用不会进去,会直接跳过

我们通过 si 指令,一行一行走,进入 OSSpinLockLock 函数,继续,进入 _OSSpinLockLockSlow 函数,这个时候要注意了:会一直在一块儿内存地址代码之间重复执行,这种就是典型的while循环,自旋锁,只有锁被放开之后才会往下继续执行

2.os_unfair_lock_lock

1> os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始支持
2> 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等;Low-level lock,低级锁,等不到锁时休眠
3> 需要导入头文件 #import <os/lock.h>

1
2
3
4
5
6
7
8
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);

验证 os_unfair_lock_lock 非忙等:
os_unfair_lock_lock -> _os_unfair_lock_lock_slow -> __ulock_wait,进入之后,发现断点过着过着、到syscall时直接过去了,直接休眠了,这也说明os_unfair_lock_lock线程等待时并非忙等,而是休眠了

3. pthread_mutex

1> mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
2> 需导入头文件 #import <pthread.h>

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
/*
* Mutex type attributes
*/
// 普通锁
#define PTHREAD_MUTEX_NORMAL 0
// 检查错误的
#define PTHREAD_MUTEX_ERRORCHECK 1
// 递归锁
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL

// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
// 尝试加锁
pthread_mutex_trylock(&mutex);
// 加锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁相关资源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);

将属性attr的type由PTHREAD_MUTEX_NORMAL改为 PTHREAD_MUTEX_RECURSIVE,这个锁就变成了递归锁:允许 同一个线程 对 同一把锁 进行 重复加锁

4.pthread_mutex - 递归锁

1
2
3
4
5
6
7
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)

// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);

5.pthread_mutex - 条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化锁
pthread_mutex_t mutex;
// NULL代表使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&condition, &mutex);
// 激活一个等待该条件的线程
pthread_cond_signal(&condition);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&condition);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);

6.NSLock、NSRecurisiveLock

1> NSLock 是对mutex普通锁的封装
2> NSRecursiveLock 也是对mutex递归锁的封装,API跟NSLock基本一致

1
2
3
4
5
6
7
8
9
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking>
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end

NSLock 是OC对象,再从堆栈一行一行找的话不好看锁的执行流程,因为OC对象通过消息机制执行方法,堆栈里面有很多消息机制流程,对汇编不是很熟悉的话不好断点。

那我们就放弃了么,NO,NO,还是可以通过 GUNStep -> GNUstep Base 来参考下

GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值

源码地址:http://www.gnustep.org/resources/downloads.php

下载之后打开工程,搜索 NSLock.m,找到 initialize 方法:

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
+ (void) initialize
{
static BOOL beenHere = NO;

if (beenHere == NO)
{
beenHere = YES;

/* Initialise attributes for the different types of mutex.
* We do it once, since attributes can be shared between multiple
* mutexes.
* If we had a pthread_mutexattr_t instance for each mutex, we would
* either have to store it as an ivar of our NSLock (or similar), or
* we would potentially leak instances as we couldn't destroy them
* when destroying the NSLock. I don't know if any implementation
* of pthreads actually allocates memory when you call the
* pthread_mutexattr_init function, but they are allowed to do so
* (and deallocate the memory in pthread_mutexattr_destroy).
*/
pthread_mutexattr_init(&attr_normal);
pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
pthread_mutexattr_init(&attr_reporting);
pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);

/* To emulate OSX behavior, we need to be able both to detect deadlocks
* (so we can log them), and also hang the thread when one occurs.
* the simple way to do that is to set up a locked mutex we can
* force a deadlock on.
*/
pthread_mutex_init(&deadlock, &attr_normal);
pthread_mutex_lock(&deadlock);
}
}

我们发现,其实就是 pthread_mutex_init(&deadlock, &attr_normal);,所以 NSLock 其实就是对 pthread_mutex 普通锁的封装

1
2
3
4
5
6
7
8
9
10
11
12
// NSRecursiveLock
- (id) init
{
if (nil != (self = [super init]))
{
if (0 != pthread_mutex_init(&_mutex, &attr_recursive))
{
DESTROY(self);
}
}
return self;
}

7.NSCondition

1> NSCondition是对mutex和cond的封装

1
2
3
4
5
6
@interface NSCondition : NSObject <NSLocking> {
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@end

GNUStep实现如下:

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
- (id) init
{
if (nil != (self = [super init]))
{
if (0 != pthread_cond_init(&_condition, NULL))
{
DESTROY(self);
}
else if (0 != pthread_mutex_init(&_mutex, &attr_reporting))
{
pthread_cond_destroy(&_condition);
DESTROY(self);
}
}
return self;
}

- (void) signal
{
pthread_cond_signal(&_condition);
}

- (void) wait
{
pthread_cond_wait(&_condition, &_mutex);
}

8.NSConditionLock

1> NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

1
2
3
4
5
6
7
8
9
10
@interface NSConditionLock : NSObject <NSLocking> {
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end

GUNStep:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (id) init
{
return [self initWithCondition: 0];
}

- (id) initWithCondition: (NSInteger)value
{
if (nil != (self = [super init]))
{
if (nil == (_condition = [NSCondition new]))
{
DESTROY(self);
}
else
{
_condition_value = value;
}
}
return self;
}

9.dispatch_queue(DISPATCH_QUEUE_SERIAL) GCD串行队列

1
2
3
4
dispatch_queue_t queue = dispatch_queue_create("top.istones.moneyQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.moneyQueue, ^{
// 任务
});

10.dispatch_semaphore

1> semaphore叫做”信号量”
2> 信号量的初始值,可以用来控制线程并发访问的最大数量
3> 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

1
2
3
4
5
6
7
8
9
// 信号量的初始值
int value = 1;
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信号量的值 <= 0,当前线程进入休眠等待(直到信号量的值 > 0)
// 如果信号量的值 > 0,就减1,然后往下执行后面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信号量的值加1
dispatch_semaphore_signal(semaphore);

11.@synchronized

1> @synchronized是对mutex递归锁的封装
2> 源码查看:objc4中的objc-sync.mm文件
3> @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

1
2
3
@synchronized(obj) { // objc_sync_enter
// 任务
} // objc_sync_exit

注意:效率非常低,苹果已经不推荐使用,Xcode也没有提示了

Xcode断点,发现,@synchronized在大括号进入和退出时分别调用的是 objc_sync_enter 和 objc_sync_exit。在 objc-sync.mm 文件中搜索:

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
// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;

if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}


// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;

if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}

return result;
}

性能

性能由高到低:
1> os_unfair_lock
2> OSSpinLock
3> dispatch_semaphore
4> pthread_mutex
5> dispatch_queue(DISPATCH_QUEUE_SERIAL)
6> NSLock
7> NSCondition
8> pthread_mutex(recursive)
9> NSRecursiveLock
10> NSConditionLock
11> @synchronized

推荐使用 dispatch_semaphore、pthread_mutex,os_unfair_lock 性能虽然高,但是从 iOS10 才开始支持

什么情况下使用自旋锁比较划算

1> 预计线程等待锁的时间很短
2> 加锁的代码(临界区)经常被调用,但竞争很少发生
3> CPU 资源不紧张
4> 多核处理器

什么情况下使用互斥锁比较划算

1> 预计线程等待锁的时间较长
2> 单核处理器
3> 临界区有IO操作
4> 临界区代码复杂或者循环量大
5> 临界区竞争非常激烈


atomic

1> atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
2> 可以参考源码 objc4 的 objc-accessors.mm
3> 它并不能保证使用属性的过程是线程安全的(eg.一个属性array,atomic的话只能保证在外面set和get的时候线程安全,但是不能保证array addObject、removeObject线程安全)

提到 atomic,我们想到更多的是 nonatomic,atomic 原子性,在 macOS 中有用,在 iOS 项目中几乎不会用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
nonatomic 和 atomic
atom:原子
atomic:原子性

给属性加上atomic属性修饰,可以保证属性的setter和getter都是原子性操作,也就是保证setter和getter内部都是线程同步的,相当于下面:

- (void)setName:(NSString *)name {
// 加锁
_name = name;
// 加锁
}

- (NSString *)name {
// 加锁
// 取值
// 解锁
// 返回
return _name;
}
*/

打开objc4源码,搜索 objc-accessors.mm 文件,观察 reallySetProperty 和 objc_getProperty 方法:

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
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);

if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}

if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}

objc_release(oldValue);
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}

// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;

// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();

// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}

我们可以发现,如果是atomic,就加了一把自旋锁,如果是nonatomic,直接set或返回值


iOS中的读写安全方案

我们先思考如何实现以下场景:
1> 同一时间,只能有1个线程进行写的操作
2> 同一时间,允许有多个线程进行读的操作
3> 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作

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
- (void)test {
for (NSInteger i = 0; i < 10; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(read) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(write) object:nil] start];
}
}

#pragma mark - private methods
/**
如果读也加上 semaphore 的话,确实可以保证读写都安全,同时只能读或者写,但是没必要。iOS中读写方案一般设计多读单写,读不涉及资源抢占,可以同时进行。

但是,这样依然存在问题,虽然可以多读单写。但是写入加锁了,读却没有加锁,读的同时依然可以写,这个依然不可控,依然不安全,不能保证读的同时没有线程在进行写的操作。
*/
- (void)read {
// dispatch_semaphore_wait(self.semphore, DISPATCH_TIME_FOREVER);

NSLog(@"%s", __func__);

// dispatch_semaphore_signal(self.semphore);
}

- (void)write {
dispatch_semaphore_wait(self.semphore, DISPATCH_TIME_FOREVER);

NSLog(@"%s", __func__);

dispatch_semaphore_signal(self.semphore);
}

好像简单这样用semphore的话并不能实现安全多读单写,那么怎么实现呢?

iOS中的实现方案有:
1> pthread_rwlock:读写锁
2> dispatch_barrier_async:异步栅栏调用

pthread_rwlock

等待锁的线程会进入休眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);

dispatch_barrier_async

1> 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
2> 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_queue_create("top.istones.rwQueue", DISPATCH_QUEUE_CONCURRENT);

// 读
dispatch_async(queue, ^{

});

// 写
dispatch_barrier_async(queue, ^{

});

注意:读和写一定要放到一个队列里面!

本文demo
objc4
GNUstep Base

深入理解KVO

发表于 2018-09-04 | 更新于 2018-09-24 | 分类于 iOS底层原理

iOS | KVO | Objective-C

KVO的本质是什么,如何手动触发KVO?


1.什么是KVO

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变

添加监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef enum NSKeyValueObservingOptions : NSUInteger {
// 新值(包含于回调方法change字典中)
NSKeyValueObservingOptionNew = 0x01,
// 旧值(包含于回调方法change字典中)
NSKeyValueObservingOptionOld = 0x02,
// 观察最初的值(在注册观察服务时会调用一次触发方法)
NSKeyValueObservingOptionInitial = 0x04,
// 分别在值修改前后触发方法(即一次修改有两次触发)
NSKeyValueObservingOptionPrior = 0x08
} NSKeyValueObservingOptions;

/**
监听属性方法,方法调用者为被观察对象

@param observer 观察者/订阅者
@param keyPath 要观察的属性
@param options 监听变化条件
@param context 上下文,将会传递到监听回调函数中
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

监听回调:

1
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;

移除监听:

1
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

KVO使用大家都比较熟悉,Demo应该就没有写的必要了,下面我们直接来探索下本质

2.KVO的本质

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
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

self.person1 = [[Person alloc] init];
self.person1.age = 10;

self.person2 = [[Person alloc] init];
self.person2.age = 15;

// 输出 >> 监听之前:Person, Person
NSLog(@"监听之前:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
// 输出 >> 监听之前 setter 方法:0x10d2fb550, 0x10d2fb550
NSLog(@"监听之前 setter 方法:%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

// 输出 >> 监听之后:NSKVONotifying_Person, Person
NSLog(@"监听之前:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
// 输出 >> 监听之后 setter 方法:0x10d643bf4, 0x10d2fb550
NSLog(@"监听之后 setter 方法:%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);

// (lldb)po ([NSKVONotifying_Person class]).superclass Person
// (lldb) p (IMP)0x10d2fb550 (IMP) $0 = 0x000000010d2fb550 (KVO与KVC`-[Person setAge:] at Person.h:13)
// (lldb) p (IMP)0x10d643bf4 (IMP) $1 = 0x000000010d643bf4 (Foundation`_NSSetLongLongValueAndNotify)
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 20;
self.person2.age = 25;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到:%@ 的:%@ 属性值改变了,change:%@ - context:%@", object, keyPath, change, context);
}

通过断点调试,我们发现 person2 的类对象没有发生变化,person1 的类对象变成了 NSKVONotifying_Person,而且是Person的子类。

使用 Runtime 打印 NSKVONotifying_Person 方法列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)printMethodNamesOfClass:(Class)cls {
unsigned int methCount = 0;
NSMutableArray *methodArr = [NSMutableArray array];
Method *meths = class_copyMethodList(cls, &methCount);
for(int i = 0; i < methCount; i++) {
Method meth = meths[i];
SEL sel = method_getName(meth);
NSString *methodName = NSStringFromSelector(sel);
[methodArr addObject:methodName];
}
if (methodArr.count) NSLog(@"%@", methodArr);
free(meths);
}

  • NSKVONotifying_Person 实例方法列表:
    • setAge:
    • class
    • dealloc
    • _isKVOA

总结:
1> 添加监听时,利用RuntimeAPI动态生成Person子类: NSKVONotifying_Person ,并且使 person1 的 isa 指针指向新的类
2> 重写 setAge: ,person1 调用 setter 方法时会从 NSKVONotifying_Person 开始查找,在自己的类对象中能够找到,所以会调用自己的 setAge:方法( 会调用Foundation的_NSSetValueAndNotify函数)
3> _NSSet
ValueAndNotify 调用流程:willChangeValueForKey -> [super setAge:] (Person 的 setter 方法) -> didChangeValueForKey(同时触发 observeValueForKeyPath 监听回调方法,订阅者接收)
4> class 方法:重写 class 方法的目的是什么呢?(lldb) po self.person1.class 输出为:Person,原来,苹果粑粑是想要隐藏NSKVONotifying_Person,让开发者无感,使用时与未添加监听时无异
5> dealloc方法:释放 KVO 新产生的资源
6> _isKVOA方法:标记这个新类 KVO 机制新建的

对willChangeValueForKey/didChangeValueForKey还有疑惑的同学可以在 Person.m 中对这两个方法进行重写,再进行调试以变理解。

深入理解RunLoop

发表于 2018-09-04 | 更新于 2018-09-11 | 分类于 iOS底层原理

深入理解RunLoop

什么是 RunLoop,RunLoop的应用范畴,RunLoop内部实现逻辑,RunLoop与线程的关系,timer与RunLoop的关系,RunLoop是怎么响应用户操作的,RunLoop的几种状态,RunLoop的mode作用是什么?

什么是RunLoop

顾名思义,RunLoop是运行循环的意思,就是在程序运行中循环做一些事情。

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {
@autoreleasepool {
// 如果没有runloop,程序执行到这里就结束退出了
return 0;
// 因为有runloop,程序才能保持运行
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

RunLoop的基本作用:保持程序的持续运行、处理App中的各种事件(eg:触摸事件、定时器事件)、节省CPU资源,提高程序性能:有事做做事,没事做休息

RunLoop的应用范畴

RunLoop主要应用场景有以下几个:
1> 定时器(Timer)、performSelector
2> GCD Asyc Main Queue
3> 事件响应、手势识别、界面刷新
4> 网络请求
5> AutoreleasePool

RunLoop 对象

iOS中有2套API来访问和使用RunLoop:
1> Foundation : NSRunLoop
2> Core Foundation : CFRunLoopRef

1
2
3
4
5
6
// 两种获取 RunLoop 方式
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop];

CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
CFRunLoopRef mainRunloop2 = CFRunLoopGetMain();

NSRunLoop和CFRunLoopRef都代表着RunLoop对象,NSRunLoop是基于CFRunLoop的一层OC封装。

CFRunLoopRef是开源的,点击查看

RunLoop与线程

1> 每条线程都有位移的一个与之对应的RunLoop对象
2> RunLoop保存在一个全局的Dictionary,线程作为key,RunLoop作为对象
3> 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建,主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
4> RunLoop会在线程结束时销毁

源码分析

打开刚才下载的源码,我们发现CF的库是没有xcode项目的。新建一个项目,讲所有文件拖进来。

搜索RunLoop.c文件,CFRunLoopGetMain(void)函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
...
// 如果不存在,create
if (NULL != rlm && NULL == rlm->_sources0) {
...
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
// 方便理解,省略一些代码,感兴趣的同学可以深入研究
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
...
return loop;
}

里面调的是_CFRunLoopGet0(pthread_t t)这个函数,内部调用的(CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));,这个函数,根据方法名,我们可以猜测,应该是个字典,传入的参数是一个pthreadPointer线程指针,返回一个runloop对象,也证实了刚才所述的字典管理猜想。

RunLoop相关的类

Core Foundation中关于RunLoop的5个类
1> CFRunLoopRef
2> CFRunLoopModeRef
3> CFRunLoopSourceRef
4> CFRunLoopTimerRef
5> CFRunLoopObserverRef

CFRunLoopRef底层结构:

1
2
// CFRunLoopRef 底层是这种类型结构体
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

继续搜索 struct __CFRunLoop,会发现搜索出来很多,一个小技巧快速定位,在后面加一个空格和大括号:struct __CFRunLoop {

1
2
3
4
5
6
7
8
9
10
struct __CFRunLoop {
// 实际结构体成员比这个多,将几个重点的选出:
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
// 当前模式
CFRunLoopModeRef _currentMode;
// _models 存储了多个 CFRunLoopModeRef 对象,只有一个为 CurrentMode
CFMutableSetRef _modes;
};

我们发现一个RunLoop对象里面有一个pthread_t线程,所以每个线程都有一个与之对应的RunLoop;CFMutableSetRef上层与之对应NSMutableSet,与NSMutableArray用法类似,区别是数组有序,可以使用index进行取值;集合无序,取值只能通过[set anyObject]方式。

CFRunLoopModeRef底层结构是什么样子

跟刚才小技巧一样,我们直接搜索struct __CFRunLoopMode {

1
2
3
4
5
6
7
8
9
10
11
12
13
struct __CFRunLoopMode {
...
// 同样将本文关心之外的省略
// mode 名称
CFStringRef _name;
// _sources0/_sources0 集合存储CFRunLoopSourceRef对象
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
// _observers 集合存储 CFRunLoopObserverRef 对象
CFMutableArrayRef _observers;
// _timers 存储 CFRunLoopTimerRef 对象
CFMutableArrayRef _timers;
};

总结:
1> CFRunLoopModeRef代表着RunLoop的运行模式
2> 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
3> RunLoop启动时只能选择其中一个Mode,作为currentMode
4> 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。这样做的好处是不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响,系统一个时刻只处理一种mode下面的事件
5> 如果一个Mode里面没有任何的Source0/Source1/Timer/Observer,RunLoop会立马退出
如下图:
RunLoop结构

常用的Mode

1> kCFRunLoopDefaultMode (NSDefaultRunLoopMode):App默认Mode,通常主线程是在这个Mode下运行

2> UITrackingRunLoopMode:界面跟踪Mode,用于Scrollow追踪触摸滑动,保证界面在滑动时不受其他Mode影响

3> kCFRunLoopCommonModes 通用模式,默认包含上面两种模式

Source0/Source1/Timers/Obvers处理什么事件

RunLoop结构
断点touchesBegan,打印堆栈,我们发现,时间调用从Source0开始,到最后的touchesBegan

Source0:
1> 触摸事件处理
2> performSelector:onThread:

Source1:
1> 基于Port的线程间通信
2> 系统时间捕捉(eg.上面的点击事件虽然最后是Source0处理的,但是最开始其实是Source1来进行捕捉的,然后分发给Source0来进行处理)

Timers:
1> NSTimer
2> performSelector:withObject:afterDelay:

Observers:
1> 用于监听RunLoop的状态
2> UI刷新(BeforeWaiting,在休眠之前刷新UI)
3> Autorelease pool(BeforeWaiting,休眠之前将池内需要操作的对象进行一次release操作)

RunLoop几种状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即将进入 Loop
kCFRunLoopEntry = (1UL << 0),
// 即将处理 Timer
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理 Source
kCFRunLoopBeforeSources = (1UL << 2),
// 即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 刚从休眠中唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 即将退出 Loop
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

监听 RunLoop 状态:

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
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

// 创建 Observer
#if 1
// C 函数方式
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observerRunLoopActivities, NULL);
#elif 0
// block 方式
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

});
#endif
// 添加 Observer
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
}

void observerRunLoopActivities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
case kCFRunLoopAllActivities:
NSLog(@"kCFRunLoopAllActivities");
break;
default:
break;
}
}

OC对象的本质(下)

发表于 2018-08-26 | 更新于 2018-09-11 | 分类于 iOS底层原理

iOS | OC对象本质 | Objective-C

OC对象的分类,对象的isa指针指向哪里,superClass指针,OC的类信息存放在哪里?

上篇文章中,我们讲述了一个 NSObject实例对象,Person实例对象的本质,在内存中长什么样子。但是,上篇文章中讲的OC对象的本质,还不包括所有的OC对象。

1.OC对象的分类

Objective-C中的对象,简称OC对象,主要可以分为 3 种:

  • instance对象(实例对象)
    instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象
    1
    2
    NSObject *object1 = [[NSObject alloc] init];
    NSObject *object2 = [[NSObject alloc] init];

instance对象
1> object1、object2是NSObject的instance对象(实例对象)
2> 它们是不同的两个对象,分别占据着两块不同的内存
3> instance对象在内存中存储信息包括:isa指针、其他成员变量

  • class对象(类对象)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // instance 对象,实例对象
    NSObject *object = [[NSObject alloc] init];

    // class 对象,类对象
    Class objectClass1 = [object class];
    Class objectClass2 = [NSObject class];
    // class 方法返回的一直是class对象,类对象
    // Class objectClass2 = [[[[NSObject class] class] class] class];
    Class objectClass3 = object_getClass(object);

    // 0x100444830 0x7fffa5ab9140 0x7fffa5ab9140 0x7fffa5ab9140
    NSLog(@"%p %p %p %p", object, objectClass1, objectClass2, objectClass3);

class对象
1> objectClass1~objectClass3都是NSObject的class对象(类对象)
2> 它们都是同一个对象,每个类在内存中有且只有一个class对象
3> class对象在内存中存储的信息主要包括:isa指针、superClass指针、类的属性信息(@property)、类的实例方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar,这里指的并不是成员变量的值是多少,成员变量的值是实例对象进行存储的,类对象存储的是成员变量的类型、变量名等只需要存储一份的)等…

  • meta-class对象(元类对象)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // class 对象,类对象
    Class objectClass = [object class];

    // meta-class 对象,元类对象
    // 将类对象当做参数传入,获得元类对象;将实例对象当做参数传入,获得的是类对象
    Class metaClass1 = object_getClass([object class]);
    Class metaClass2 = object_getClass([NSObject class]);
    Class metaClass3 = object_getClass(object_getClass(object));

    // 0x7fffa5ab90f0 0x7fffa5ab90f0 0x7fffa5ab90f0 0 1
    NSLog(@"%p %p %p %d %d", metaClass1, metaClass2, metaClass3, class_isMetaClass(objectClass), class_isMetaClass(metaClass1));

1> objectMetaClass是NSObject的meta-class对象(元类对象)
2> 每个类在内存中有且只有一个meta-class对象
3> meta-class对象和class对象的内存结构是一样的(都是 Class 类型),但是用途不一样,在内存中存储的信息主要包括:isa指针、superClass指针、类的类方法信息(class method)、等…
meta-class对象
注意:为什么说 meta-class 对象和 class 对象结构一样,但是图上画的却不一样呢,因为图上只是将比较重要的一些东西摘了出来,方便理解。其实本质是,class对象类方法信息存储的可能是null空的,meta-class内部属性信息、对象方法信息、协议信息、成员变量信息存储的可能是null空的。

objc_getClass、object_getClass方法区别?
本文和上篇文章有用到这几个方法,这几个方法有什么区别呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;
// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}

Class object_getClass(id obj)
{
// 如果传入instance对象,返回class对象
// 如果传入class对象,返回meta-class对象
// 如果传入meta-class对象,返回NSObject的meta-class对象
if (obj) return obj->getIsa();
else return Nil;
}

从源码进行分析,objc_getClass传入参数为字符串,根据字符串去Map中取出类对象并返回。 object_getClass传入参数为 id,并且返回值是通过 getIsa 获得,说明返回 isa 指向的类型(即:传入instance对象,返回类对象;传入class对象,返回meta-class对象;传入meta-class对象,返回NSObject的meta-class对象)。

2.isa指针

我们发现,OC对象不管是instance对象、类对象还是meta-class都有一个isa指针,那么,isa指针都指向哪里呢,起到了什么作用。

我们都知道,OC对象调用方法是通过消息机制实现的,通过上面的总结我们也知道了实例方法存放在class对象中,类方法存放在meta-class对象中,那么对象是怎么查找到方法并实现调用呢?

这个时候就需要isa指针了,instance对象的isa指针指向class对象,class对象的isa指向meta-class对象,通过isa指针,instance对象、class对象、meta-class对象就可以串起来了,方法调用、以及各种作用就都可以实现了。

isa指针

  • instance对象的isa指向class对象,当调用对象方法时,通过instance对象的isa找到class对象,最后找到对象方法的实现进行调用
  • class对象的isa指向meta-class对象,当调用类方法时,通过class对象的isa找到meta-class对象,最后找到类方法的实现进行调用

注意:isa指针并不是直接指向对象地址值,还需要逻辑与上一个掩码 ISA_MASK,这个了解下就行,如果不了解的话可以直接理解为isa直接指向class对象、meta-class对象。

superClass指针

附上一张经典的图:
superClass && isa
总结如下:

  • instance对象的isa指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象
  • class对象的superClass指向父类的class对象,如果没有父类,superClass指针为nil
  • meta-class对象的superClass指针指向父类的meta-class对象,基类的meta-class对象的superClass指针指向基类的class对象
  • instance调用方法的轨迹,isa找到class对象,方法不存在,就通过superClass寻找父类
  • class调用类方法的轨迹,isa找到meta-class,方法不存在,就通过superClass寻找父类

3.窥探 objc_class 结构

类对象与元类对象的结构是一样的,通过都是class类型就可以证明。即 typedef struct objc_class *Class; 这个结构体,接下来我们一起来探究下这个结构体内部结构。

我们搞懂这个结构体之后也就可以知道class对象和meta-class对象里面到底放了什么东西了。

点击 objc_class 进入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

进入之后,我们发现,内部如我们想象之中差不多,确实是有 ivars、methodLists、protocols之类的。

但是,我们发现里面有一个条件编译:#if !OBJC2,意思是如果不是 OBJC2 的话才会编译这段代码,现在最新已经是 OC2.0 了,所以,这段代码相当于已经过时了,这个结构体在 OC2.0 时已经不可用了,我们要研究学习的话建议还是参考最新的代码,这个已经研究价值不大了。

打开上篇文章带领大家下载的 objc4源码,搜索 struct objc_class, 找到 objc-runtime-new.h 中 结构体的最新定义:

我们发现 struct objc_class : objc_object,结构体也可以继承,对,没错,这是 C++ 语法,C++ 里面的结构体跟类其实区别不大,基本上卫衣的区别是成员变量默认 public 还是private。C++ 结构体里面还可以定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();

......
方便理解,部分与本文讲述联系不大的先省略了,感兴趣的同学可以自行查看

我们可以发现:struct objc_object 结构体内部只有一个 isa 成员以及一些方法。

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

class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}

void setInfo(uint32_t set) {
assert(isFuture() || isRealized());
data()->setFlags(set);
}

void clearInfo(uint32_t clear) {
assert(isFuture() || isRealized());
data()->clearFlags(clear);
}

......
方便理解,部分与本文讲述联系不大的先省略了,感兴趣的同学可以自行查看

我们发现其内部有一个 class_rw_t,即可读、可写、table,一个可读可写的表格,而且一些信息都在这个表格里面:

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
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

......
方便理解,部分与本文讲述联系不大的先省略了,感兴趣的同学可以自行查看
};

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

总结如下图:
struct objc_class的结构

总结: 通过 struct objc_class 内部结构,内部确实有存放 方法、成员变量、协议 的数组,也证明 class对象/meta-class对象 内部确实是如我们所才像一样。对象方法、属性、成员变量、协议信息,存放在class对象中;类方法,存放在meta-class对象中;成员变量的具体值,存放在instance对象中。

OC对象的本质(上)

发表于 2018-08-19 | 更新于 2018-09-11 | 分类于 iOS底层原理

iOS | OC对象本质 | Objective-C

什么是OC语言,OC对象、类的本质是什么,OC对象的内存布局是什么样子的,一个NSObject对象占用多少内存,一个自定义类的对象的本质及占用多少内存?

万物皆对象,可能是大部分程序员对这个问题的第一反应,再往具体说,可能就不太知道怎么描述了,本文将围绕这几个问题进行展开。


1.什么是OC语言?

首先我们谈谈什么是做编程语言,编程语言是一种让人们能读懂并且能够展现程序的执行行为的语言,包括语法(正确的表达式以及状态机的使用规则)以及语义(如何去组织这些表达式以及状态机以一种有意义的方式去完成我们的目标)。

我们平时编写的Objective-C代码,底层实现都是C/C++代码,Objective-C的面向对象都是基于C/C++的数据结构实现的。

OC对象的本质

2.OC对象、类的本质是什么?

OC对象、类是基于C\C++的什么数据结构实现的?

我们可以将Objective-C代码转换为C\C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

如果需要链接其他框架,使用-framework参数。比如-framework UIKit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>

// NSObject Implementation (NSObject 底层实现)
struct NSObject_IMPL {
Class isa;
};

// isa 本质就是一个指向 objc_class 结构体的指针
typedef struct objc_class *Class;

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}

我们可以发现,OC对象、类其实是基于C\C++结构体实现的。

3.一个OC对象在内存中是如何布局的?

一个OC对象的内存布局入下图所示:
OC对象的本质

4.一个NSObject对象占用多少内存?

既然 isa 本质上就是一个指针,一个指针在32位环境下占用4个字节,64位环境下占用8个字节。一个 NSObject 对象结构体内部就包含一个 isa 指针,那么,我们可以认为一个 NSObject 对象就占用8个字节么? NO NO NO,虽然表面如此,但是实际上并不是。

1
2
3
4
5
6
7
NSObject *obj = [[NSObject alloc] init];

// 获得 NSObject 实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 输出为8

// 获得 obj 指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)(obj))); // 输出为 16

我们也可以通过 Xcode -> Debug -> Debug Workflow -> View Memory
View Memory
或者 LLDB 进行查看
LLDB常用指令

这个是什么意思呢,其实一个 NSObject 实例对象的大小确实为8个字节,但是系统给其分配的内存其实是16个字节,接下来我们通过objc4源码来探究下到底是为什么。

objc源码: https://opensource.apple.com/tarballs/objc4/

objc源码
objc4源码

打开下载好的objc4源码,搜索class_getInstanceSize方法

1
2
3
4
5
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}

我们会发现这个方法返回值是cls->alignedInstanceSize(),点进去查看如下:

1
2
3
4
5
// Class's ivar size rounded up to a pointer-size boundary.
// 注释意思:返回值成员变量的占用内存大小
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

我们继续看下 malloc_size,由于苹果部分源码不公开,不过不影响今天讨论内容,我们先 malloc.h 文件中函数声明:

1
2
3
/* Returns size of given ptr */
// 注释意思:返回分配给指针的占用内存大小
extern size_t malloc_size(const void *ptr);

总结:通过阅读源码,发现一个 NSObject 对象,系统给其分配的空间为 16 个字节,只不过其真正利用起来的只有 8 个字节。

真的是分配 16 个字节么?

1
NSObject *obj = [[NSObject alloc] init];

上面这行代码,可以发现,创建一个新的实例对象,分为两步:

alloc:分配一块内存空间
init:初始化

所以,我们想探究实质的话可以从 alloc 方法往里面查看,从 alloc 开始搜索的话太多了,我们直接从 allocWithZone 开始查看,感兴趣的同学可以从 alloc 开始进行查看。

1
2
3
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

allocWithZone 调用的是: _objc_rootAllocWithZone

1
2
3
4
5
6
7
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
/* 为了更方便理解,将部分代码省略 */
obj = class_createInstance(cls, 0);
return obj;
}

_objc_rootAllocWithZone 分配内存空间其实是: class_createInstance

1
2
3
4
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}

继续点击进去查看:

1
2
3
4
5
6
7
8
9
10
11
static __attribute__((always_inline)) 
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
/* 为了更方便理解,将部分代码省略 */
size_t size = cls->instanceSize(extraBytes);

id obj;
obj = (id)calloc(1, size);

return obj;
}

我们发现,最后调用 C 语言底层的 calloc 分配内存函数,我们发现传入了一个 size 参数, size 通过 cls 的 instanceSize 函数获得。

嘿,哥们儿,别睡了,重点来了:

1
2
3
4
5
6
7
8
9
10
11
12
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

size_t instanceSize(size_t extraBytes) {
// 如果是 NSObject ,下面这行代码相当于 size_t size = 8;
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

通过注释和代码可以发现,CF:CoreFoundation,硬性规定,返回 size 最小为16。

这是为什么呢,因为苹果设计 CF 框架,包括我们自己设计一套框架,为了我们的框架能够更好的运行,肯定会做出一些规定、约束,这样就可以理解了。

至于 word_align,涉及到 内存对齐 概念,下面的的章节也会提到一些,但不会涉及太深,感兴趣的同学可以 Google 相关文档。


接下来,我们可以对这个问题做下总结:

一个NSObject对象占用多少内存?

  • 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
  • 但 NSObject 对象内部只使用了8个字节的空间(64bit环境下,可以通过 class_getInstanceSize 函数获得)

5.一个自定义类的对象占用多少内存?

讲到这里,相信很多小伙伴还是有很多疑问的。刚才只讲了NSObject相关知识。我们平常开发中肯定不会只用NSObject对象,基本上都是我们自定义自己的对象,接下来,来通过两个复杂一点的例子来进行讲解。

(1)自定义一个 Student 类继承 NSObject :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Student : NSObject {
@public
int _no;
int _age;
}
@end

@implementation Student
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
}
return 0;
}

根据上文通过 NSObject 实例对象讲解的铺垫,Student 实例对象的本质以及其在内存中布局如下图所示:

Student 实例对象本质:
Student实例对象本质
Student 实例对象内存布局:
Student实例对象内存布局_1
从图中最下面把实例对象 stu 强转成结构体类型 stu2,通过结构体可以正常进行访问,也从另一角度证明 stu 底层结构确实为 Student_IMPL 结构体类型。当然也可以从 View Memory 或者 LLDB 进行证明。

内存布局这样画可能理解更清楚:
Student实例对象内存布局_2

(2)举一反三,当 Person 继承 NSObject,Student 继承 Person 的情况,一个 Person 对象,一个 Student 对象占用多少内存空间?
Student: Person: NSObject:

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
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

struct NSObject_IMPL {
Class isa;
};

struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};

struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no;
};

@interface Person: NSObject {
@public
int _age;
}
@end

@implementation Person
@end

@interface Student: Person {
@public
int _no;
}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu->_no = 5;
NSLog(@"student:%zd, %zd", class_getInstanceSize([Student class]), malloc_size((__bridge const void*)stu));

Person *per = [[Person alloc] init];
per->_age = 4;
NSLog(@"person:%zd, %zd", class_getInstanceSize([Person class]), malloc_size((__bridge const void*)per));
}
return 0;
}

Student: Person: NSObject

我们先来分析下 Person 实例对象占用多少内存空间:
struct NSObject_IMPL NSObject_IVARS;即 isa 占用8个字节,int _age; 占4个字节,那么 Person 实例对象占 8 + 4 = 12 个字节么,错,上文中也有提到,一个 OC 对象至少占用 16 个字节,所以 Person 实例对象占用 16 个字节。

从另外一个角度,其实还有 内存对齐 这个概念,就算是没有 OC对象 至少占用 16 个字节这个规定, Person_IMPL 也占用 16 个字节,内存对齐有一条规定:结构体的大小比必须是最大成员大小的倍数。

内存对齐还有很多规定,属于计算机知识范畴,感兴趣的同学可以自行 Google。

我们再来分析下 Student 实例对象占用多少内存空间:

  • struct Person_IMPL Person_IVARS; 占用 16 个字节,int _no;占用 4 个字节,16 + 4 = 20,而且刚讲了内存对齐规定结构体大小必须是最大成员变量大小的倍数,那么, Student_IMPL 占用 16 2 = 32 个字节么?错!结果为 16 1 = 16 个字节。
  • 为什么呢?因为 Person_IMPL 虽然分配16个字节,但是实际变量只占用了 12 个字节,还有 4 个字节空出来了,我们伟大的 iOS 系统会这么傻,白白浪费这 4 个字节的空间么,当然不会,所以,int _no;其实被放到了 Person_IMPL 空余的 4 个字节空间当中。

malloc_size 我们已经没有太多疑问了,但是可能对 class_getInstanceSize 还存在疑问,class_getInstanceSize 返回 ivar size,即成员变量 size,那么上文 Person instance size 为什么不返回 12 呢?
有疑问怎么办,撸源码:

1
2
3
4
5
6
7
8
9
10
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

通过源码可以发现,class_getInstanceSize 实际返回的其实也是 word_align(unalignedInstanceSize()); 内存对齐过的大小。

总结:我们更多的时候其实不需要过多关注 class_getInstanceSize,我们只需要关注 malloc_size 返回的实际分配的内存大小即可。

结尾

通过今天的讲解,希望可以给一些小伙伴带来帮助,刚开始写博客,排版略乱,还请见谅,文中错误或者不足欢迎指点。

本文demo

开篇

发表于 2018-08-18 | 更新于 2018-09-11 | 分类于 关于我

欢迎来到小马同学的技术博客

iOS | 技术分享 | 学习交流

开篇

Stay hungry. Stay Foolish. - Steve Jobs. 虚心若渴,求知若愚!

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

作为一位iOS开发工程师,开场当然先要致敬下乔布斯老爷子。Steve Jobs 于2005年6月12号在斯坦福大学的毕业典礼上面的演讲,主要谈及了他人生中的三点感悟:因果、得失、死亡。最后以 “Stay Hungry. Stay Foolish.”总结。

关于我

iOS小白一枚,写这个博客,首先是为了总结,通过写作来提高自己的写作和表达能力。其次是想把自己理解的东西分享给大家,与大家进行交流,也希望大家可以指出我的不足并提供宝贵的意见!

欢迎交流指点并提出宝贵意见

  • Github:@小马同学的Github
  • 简书:@小马同学的简书
  • 邮箱:zzumalei@163.com
  • QQ:492843636
12
小马同学

小马同学

iOS小白一枚,写这个博客,首先是为了总结,通过写作来提高自己的写作和表达能力。其次是想把自己理解的东西分享给大家,与大家进行交流,也希望大家可以指出我的不足并提供宝贵的意见!

18 日志
4 分类
14 标签
GitHub E-Mail 简书
© 2019 小马同学
主题 – NexT.Muse v6.4.0
总访问量次 | 总访客人 |