iOS中的“锁事”

抛砖引玉

说到锁不得不提线程安全,说到线程安全,作为iOS程序员又不得不提 nonatomicatomic

  • nonatomic 不会对生成的 gettersetter 方法加同步锁(非原子性)
  • atomic 会对生成的 gettersetter 加同步锁(原子性)

setter / getteratomic 修饰的属性时,该属性是读写安全的。然而读写安全并不代表线程安全。

线程安全概念(thread safety)

  • 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
  • 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

验证 atomic 非线程安全

  • 验证代码
#import "ViewController.h"

@interface ViewController ()

@property (strong, atomic) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //atomic非线程安全验证
    //Jack
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            self.name = @"Jack";
            NSLog(@"Jack is %@", self.name);
        }
    });
    
    //Rose
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            self.name = @"Rose";
            NSLog(@"Rose is %@", self.name);
        }
    });
}
  • 验证结果
2017-11-29 11:21:27.713446+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.713487+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.713638+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.713659+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.713840+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.714050+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.714205+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.718069+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.718069+0800 LockDemo[42637:1199500] Jack is Rose
2017-11-29 11:21:27.718199+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.718199+0800 LockDemo[42637:1199499] Rose is Jack

最后一行和倒数第三行可以看到,atomic 非线程安全验证完毕。

  • 也就是说 atomic 只能做到读写安全并不能做到线程安全,若要实现线程安全还需要采用更为深层的锁定机制才行。
  • iOS开发时一般都会使用 nonatomic 属性,因为在iOS中使用同步锁的开销较大,这会带来性能问题,但是在Mac OS X程序时,使用 atomic 属性通常都不会有性能瓶颈。

锁的概念

在计算机科学中,锁是一种同步机制,用于在存在多线程的环境中实施对资源的访问限制。

锁的作用

  • 通俗来讲:就是为了防止在多线程的情况下对共享资源的脏读或者脏写。
  • 也可以理解为:执行多线程时用于强行限制资源访问的同步机制,即并发控制中保证互斥的要求。

iOS开发中常用的锁

  • @synchronized
  • NSLock 对象锁
  • NSRecursiveLock 递归锁
  • NSConditionLock 条件锁
  • pthread_mutex 互斥锁(C语言)
  • dispatch_semaphore 信号量实现加锁(GCD
  • OSSpinLock 自旋锁
性能图来源:ibireme

@synchronized

@synchronized 其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读性。
@synchronized 是我们平常使用最多的但是性能最差的。
OC写法:

@synchronized(self) {
    //需要执行的代码块
}

swift写法:

objc_sync_enter(self)
//需要执行的代码块
objc_sync_exit(self)

代码示例:

    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(self) {
            NSLog(@"第一个线程同步操作开始");
            sleep(3);
            NSLog(@"第一个线程同步操作结束");
        }
    });
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        @synchronized(self) {
            NSLog(@"第二个线程同步操作");
        }
    });

结果:

2017-11-29 14:36:52.056457+0800 LockDemo[46145:1306472] 第一个线程同步操作开始
2017-11-29 14:36:55.056868+0800 LockDemo[46145:1306472] 第一个线程同步操作结束
2017-11-29 14:36:55.057261+0800 LockDemo[46145:1306473] 第二个线程同步操作
  • @synchronized(self) 指令使用的 self 为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的 self 改成其它对象,线程2就不会被阻塞。
    NSString *s = [NSString string];
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(self) {
            NSLog(@"第一个线程同步操作开始");
            sleep(3);
            NSLog(@"第一个线程同步操作结束");
        }
    });
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        @synchronized(s) {
            NSLog(@"第二个线程同步操作");
        }
    });
2017-11-29 14:43:54.930414+0800 LockDemo[46287:1312173] 第一个线程同步操作开始
2017-11-29 14:43:55.930761+0800 LockDemo[46287:1312158] 第二个线程同步操作
2017-11-29 14:43:57.932287+0800 LockDemo[46287:1312173] 第一个线程同步操作结束
  • @synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理来保护代码,该处理会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

NSLock

  • NSLock 中实现了一个简单的互斥锁。通过 NSLocking 协议定义了 lockunlock 方法。
@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

举个栗子卖冰棍儿

- (void)nslockTest {
    //设置冰棍儿的数量为5
    _count = 5;
    
    //创建锁
    _lock = [[NSLock alloc] init];
    
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleIceCream];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleIceCream];
    });
}

- (void)saleIceCream
{
    while (1) {
        sleep(1);
        //加锁
        [_lock lock];
        if (_count > 0) {
            _count--;
            NSLog(@"剩余冰棍儿数= %ld, Thread - %@", _count, [NSThread currentThread]);
        } else {
            NSLog(@"冰棍儿卖光光  Thread - %@",[NSThread currentThread]);
            break;
        }
        //解锁
        [_lock unlock];
    }
}

加锁结果:

2017-11-29 16:21:29.728198+0800 LockDemo[55262:1411318] 剩余冰棍儿数= 4, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:29.728428+0800 LockDemo[55262:1411319] 剩余冰棍儿数= 3, Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
2017-11-29 16:21:30.729009+0800 LockDemo[55262:1411318] 剩余冰棍儿数= 2, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:30.729378+0800 LockDemo[55262:1411319] 剩余冰棍儿数= 1, Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
2017-11-29 16:21:31.733061+0800 LockDemo[55262:1411318] 剩余冰棍儿数= 0, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:31.733454+0800 LockDemo[55262:1411319] 冰棍儿卖光光  Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}

不加锁结果:

2017-11-29 16:23:38.702352+0800 LockDemo[55316:1412917] 剩余冰棍儿数= 3, Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:38.702352+0800 LockDemo[55316:1412919] 剩余冰棍儿数= 4, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:39.705096+0800 LockDemo[55316:1412919] 剩余冰棍儿数= 2, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:39.705099+0800 LockDemo[55316:1412917] 剩余冰棍儿数= 1, Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:40.709617+0800 LockDemo[55316:1412919] 剩余冰棍儿数= 0, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:40.709617+0800 LockDemo[55316:1412917] 冰棍儿卖光光  Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:41.714002+0800 LockDemo[55316:1412919] 冰棍儿卖光光  Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
  • NSLock 类还增加了 tryLocklockBeforeDate: 方法
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回NO。
lockBeforeDate: 方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)。

NSRecursiveLock 递归锁

有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁。

- (void)recursiveLockTest {
    //创建锁
    _lock = [[NSLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^TestMethod)(int);
        TestMethod = ^(int value)
        {
            [_lock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                value--;
                TestMethod(value);
            }
            [_lock unlock];
        };
        TestMethod(5);
        NSLog(@"结束");
    });
}

我们发现 "结束" 永远不会被打印出来,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

- (void)recursiveLockTest {
    //创建锁
    _recursiveLock = [[NSRecursiveLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^TestMethod)(int);
        TestMethod = ^(int value)
        {
            [_recursiveLock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                value--;
                TestMethod(value);
            }
            [_recursiveLock unlock];
        };
        TestMethod(5);
        NSLog(@"结束");
    });
}

此时 "结束" 5秒后会被打印出来。

NSConditionLock 条件锁

NSCoditionLock 做多线程之间的任务等待调用,而且是线程安全的。

- (void)conditionLockTest {
    
    NSInteger HAS_DATA = 1;
    NSInteger NO_DATA = 0;
    
    _conditionLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
    NSMutableArray *products = [NSMutableArray array];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            [_conditionLock lockWhenCondition:NO_DATA];
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"生产");
            [_conditionLock unlockWithCondition:HAS_DATA];
            sleep(5);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            NSLog(@"等待");
            [_conditionLock lockWhenCondition:HAS_DATA];
            [products removeObjectAtIndex:0];
            NSLog(@"售卖");
            [_conditionLock unlockWithCondition:NO_DATA];
        }
    });
}

NSConditionLock 也跟其它的锁一样,是需要 lockunlock 对应的,只是 lock , lockWhenCondition:unlockunlockWithCondition: 是可以随意组合的,当然这是与需求相关的。

POSIX(pthread_mutex)

  • C语言定义下多线程加锁方式。 pthread_mutex 和 dispatch_semaphore_t 很像,但是完全不同。pthread_mutex 是Unix/Linux平台上提供的一套条件互斥锁的API。
  • 新建一个简单的 pthread_mutex 互斥锁,引入头文件 #import <pthread.h> 声明并初始化一个 pthread_mutex_t 的结构。使用 pthread_mutex_lockpthread_mutex_unlock 函数。调用 pthread_mutex_destroy 来释放该锁的数据结构。

使用:
#import <pthread.h>

- (void)pthreadTest {
    __block pthread_mutex_t theLock;
    pthread_mutex_init(&theLock, NULL);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        pthread_mutex_lock(&theLock);
        NSLog(@"第一个线程同步操作开始");
        sleep(3);
        NSLog(@"第一个线程同步操作结束");
        pthread_mutex_unlock(&theLock);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        pthread_mutex_lock(&theLock);
        NSLog(@"第二个线程同步操作");
        pthread_mutex_unlock(&theLock);
        
    });
}

执行结果:

2017-11-29 17:51:11.901064+0800 LockDemo[56729:1466788] 第一个线程同步操作开始
2017-11-29 17:51:14.904834+0800 LockDemo[56729:1466788] 第一个线程同步操作结束
2017-11-29 17:51:14.905195+0800 LockDemo[56729:1466789] 第二个线程同步操作
  • pthread_mutex 还可以创建条件锁,提供了和 NSCondition 一样的条件控制,初始化互斥锁同时使用 pthread_cond_init 来初始化条件数据结构
    // 初始化
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
    // 等待(会阻塞)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
    // 定时等待
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
    // 唤醒
    int pthread_cond_signal (pthread_cond_t *cond);
    // 广播唤醒
    int pthread_cond_broadcast (pthread_cond_t *cond);
    // 销毁
    int pthread_cond_destroy (pthread_cond_t *cond);

pthread_mutex 还提供了很多函数,有一套完整的API,包含 Pthreads 线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。

dispatch_semaphore_t

dispatch_semaphore_t GCD中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量 +1;每当发送一个等待信号时信号量 -1,;如果信号量为 0 则信号会处于等待状态,直到信号量大于 0 开始执行。

api注释:

/*! 
 * @param value
 *信号量的起始值,当传入的值小于零时返回NULL
 * @result
 * 成功返回一个新的信号量,失败返回NULL
 */
dispatch_semaphore_t dispatch_semaphore_create(long value)

/*!
 * @discussion
 * 信号量减1,如果结果小于0,那么等待队列中信号增量到来直到timeout
 * @param dsema
 * 信号量
 * @param timeout
 * 等待时间
 * 类型为dispatch_time_t,这里有两个宏DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
 * @result
 * 若等待成功返回0,timeout返回非0
 */
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

/*!
 * @discussion
 * 信号量加1,如果之前的信号量小于0,将唤醒一条等待线程
 * @param dsema 
 * 信号量
 * @result
 * 唤醒一条线程返回非0,否则返回0
 */
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

使用:

- (void)semaphoreTest {
    // 创建信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务1");
        sleep(10);
        dispatch_semaphore_signal(semaphore);
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务2");
        dispatch_semaphore_signal(semaphore);
    });
}

执行结果:

2017-11-30 14:38:11.943521+0800 LockDemo[91493:2075379] 任务1
2017-11-30 14:38:21.946222+0800 LockDemo[91493:2075380] 任务2

OSSpinLock 自旋锁

使用:
#import <libkern/OSAtomic.h>

__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    NSLog(@"第一个线程同步操作开始");
    sleep(3);
    NSLog(@"第一个线程同步操作结束");
    OSSpinLockUnlock(&theLock);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    sleep(1);
    NSLog(@"第二个线程同步操作");
    OSSpinLockUnlock(&theLock);
});

执行结果:

2017-11-30 15:12:31.701180+0800 LockDemo[92422:2104479] 第一个线程同步操作开始
2017-11-30 15:12:39.705473+0800 LockDemo[92422:2104479] 第一个线程同步操作结束
2017-11-30 15:12:39.705820+0800 LockDemo[92422:2104478] 第二个线程同步操作开始

OSSpinLock 自旋锁,性能最高的锁。它的缺点是当等待时会消耗大量 CPU 资源,不太适用于较长时间的任务。 YY大神在博客 不再安全的 OSSpinLock 中说明了OSSpinLock已经不再安全,暂不建议使用。

iOS 10 之后,苹果给出了解决方案,就是用 os_unfair_lock 代替 OSSpinLock。

'OSSpinLockLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock_lock() from <os/lock.h> instead

#import <os/lock.h>

    __block os_unfair_lock  lock = OS_UNFAIR_LOCK_INIT;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        os_unfair_lock_lock(&lock);
        NSLog(@"第一个线程同步操作开始");
        sleep(8);
        NSLog(@"第一个线程同步操作结束");
        os_unfair_lock_unlock(&lock);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        os_unfair_lock_lock(&lock);
        NSLog(@"第二个线程同步操作开始");
        os_unfair_lock_unlock(&lock);
    });

执行结果:

2017-11-30 15:12:31.701180+0800 LockDemo[92422:2104479] 第一个线程同步操作开始
2017-11-30 15:12:39.705473+0800 LockDemo[92422:2104479] 第一个线程同步操作结束
2017-11-30 15:12:39.705820+0800 LockDemo[92422:2104478] 第二个线程同步操作开始

总结

  • @synchronized:适用线程不多,任务量不大的多线程加锁
  • NSLock:性能不算差,但感觉用的人不多。
  • dispatch_semaphore_t:使用信号来做加锁,性能很高和 OSSpinLock 差不多。
  • NSConditionLock:多线程处理不同任务的通信建议时用, 只加锁的话性能很低。
  • NSRecursiveLock:性能不错,使用场景限制于递归。
  • POSIX(pthread_mutex):C语言的底层api,复杂的多线程处理建议使用,也可以封装自己的多线程。
  • OSSpinLock:性能非常高,可惜不安全了,使用 os_unfair_lock 来代替。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容