毕业设计踩坑笔记

转眼间就从稚嫩的大一小鲜肉变成了大四老腊肉...心里还是有颇多感慨的。然而这篇文章并不打算走情感路线,主要是为了记录以及整理我整个毕业设计的过程。既方便后面写毕设论文,也可以和各位一起分享我的蛋疼毕设。

毕设主题

主题就是做一个**「高性能的JSON对象映射库」**。可能看到题目你看不太懂啥意思,但是作为一个iOS开发者,你一定知道最近流行起来的 YYKit 中的 YYModel 吧!没错,我的毕设内容就是分析市面上比较常用的这种库,然后做一个属于自己的库。

(其实当我看到组里其他同学的毕设主题都是做一个XXXApp的时候,我的内心是崩溃的X_X)(逃

前言

由于博主才疏学浅(呸!明明是不好好上课!),所以在做毕设之前寡人也下了决心,通过毕业设计来学习各种欠缺的知识。所以你将会看到我在下面各种摸爬滚打的痕迹。

[Update:我目前撸的Model库放在Github上面,因为还在摸爬滚打,所以还需要很多要写的地方。如果观众们有兴趣可以看瞧瞧~~顺便提几个issue指点我一下~Thx]

知识储备期

在略读 YYModel 的代码的时候,我发现有不少不懂的东西,所以就决定先去学会在源码中不会的东西,再来实现自己的库。这样在后面自己实现的时候才可以更加顺利一些,同时也可以学到不少知识。


OSSpinLock 自旋锁

最先映入眼帘的就是这个 OSSpinLock 了,因为看到 YYModel 的作者在其博客中发表了一篇有关 OSSpinLock 的文章:不再安全的 OSSpinLock。我就开始了锁相关的学习。

自旋锁的原理:

按照我的理解,通俗来讲,当资源已经被占用的时候,资源申请者就会不断地在那里循环查看自旋锁是否被释放。如果自旋锁没有被释放,则继续循环。直到自旋锁被释放,资源的申请者才可以占用资源。

自己在那旋转(循环),也因此得名:自旋锁。

用代码表示大概是这样的:

// 当锁被持有的时候,就在这里死循环(亦称作旋转)。直到锁被释放
while(lock_is_obtained()) {}
obtain_the_lock(); // 持有
do_sth(); // 临界区
release_the_lock(); // 释放

在 Wikipedia 中,有一段 8086 的汇编代码实现的 Spinlock,也值得一看:Spinlock

自旋锁的缺点:

  1. 容易导致死锁。有以下几种情况:
    • 递归:持有自旋锁的 CPU 想再次获得这个锁时
    • 持有自旋锁的 CPU 被阻塞时
  2. 会过多地占用 CPU 资源。尤其在持有自选锁的时间较长,且申请占有者较多的时候,会很容易占用大量的 CPU 资源。与其这样,不如将等待的时间用于其他的任务(比如用互斥锁)

OSSpinLock 的使用方法

OSSpinLock 是在 Objective-C 中封装好了的自旋锁。

#import <libkern/OSAtomic.h>

.......

static OSSpinLock lock;
static dispatch_once_t once;
dispatch_once(&once, ^{
	lock = OS_SPINLOCK_INIT; // lock 仅需要被初始化一次就好了
});
OSSpinLockLock(&lock);
do_sth();  // 临界区
OSSpinLockUnlock(&lock);

为何 OSSpinLock 不再安全?

在阅读了《不再安全的 OSSpainLock》以及稳重引用到的相关资料之后,我的理解是这样的:

在 iOS 中,当低优先级的线程持有了自旋锁时,想持有这个锁的高优先级的线程将 CPU 资源抢去忙等。两个线程都没做事,一个没有了时间片,另一个忙等着。文艺点说这叫「优先级反转(Priority Inversion)」,通俗点说就是占着茅坑不拉屎

替代 OSSpinLock

我们可以在有些开源项目里面已经把 OSSpinLock 给替换成了其他的东西\比如说 Apple 官方的 CFInternal(改前, 改后),Google 的 protobuf,YYModel 中的 YYClassInfo。同时 ReactiveCocoa 也正在考虑是将 OSSpinLock 改成 dispatch_semaphore 还是 pthread_mutex

温馨提示: 在不同优先级的线程中使用 dispatch_semaphore 来做锁的功能时请慎重。暂时不清楚是否会有优先级反转的情况出现,最保险的还是用 pthread_mutex

为啥要用 CFArray 而不用 NSArray?

在看代码的时候是懵逼的,因为里面有大量直接调用 CFArray 和 CFDictionary 的 API 的代码。很明显,作者不用 NS 那套简洁的 API,一定是想优化什么。然而瞎猜并没有什么实质性的卵用,所以我还是决定翻翻资料并着手做一些测试。

在查阅一些资料的时候我发现了一篇很老很老的文章:Obj-C Optimization: IMP Cacheing Deluxe。老到里面用来测试性能的工具 Shark 现在都已经没了(逃),几番搜索之后才知道那个工具现在已经被合入了 Instruments。

We can see that -[NSArray objectAtIndex:] calls a CoreFoundation function to do the actual work, which is vectored through a dyld_stub... By using CoreFoundation directly, we can gain a surprising bit of performance

文中指出,[NSArray objectAtIndex:] 方法会通过 dyld_stub(注意!!!这是神马我暂时还不知道啊喵!!!)。如果我们直接使用 Core Foundation 的 API,那么就可以拥有不小的性能优化。

- (void)usingNSArray:(NSArray *)arr {
    for (int i = 0; i < count; i++) {
        [arr objectAtIndex:i];
    }
}

- (void)usingCFArray:(NSArray *)arr {
    for (int i = 0; i < count; i++) {
        CFArrayGetValueAtIndex((CFArrayRef)arr, (CFIndex)i);
    }
}

这两个方法,在使用大小为 10,000,000 的 NSArray 时,分别消耗了0.299s,与0.159s。
(使用 XCTestCase 中的 measureBlock 计时,测试环境:Macbook Air, Mid 2013, 1.4 GHz Core i5)

可以看出,使用 [NSArray objectAtIndex:i] 与 CFArrayGetValueAtIndex() 的效率相差了一倍。虽然本测试并不够精准,但足以说明使用 Core Foundation 的 API 可以让这一类的操作变得更富有效率。

此外,美团博客也有一篇科普 Method Caching 的,可以看一看:深入理解Objective-C:方法缓存

How to Set or Get a Variable

以前只知道调用类的 setter 与 getter 方法比直接用 ivar 要慢一些,但从来没有真正地测试一下。这次就小小地做了一个实验:

@interface JYEasyModelTests : XCTestCase
@property (nonatomic, strong) NSNumber *number;
@end

@implementation JYEasyModelTests

- (void)testIvarSetting {
    NSInteger count = 10000000;

    [self measureBlock:^{    // 0.301 sec
        for (int i = 0; i < count; i++) {
            _number = @0;
        }
    }];
}

- (void)testSetterSetting {
    NSInteger count = 10000000;

    [self measureBlock:^{    // 0.542 sec
        for (int i = 0; i < count; i++) {
            self.number = @0;
        }
    }];
}

@end

从测试中我们可以看到,「直接用 ivar 来赋值」与 「使用 setter 来赋值」之间的效率差距在10,000,000次调用时,虽然不是很大,但也能说明一些问题了。

BNR 中有篇对比的文章也可作为参考。

文中也有一个实验,比较简单,也可以说明使用 property 的 getter 和 setter 效率相比来说是低于直接 ivar 赋值的。

// 循环里用 property
for (NSUInteger i = 0; i < loopSize; i++) {
    x += self.propertyValue;
}

// 循环里用 ivar
for (NSUInteger i = 0; i < loopSize; i++) {
    x += _ivarValue;
}

反汇编后:

; 循环里用 property
0xc4a7a:  mov    r0, r11         ; move address of "self" into r0
0xc4a7c:  mov    r1, r6          ; move getter method name into r1
0xc4a7e:  blx    0xc6fc0         ; call objc_msgSend(self, getterName)
0xc4a82:  vmov   d16, r0, r0     ; move the result of the getter into d16
0xc4a86:  subs   r5, #1          ; subtract 1 from our loop counter
0xc4a88:  vadd.f32 d9, d9, d16   ; add d9 and d16 and store result into d16
0xc4a8c:  bne    0xc4a7a         ; repeat unless our loop counter is now 0

; 循环里面用 ivar
0xc4aba:  vadd.f32 d9, d9, d0    ; add d9 and d0 and store result into d9
0xc4abe:  subs   r0, #1          ; subtract 1 from our loop counter
0xc4ac0:  bne    0xc4aba         ; repeat unless our loop counter is now 0

下面是一些我的理解与总结:

  1. 使用 setter 来赋值首先需要调用 setter 方法,然后 setter 方法中才会赋值。
  2. 使用 ivar 直接赋值是最快的,因为是编译后通过地址偏移访问的。虽然效率最高,但是我们并不能满足本毕业设计的需求,因为我们需要在运行时拿成员变量。也就引出了第三点:
  3. 在运行时获取成员变量。
  • 如何获取:通过 object_getIvar()object_setIvar()
  • 效率如何:没有测试,但在 YYModel 的一个 issue 中,作者有提到他做过测试,这两个方法效率并不高。
  • 缺点:如果在运行时使用这玩意儿,我们不仅没捞到什么好处,还要为它实现 strong, weak 这些的逻辑。
  1. ibireme 也在上面提到的那个 issue 中提到了另一个捷径来获取/赋值,是: 通过 OC 运行时实际上也能获得属性在对象 struct 里的地址偏移,是一个 ptrdiff_t 类型的数值,但我没见过有人能用这个值来直接访问内存地址。第一眼看着感觉可能可以搞搞,但仔细想想:即使搞搞可以弄出来,但苹果明明已经有了相关的API,效率还不高,那自己撸出来的获取/设置方法的效率也就不可高估了。所以暂时是不考虑这个的。

最后得出结论,我还是老老实实地用 setter 和 getter 吧。


待续..0v0..


参考资料: