關聯對象 AssociatedObject 完全解析

關注倉庫,及時獲得更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

我們在 iOS 開發中經常需要使用分類(Category),為已經存在的類添加屬性的需求,但是使用 @property 并不能在分類中正確創建實例變量和存取方法。

不過,通過 Objective-C 運行時中的關聯對象,也就是 Associated Object,我們可以實現上述需求。

寫在前面

這篇文章包含了兩方面的內容:

注:如果你剛剛入門 iOS 開發,筆者相信了解第一部分的內容會對你的日常開發中有所幫助,不過第二部分的內容可能有些難以理解。

如果你對關聯對象的使用非常熟悉,可以直接跳過第一部分的內容,從這里開始深入了解其底層實現。

關聯對象的應用

關于關聯對象的使用相信已經成為了一個老生常談的問題了,不過為了保證這篇文章的完整性,筆者還是會在這里為各位介紹這部分的內容的。

分類中的 @property

@property 可以說是一個 Objective-C 編程中的“宏”,它有元編程的思想。

@interface DKObject : NSObject

@property (nonatomic, strong) NSString *property;

@end

在使用上述代碼時會做三件事:

  • 生成實例變量 _property
  • 生成 getter 方法 - property
  • 生成 setter 方法 - setProperty:
@implementation DKObject {
    NSString *_property;
}

- (NSString *)property {
    return _property;
}

- (void)setProperty:(NSString *)property {
    _property = property;
}

@end

這些代碼都是編譯器為我們生成的,雖然你看不到它,但是它確實在這里,我們既然可以在類中使用 @property 生成一個屬性,那么為什么在分類中不可以呢?

我們來做一個小實驗:創建一個 DKObject 的分類 Category,并添加一個屬性 categoryProperty

@interface DKObject (Category)

@property (nonatomic, strong) NSString *categoryProperty;

@end

看起來還是很不錯的,不過 Build 一下這個 Demo,會發現有這么一個警告:

objc-ao-warning-category-property

在這里的警告告訴我們 categoryProperty 屬性的存取方法需要自己手動去實現,或者使用 @dynamic 在運行時實現這些方法。

換句話說,分類中的 @property 并沒有為我們生成實例變量以及存取方法,而需要我們手動實現。

使用關聯對象

Q:我們為什么要使用關聯對象?

A:因為在分類中 @property 并不會自動生成實例變量以及存取方法,所以一般使用關聯對象為已經存在的類添加『屬性』

上一小節的內容已經給了我們需要使用關聯對象的理由。在這里,我們會介紹 ObjC 運行時為我們提供的與關聯對象有關的 API,并在分類中實現一個偽屬性

#import "DKObject+Category.h"
#import <objc/runtime.h>

@implementation DKObject (Category)

- (NSString *)categoryProperty {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setCategoryProperty:(NSString *)categoryProperty {
    objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

這里的 _cmd 代指當前方法的選擇子,也就是 @selector(categoryProperty)

我們使用了兩個方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 來模擬『屬性』的存取方法,而使用關聯對象模擬實例變量。

在這里有必要解釋兩個問題:

  1. 為什么向方法中傳入 @selector(categoryProperty)
  2. OBJC_ASSOCIATION_RETAIN_NONATOMIC 是干什么的?

關于第一個問題,我們需要看一下這兩個方法的原型:

id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

@selector(categoryProperty) 也就是參數中的 key,其實可以使用靜態指針 static void * 類型的參數來代替,不過在這里,筆者強烈推薦使用 @selector(categoryProperty) 作為 key 傳入。因為這種方法省略了聲明參數的代碼,并且能很好地保證 key 的唯一性。

OBJC_ASSOCIATION_RETAIN_NONATOMIC 又是什么呢?如果我們使用 Command 加左鍵查看它的定義:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

從這里的注釋我們能看到很多東西,也就是說不同的 objc_AssociationPolicy 對應了不通的屬性修飾符:

objc_AssociationPolicy modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN atomic, strong
OBJC_ASSOCIATION_COPY atomic, copy

而我們在代碼中實現的屬性 categoryProperty 就相當于使用了 nonatomicstrong 修飾符。

關于屬性修飾符的區別,并不是這篇文章的主要內容,如果你需要了解它們的區別,Google 是一個很好的選擇。

到這里,我們已經完成了對關聯對象應用的介紹,再來回顧一下小節的內容。

@property` 其實有元編程的思想,它能夠為我們自動生成實例變量以及存取方法,而這三者構成了屬性這個類似于語法糖的概念,為我們提供了更便利的點語法來訪問屬性:

self.property <=> [self property]
self.property = value <=> [self setProperty:value]

在分類中,因為類的實例變量的布局已經固定,使用 @property 已經無法向固定的布局中添加新的實例變量(這樣做可能會覆蓋子類的實例變量),所以我們需要使用關聯對象以及兩個方法來模擬構成屬性的三個要素

如果你是一個 iOS 開發方面的新手,我相信這篇文章的前半部分對已經足夠使用了,不過,如果你還對關聯對象的實現非常感興趣,也可以嘗試閱讀下面的內容。

關聯對象的實現

探索關聯對象的實現一直是我想要做的一件事情,直到最近,我才有足夠的時間來完成這篇文章,希望能夠對各位讀者有所幫助。

這一部分會從三個 objc 運行時的方法為入口來對關聯對象的實現一探究竟,其中兩個方法是上一部分使用到的方法:

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

三個方法的作用分別是:

  • 以鍵值對形式添加關聯對象
  • 根據 key 獲取關聯對象
  • 移除所有關聯對象

而接下來的內容自然就是圍繞這三個方法進行的,我們會對它們的實現進行分析。

objc_setAssociatedObject

首先是 objc_setAssociatedObject 方法,這個方法的調用棧并不復雜:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 
└── void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy)
    └── void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)

調用棧中的 _object_set_associative_reference 方法實際完成了設置關聯對象的任務:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        ObjectAssociationMap *refs = i->second;
        ...
    }
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

在這里的實現省略了大多的實現代碼,而且忽略了很多邏輯上的順序,不過不要在意這里的代碼能否執行。

我們需要注意其中的幾個類和數據結構,因為在具體分析這個方法的實現之前,我們需要了解其中它們的作用:

  • AssociationsManager
  • AssociationsHashMap
  • ObjcAssociationMap
  • ObjcAssociation

AssociationsManager

AssociationsManager 在源代碼中的定義是這樣的:

class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

spinlock_t AssociationsManager::_lock;
AssociationsHashMap *AssociationsManager::_map = NULL;

它維護了 spinlock_tAssociationsHashMap 的單例,初始化它的時候會調用 lock.lock() 方法,在析構時會調用 lock.unlock(),而 associations 方法用于取得一個全局的 AssociationsHashMap 單例。

也就是說 AssociationsManager 通過持有一個自旋鎖 spinlock_t 保證對 AssociationsHashMap 的操作是線程安全的,即每次只會有一個線程對 AssociationsHashMap 進行操作

如何存儲 ObjcAssociation

ObjcAssociation 就是真正的關聯對象的類,上面的所有數據結構只是為了更好的存儲它。

首先,AssociationsHashMap 用與保存從對象的 disguised_ptr_tObjectAssociationMap 的映射:

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
    void *operator new(size_t n) { return ::malloc(n); }
    void operator delete(void *ptr) { ::free(ptr); }
};

ObjectAssociationMap 則保存了從 key 到關聯對象 ObjcAssociation 的映射,這個數據結構保存了當前對象對應的所有關聯對象

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
public:
   void *operator new(size_t n) { return ::malloc(n); }
   void operator delete(void *ptr) { ::free(ptr); }
};

最關鍵的 ObjcAssociation 包含了 policy 以及 value

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
public:
    ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
    ObjcAssociation() : _policy(0), _value(nil) {}

    uintptr_t policy() const { return _policy; }
    id value() const { return _value; }
    
    bool hasValue() { return _value != nil; }
};

舉一個簡單的例子來說明關聯對象在內存中以什么形式存儲的,以下面的代碼為例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSObject *obj = [NSObject new];
        objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }
    return 0;
}

這里的關聯對象 ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello") 在內存中是這么存儲的:

objc-ao-associateobjcect

接下來我們可以重新回到對 objc_setAssociatedObject 方法的分析了。

在這里會將方法的執行分為兩種情況:

  • new_value != nil 設置/更新關聯對象的值
  • new_value == nil 刪除一個關聯對象

new_value != nil

先來分析在 new_value != nil 的情況下,該方法的執行是什么樣的:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);

        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                old_association = j->second;
                j->second = ObjcAssociation(policy, new_value);
            } else {
                (*refs)[key] = ObjcAssociation(policy, new_value);
            }
        } else {
            ObjectAssociationMap *refs = new ObjectAssociationMap;
            associations[disguised_object] = refs;
            (*refs)[key] = ObjcAssociation(policy, new_value);
            object->setHasAssociatedObjects();
        }
    }
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
  1. 使用 old_association(0, nil) 創建一個臨時的 ObjcAssociation 對象(用于持有原有的關聯對象,方便在方法調用的最后釋放值)

  2. 調用 acquireValuenew_value 進行 retain 或者 copy

    static id acquireValue(id value, uintptr_t policy) {
        switch (policy & 0xFF) {
        case OBJC_ASSOCIATION_SETTER_RETAIN:
            return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
        case OBJC_ASSOCIATION_SETTER_COPY:
            return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
        }
        return value;
    }
    
  3. 初始化一個 AssociationsManager,并獲取唯一的保存關聯對象的哈希表 AssociationsHashMap

    AssociationsManager manager;
    AssociationsHashMap &associations(manager.associations());
    
  4. 先使用 DISGUISE(object) 作為 key 尋找對應的 ObjectAssociationMap

  5. 如果沒有找到,初始化一個 ObjectAssociationMap,再實例化 ObjcAssociation 對象添加到 Map 中,并調用 setHasAssociatedObjects 方法,表明當前對象含有關聯對象

    ObjectAssociationMap *refs = new ObjectAssociationMap;
    associations[disguised_object] = refs;
    (*refs)[key] = ObjcAssociation(policy, new_value);
    object->setHasAssociatedObjects();
    
  6. 如果找到了對應的 ObjectAssociationMap,就要看 key 是否存在了,由此來決定是更新原有的關聯對象,還是增加一個

    ObjectAssociationMap *refs = i->second;
    ObjectAssociationMap::iterator j = refs->find(key);
    if (j != refs->end()) {
        old_association = j->second;
        j->second = ObjcAssociation(policy, new_value);
    } else {
        (*refs)[key] = ObjcAssociation(policy, new_value);
    }
    
  7. 最后的最后,如果原來的關聯對象有值的話,會調用 ReleaseValue() 釋放關聯對象的值

    struct ReleaseValue {
        void operator() (ObjcAssociation &association) {
            releaseValue(association.value(), association.policy());
        }
    };
    
    static void releaseValue(id value, uintptr_t policy) {
        if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) {
            ((id(*)(id, SEL))objc_msgSend)(value, SEL_release);
        }
    }
    

到這里,該條件下的方法實現就結束了。

new_value == nil

如果 new_value == nil,就說明我們要刪除對應 key 的關聯對象,實現如下:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);

        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i !=  associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                old_association = j->second;
                refs->erase(j);
            }
        }
    }
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

這種情況下方法的實現與前面的唯一區別就是,我們會調用 erase 方法,擦除 ObjectAssociationMapkey 對應的節點。

setHasAssociatedObjects()

其實上面的兩種情況已經將 objc_setAssociatedObject 方法的實現分析得很透徹了,不過,這里還有一個小問題來等待我們解決,setHasAssociatedObjects() 方法的作用是什么?

inline void objc_object::setHasAssociatedObjects() {
    if (isTaggedPointer()) return;

 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if (!newisa.indexed) return;
    if (newisa.has_assoc) return;
    newisa.has_assoc = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}

它會將 isa 結構體中的標記位 has_assoc 標記為 true,也就是表示當前對象有關聯對象,在這里我還想祭出這張圖來介紹 isa 中的各個標記位都是干什么的。

objc-ao-isa-struct

如果想要了解關于 isa 的知識,可以閱讀從 NSObject 的初始化了解 isa

objc_getAssociatedObject

我們既然已經對 objc_setAssociatedObject 的實現已經比較熟悉了,相信對于 objc_getAssociatedObject 的理解也會更加容易。

方法的調用棧和 objc_setAssociatedObject 非常相似:

id objc_getAssociatedObject(id object, const void *key)
└── id objc_getAssociatedObject_non_gc(id object, const void *key);
    └── id _object_get_associative_reference(id object, void *key) 

_object_get_associative_reference 相比于前面方法的實現更加簡單。

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

代碼中尋找關聯對象的邏輯和 objc_setAssociatedObject 差不多:

  1. 獲取靜態變量 AssociationsHashMap

  2. DISGUISE(object) 為 key 查找 AssociationsHashMap

  3. void *key 為 key 查找 ObjcAssociation

  4. 根據 policy 調用相應的方法

    if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
    
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    
  5. 返回關聯對象 ObjcAssociation 的值

objc_removeAssociatedObjects

關于最后的 objc_removeAssociatedObjects 方法,其實現也相對簡單,這是方法的調用棧:

void objc_removeAssociatedObjects(id object)
└── void _object_remove_assocations(id object)

這是簡化版本的 objc_removeAssociatedObjects 方法實現:

void objc_removeAssociatedObjects(id object) {
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

為了加速移除對象的關聯對象的速度,我們會通過標記位 has_assoc 來避免不必要的方法調用,在確認了對象和關聯對象的存在之后,才會調用 _object_remove_assocations 方法移除對象上所有的關聯對象:

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            delete refs;
            associations.erase(i);
        }
    }
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

方法會將對象包含的所有關聯對象加入到一個 vector 中,然后對所有的 ObjcAssociation 對象調用 ReleaseValue() 方法,釋放不再被需要的值。

小結

關于應用

本來在這個系列的文章中并不會涉及關聯對象這個話題,不過,有人問過我這么一個問題:在分類中到底能否實現屬性?其實在回答這個問題之前,首先要知道到底屬性是什么?而屬性的概念決定了這個問題的答案。

  • 如果你把屬性理解為通過方法訪問的實例變量,我相信這個問題的答案是不能,因為分類不能為類增加額外的實例變量
  • 不過如果屬性只是一個存取方法以及存儲值的容器的集合,那么分類是可以實現屬性的。

分類中對屬性的實現其實只是實現了一個看起來像屬性的接口而已

關于實現

關聯對象又是如何實現并且管理的呢:

  • 關聯對象其實就是 ObjcAssociation 對象
  • 關聯對象由 AssociationsManager 管理并在 AssociationsHashMap 存儲
  • 對象的指針以及其對應 ObjectAssociationMap 以鍵值對的形式存儲在 AssociationsHashMap
  • ObjectAssociationMap 則是用于存儲關聯對象的數據結構
  • 每一個對象都有一個標記位 has_assoc 指示對象是否含有關聯對象

關注倉庫,及時獲得更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

原文鏈接: http://draveness.me/ao

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,746評論 0 9
  • 對于從事 iOS 開發人員來說,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,730評論 7 64
  • 臨江仙·滾滾長江東逝水 【作者】楊慎【朝代】明 《廿一史彈詞》第三段說秦漢開場詞 滾滾長江東逝水,浪花淘盡英雄。 ...
    熠心勵行閱讀 390評論 0 0
  • 26. 膽結石,為什么不建議大家去開刀去取結石呢?因為,取了結石,還長,膽結石的溫床還在,取了一個石頭是又長一個石...
    海風居士6645閱讀 370評論 0 0
  • 2017年的法定假日過完了,生活又回到了正軌,每天上班,下班帶孩子奔赴課外班,還有兼顧孩子的體育鍛煉,興趣班學的東...
    馬夢兒閱讀 206評論 0 0