iOS NSNotification使用及原理实现

概述

NSNotification是苹果提供的一种”同步“单向且线程安全的消息通知机制(并且消息可以携带信息),观察者通过向单例的通知中心注册消息,即可接收指定对象或者其他任何对象发来的消息,可以实现”单播“或者”广播“消息机制,并且观察者和接收者可以完全解耦实现跨层消息传递;

同步:消息发送需要等待观察者处理完成消息后再继续执行;

单向:发送者只发送消息,接收者不需要回复消息;

线程安全:消息发送及接收都是在同一个线性完成,不需要处理线程同步问题,这个后面会详述;

使用

NSNotification

NSNotification包含了消息发送的一些信息,包括name消息名称、object消息发送者、userinfo消息发送者携带的额外信息,其类结构如下:

@interface NSNotification : NSObject <NSCopying, NSCoding>

@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;

- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

@end

@interface NSNotification (NSNotificationCreation)

+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

- (instancetype)init /*API_UNAVAILABLE(macos, ios, watchos, tvos)*/;    /* do not invoke; not a valid initializer for this class */

@end

可以通过实例方式构建NSNotification对象,也可以通过类方式构建;

NSNotificationCenter

NSNotificationCenter消息通知中心,全局单例模式(每个进程都默认有一个默认的通知中心,用于进程内通信),通过如下方法获取通知中心短息:

对于macOS系统,每个进程都有一个默认的分布式通知中心NSDistributedNotificationCenter,具体可参见NSDistributedNotificationCenter

+ (NSNotificationCenter *)defaultCenter

具体的注册通知消息方法如下:

//注册观察者
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

注册观察者方法提供了两种形式:selectorblock,对于添加指定观察者对象的方式,observer不能为nilblock方式会执行copy方法,返回的是使用的匿名观察者对象,且指定观察者处理消息的操作对象NSOperationQueue

对于指定的消息名称name及发送者对象object都可以为空,即接收所有消息及所有发送对象发送的消息;若指定其中之一或者都指定,则表示接收指定消息名称及发送者的消息;

对于block方式指定的queue队列可为nil,则默认在发送消息线程处理;若指定主队列,即主线程处理,避免执行UI操作导致异常;

注意:注册观察者通知消息应避免重复注册,会导致重复处理通知消息,且block对持有外部对象,因此需要避免引发循环引用问题;

消息发送方法如下:

//发送消息
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

可以通过NSNotification包装的通知消息对象发送消息,也可以分别指定消息名称、发送者及携带的信息来发送,且为同步执行模式,需要等待所有注册的观察者处理完成该通知消息,方法才会返回继续往下执行,且对于block形式处理通知对象是在注册消息指定的队列中执行,对于非block方式是在同一线程处理;

注意:消息发送类型需要与注册时类型一致,即若注册观察者同时指定了消息名称及发送者,则发送消息也需要同时指定,否则无法接收到消息;

移除观察者方法如下:

//移除观察者
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

可移除指定的观察者所有通知消息,即该观察者不再接收任何消息,一般用于观察者对象dealloc释放后调用,但在ios9macos10.11之后不需要手动调用,dealloc已经自动处理;

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method. Otherwise, you should call this method or removeObserver:name:object: before observer or any object specified in addObserverForName:object:queue:usingBlock: or addObserver:selector:name:object:is deallocated.

也可以指定消息名称或者发送者移除单一或者所有的消息(通过置nil可移除对应类型下的所有消息);

NSNotificationQueue

NSNotificationQueue通知队列实现了通知消息的管理,如消息发送时机、消息合并策略,并且为先入先出方式管理消息,但实际消息发送仍然是通过NSNotificationCenter通知中心完成;

@interface NSNotificationQueue : NSObject
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;

- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;

- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;

- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

可以通过defaultQueue获取当前线程绑定的通知消息队列,也可以通过initWithNotificationCenter:来指定通知管理中心,具体的消息管理策略如下:

NSPostingStyle:用于配置通知什么时候发送

  • NSPostASAP:在当前通知调用或者计时器结束发出通知
  • NSPostWhenIdle:当runloop处于空闲时发出通知
  • NSPostNow:在合并通知完成之后立即发出通知。

NSNotificationCoalescing(注意这是一个NS_OPTIONS):用于配置如何合并通知

  • NSNotificationNoCoalescing:不合并通知
  • NSNotificationCoalescingOnName:按照通知名字合并通知
  • NSNotificationCoalescingOnSender:按照传入的object合并通知

对于NSNotificationQueue通知队列若不是指定NSPostNow立即发送模式,则可以通过runloop实现异步发送;

NSNotification与多线程

对于NSNotification与多线程官方文档说明如下:

In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.

即是NSNotification的发送与接收处理都是在同一个线程中,对于block形式则是接收处理在指定的队列中处理,上面已说明这点,这里重点说明下如何接收处理在其他线程处理。

For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.

如官方说明;对于处理通知线程不是主线程的,如后台线程,存在此处理场景,并且官方也提供了具体的实施方案:

一种重定向的实现思路是自定义一个通知队列(注意,不是NSNotificationQueue对象,而是一个数组),让这个队列去维护那些我们需要重定向的Notification。我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程,如果不是,则将这个Notification存储到我们的队列中,并发送一个mach信号到期望的线程中,来告诉这个线程需要处理一个Notification。指定的线程在收到信号后,将Notification从队列中移除,并进行处理。

官方demo如下:

@interface MyThreadedClass: NSObject
/* Threaded notification support. */
@property NSMutableArray *notifications;
@property NSThread *notificationThread;
@property NSLock *notificationLock;
@property NSMachPort *notificationPort;
 
- (void) setUpThreadingSupport;
- (void) handleMachMessage:(void *)msg;
- (void) processNotification:(NSNotification *)notification;
@end

通知线程定义类MyThreadedClass包含了用于记录所有通知消息的通知消息队列notifications,记录当前通知接收线程notificationThread,多线程并发处理需要的互斥锁NSLock,用于线程间通信通知处理线程处理通知消息的NSMachPort;并提供了设置线程属性、处理mach消息及处理通知消息的实例方法;

对于setUpThreadSupport方法如下:

- (void) setUpThreadingSupport {
    if (self.notifications) {
        return;
    }
    self.notifications      = [[NSMutableArray alloc] init];
    self.notificationLock   = [[NSLock alloc] init];
    self.notificationThread = [NSThread currentThread];
 
    self.notificationPort = [[NSMachPort alloc] init];
    [self.notificationPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
            forMode:(NSString __bridge *)kCFRunLoopCommonModes];
}

主要是初始化类属性,并指定NSMachPort代理及添加至处理线程的runloop中;若mach消息到达而接收线程的runloop没有运行时,内核会保存此消息,直到下一次runloop运行;也可以通过performSelectro:inThread:withObject:waitUtilDone:modes实现,不过对于子线程需要开启runloop,否则该方法失效,且需指定waitUtilDone参数为NO异步调用;

NSMachPortDelegate协议方法处理如下:

- (void) handleMachMessage:(void *)msg {
    [self.notificationLock lock];
 
    while ([self.notifications count]) {
        NSNotification *notification = [self.notifications objectAtIndex:0];
        [self.notifications removeObjectAtIndex:0];
        [self.notificationLock unlock];
        [self processNotification:notification];
        [self.notificationLock lock];
    };
 
    [self.notificationLock unlock];
}

NSMachPort协议方法主要是检查需要处理的任何通知消息并迭代处理(防止并发发送大量端口消息,导致消息丢失),处理完成后同步从消息队列中移除;

通知处理方法如下:

- (void)processNotification:(NSNotification *)notification {
    if ([NSThread currentThread] != notificationThread) {
        // Forward the notification to the correct thread.
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                components:nil
                from:nil
                reserved:0];
    }
    else {
        // Process the notification here;
    }
}

为区分NSMachPort协议方法内部调用及通知处理消息回调,需要通过判定当前处理线程来处理不同的通知消息处理方式;对于通知观察回调,将消息添加至消息队列并发送线程间通信mach消息;其实本方案的核心就是通过线程间异步通信NSMachPort来通知接收线程处理通知队列中的消息;

对于接收线程需要调用如下方法启动通知消息处理:

[self setupThreadingSupport];
[[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(processNotification:)
        name:@"NotificationName"//通知消息名称,可自定义
        object:nil];

官方也给出了此方案的问题及思考:

First, all threaded notifications processed by this object must pass through the same method (processNotification:). Second, each object must provide its own implementation and communication port. A better, but more complex, implementation would generalize the behavior into either a subclass of NSNotificationCenter or a separate class that would have one notification queue for each thread and be able to deliver notifications to multiple observer objects and methods

其中指出更好地方式是自己去子类化一个NSNotficationCenter(github上有人实现了此方案,可参考GYNotificationCenter)或者单独写一个类类处理这种转发。

原理解析

通知开源gnustep-base-1.25.0代码来分析通知的具体实现;

_GSIMapTable映射表数据结构图如下:

GSIMapTable

相关的数据结构如下:

typedef struct _GSIMapBucket GSIMapBucket_t;
typedef struct _GSIMapNode GSIMapNode_t;

typedef GSIMapBucket_t *GSIMapBucket;
typedef GSIMapNode_t *GSIMapNode;

typedef struct _GSIMapTable GSIMapTable_t;
typedef GSIMapTable_t *GSIMapTable;

struct  _GSIMapNode {
    GSIMapNode  nextInBucket;   /* Linked list of bucket.   */
    GSIMapKey   key;
#if GSI_MAP_HAS_VALUE
    GSIMapVal   value;
#endif
};

struct  _GSIMapBucket {
    uintptr_t   nodeCount;  /* Number of nodes in bucket.   */
    GSIMapNode  firstNode;  /* The linked list of nodes.    */
};

struct  _GSIMapTable {
  NSZone    *zone;
  uintptr_t nodeCount;  /* Number of used nodes in map. */
  uintptr_t bucketCount;    /* Number of buckets in map.    */
  GSIMapBucket  buckets;    /* Array of buckets.        */
  GSIMapNode    freeNodes;  /* List of unused nodes.    */
  uintptr_t chunkCount; /* Number of chunks in array.   */
  GSIMapNode    *nodeChunks;    /* Chunks of allocated memory.  */
  uintptr_t increment;
#ifdef  GSI_MAP_EXTRA
  GSI_MAP_EXTRA extra;
#endif
};

GSIMapTable映射表包含了指向GSIMapNode单链表节点的指针数组nodeChunks,通过buckets数组记录单链表节点指针数组的各个链表的节点数量及链表首部地址,其中bucketCountnodeCountchunkCount分别记录了node节点、节点单链表信息数组、节点单链表指针数组的数目;

具体的从映射表中添加/删除的代码如下:

GS_STATIC_INLINE GSIMapBucket
GSIMapPickBucket(unsigned hash, GSIMapBucket buckets, uintptr_t bucketCount)
{
    return buckets + hash % bucketCount;
}

GS_STATIC_INLINE GSIMapBucket
GSIMapBucketForKey(GSIMapTable map, GSIMapKey key)
{
    return GSIMapPickBucket(GSI_MAP_HASH(map, key),
                            map->buckets, map->bucketCount);
}

GS_STATIC_INLINE void
GSIMapLinkNodeIntoBucket(GSIMapBucket bucket, GSIMapNode node)
{
    node->nextInBucket = bucket->firstNode;
    bucket->firstNode = node;
}

GS_STATIC_INLINE void
GSIMapUnlinkNodeFromBucket(GSIMapBucket bucket, GSIMapNode node)
{
    if (node == bucket->firstNode)
    {
        bucket->firstNode = node->nextInBucket;
    }
    else
    {
        GSIMapNode  tmp = bucket->firstNode;
        
        while (tmp->nextInBucket != node)
        {
            tmp = tmp->nextInBucket;
        }
        tmp->nextInBucket = node->nextInBucket;
    }
    node->nextInBucket = 0;
}

其实就是一个hash表结构,既可以以数组的形式取到每个单链表首元素,也可以以链表的形式获取,通过数组能够方便取到每个单向链表,再利用链表结构增删。

通知全局对象表结构如下:

typedef struct NCTbl {
    Observation     *wildcard;  /* Get ALL messages*/
    GSIMapTable     nameless;   /* Get messages for any name.*/
    GSIMapTable     named;      /* Getting named messages only.*/
    unsigned        lockCount;  /* Count recursive operations.  */
    NSRecursiveLock *_lock;     /* Lock out other threads.  */
    Observation     *freeList;
    Observation     **chunks;
    unsigned        numChunks;
    GSIMapTable     cache[CACHESIZE];
    unsigned short  chunkIndex;
    unsigned short  cacheIndex;
} NCTable;

其中数据结构中重要的是两张GSIMapTable表:namednameless,及单链表wildcard

  • named,保存着传入通知名称的通知hash表;
  • nameless,保存没有传入通知名称的hash表;
  • wildcard,保存既没有通知名称又没有传入object的通知单链表;

保存含有通知名称的通知表named需要注册object对象,因此该表结构体通过传入的name作为key,其中value同时也为GSIMapTable表用于存储对应的object对象的observer对象;

对没有传入通知名称只传入object对象的通知表nameless而言,只需要保存objectobserver的对应关系,因此object作为keyobserver作为value

具体的添加观察者的核心函数(block形式只是该函数的包装)大致代码如下:

- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name
              object: (id)object
{
    Observation *list;
    Observation *o;
    GSIMapTable m;
    GSIMapNode  n;

    //入参检查异常处理
    ...
        //table加锁保持数据一致性
    lockNCTable(TABLE);
        //创建Observation对象包装相应的调用函数
    o = obsNew(TABLE, selector, observer);
        //处理存在通知名称的情况
    if (name)
    {
        //table表中获取相应name的节点
        n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
        if (n == 0)
        {
           //未找到相应的节点,则创建内部GSIMapTable表,以name作为key添加到talbe中
          m = mapNew(TABLE);
          name = [name copyWithZone: NSDefaultMallocZone()];
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          GS_CONSUMED(name)
        }
        else
        {
            //找到则直接获取相应的内部table
            m = (GSIMapTable)n->value.ptr;
        }

        //内部table表中获取相应object对象作为key的节点
        n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
        if (n == 0)
        {
            //不存在此节点,则直接添加observer对象到table中
            o->next = ENDOBS;//单链表observer末尾指向ENDOBS
            GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
        }
        else
        {
            //存在此节点,则获取并将obsever添加到单链表observer中
            list = (Observation*)n->value.ptr;
            o->next = list->next;
            list->next = o;
        }
    }
    //只有观察者对象情况
    else if (object)
    {
        //获取对应object的table
        n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
        if (n == 0)
        {
            //未找到对应object key的节点,则直接添加observergnustep-base-1.25.0
            o->next = ENDOBS;
            GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
        }
        else
        {
            //找到相应的节点则直接添加到链表中
            list = (Observation*)n->value.ptr;
            o->next = list->next;
            list->next = o;
        }
    }
    //处理即没有通知名称也没有观察者对象的情况
    else
    {
        //添加到单链表中
        o->next = WILDCARD;
        WILDCARD = o;
    }
        //解锁
    unlockNCTable(TABLE);
}

对于block形式代码如下:

- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];

    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}

- (id) initWithQueue: (NSOperationQueue *)queue 
               block: (GSNotificationBlock)block
{
    self = [super init];
    if (self == nil)
        return nil;

    ASSIGN(_queue, queue);
    _block = Block_copy(block);
    return self;
}

- (void) didReceiveNotification: (NSNotification *)notif
{
    if (_queue != nil)
    {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];

        [_queue addOperation: op];
    }
    else
    {
        CALL_BLOCK(_block, notif);
    }
}

对于block形式通过创建GSNotificationObserver对象,该对象会通过Block_copy拷贝block,并确定通知操作队列,通知的接收处理函数didReceiveNotification中是通过addOperation来实现指定操作队列处理,否则直接执行block

发送通知的核心函数大致逻辑如下:

- (void) _postAndRelease: (NSNotification*)notification
{
    //入参检查校验
    //创建存储所有匹配通知的数组GSIArray
    //加锁table避免数据一致性问题
    //获取所有WILDCARD中的通知并添加到数组中
    //查找NAMELESS表中指定对应观察者对象object的通知并添加到数组中
        //查找NAMED表中相应的通知并添加到数组中
    //解锁table
    //遍历整个数组并依次调用performSelector:withObject处理通知消息发送
    //解锁table并释放资源
}

上面发送的重点就是获取所有匹配的通知,并通过performSelector:withObject发送通知消息,因此通知发送和接收通知的线程是同一个线程(block形式通过操作队列来指定队列处理);

Reference

Notification Programming Topics

NotificationCenter

Foundation: NSNotificationCenter

Notification与多线程

NSDistributedNotificationCenter

深入理解iOS NSNotification

深入理解NSNotificationCenter

iOS通讯模式(KVO、Notification、Delegate、Block、Target-Action的区别)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342