編寫高質(zhì)量iOS與OSX代碼的52個有效方法-第二章:對象吃粒、消息、運行期

用OC等面向?qū)ο笳Z言編程時翅睛,對象(object)就是基本構(gòu)造單元(building block)声搁,開發(fā)者可以通過對象來存儲并傳遞數(shù)據(jù)黑竞。

在對象之間傳遞數(shù)據(jù)并執(zhí)行任務(wù)的過程就叫做消息傳遞(Messaging).

當(dāng)應(yīng)用程序運行起來之后捕发,為其提供相關(guān)支持的代碼叫做Objective-C運行期環(huán)境(runtime),它提供了一些使得對象之間能夠傳遞消息的重要函數(shù)很魂,并且包含創(chuàng)建類實例所用的全部邏輯扎酷。

一定要理解運行期中各個部分協(xié)同工作的原理。

6遏匆、“屬性”概念

屬性(property)OC的一項特性法挨,用于封裝對象中的數(shù)據(jù)。OC對象通常會把其所需要的數(shù)據(jù)保存為各種實例變量幅聘。

@property 語法凡纳。對象接口的定義中,可以使用屬性帝蒿,這是一種標(biāo)準(zhǔn)的寫法荐糜,能夠訪問封裝在對象中的數(shù)據(jù)。因此葛超,也可以把屬性當(dāng)成一種簡稱暴氏,意思是:編譯器會自動寫出一套存取方法,可以訪問給定類型中具有給定名稱的變量绣张。

要訪問屬性答渔,可以使用點語法。

如果使用了屬性侥涵,編譯器就會自動編寫訪問這些屬性所需的方法沼撕,此過程叫做自動合成(autosynthesis)宋雏。這個過程又編譯器在編譯期執(zhí)行,所以編輯器中看不到這些合成方法(synthesized method)的源代碼务豺。除了生成方法代碼之外好芭,編譯器還要自動向類中添加適當(dāng)類型的實例變臉個,并且在屬性名前加下劃線冲呢,以此作為實例變量的名字舍败。

也可以在實現(xiàn)文件中通過@synthesize語法來指定智力變量的名字

@implementation DogObject
@synthesize dogName = _dogName;
@end

若不想令編譯器自動合成取出方法,可以自己實現(xiàn)敬拓。如果只顯示一個存取方法邻薯,那么另外一個還是由編譯器來合成。

還有一種方法都能阻止編譯器自動合成存取方法乘凸,就是使用@dynamic關(guān)鍵字厕诡,他告訴編譯器:不要自動創(chuàng)建實現(xiàn)屬性所需要的實例變量,也不要為其創(chuàng)建存取方法营勤。并且在編譯訪問屬性代碼時灵嫌,即使編譯器發(fā)現(xiàn)沒有定義存取方法,也不會報錯葛作。

屬性特質(zhì)

使用屬性時寿羞,它的各種特質(zhì)(attribute)會影響編譯器所生成的存取方法。

1赂蠢、 原子性

默認(rèn)情況下绪穆,編譯器所合成的方法會通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備nonatomic則不使用同步鎖虱岂。

如果屬性不具備nonatomic特質(zhì)玖院,就是atomic的。

如果是自己定義存取方法第岖,就應(yīng)該遵從與屬性特質(zhì)相符的原子性难菌。

2、 讀/寫權(quán)限
  • readwrite(讀寫)蔑滓,擁有獲取方法(getter)和設(shè)置方法(setter)郊酒,若該屬性由@synthesize實現(xiàn),則編譯器會自動生成兩個方法烫饼。
  • readonly(只讀)猎塞,僅擁有獲取方法。只有當(dāng)該屬性由@synthesize實現(xiàn)時杠纵,編譯器才會為其合成獲取方法荠耽。可以用此特質(zhì)把某個屬性對外公開為只讀屬性比藻,然后再class-continuation分類中將其重新定義為讀寫屬性铝量。
3倘屹、內(nèi)存管理語義

屬性用于封裝數(shù)據(jù),而數(shù)據(jù)要有具體的所有權(quán)語義(concrete ownership semantic)慢叨。

  • assign 只會執(zhí)行針對純量類型(scalar type纽匙,如CFLoat或NSInteger)的簡單賦值操作。
  • strong 表明該屬性定義一種擁有關(guān)系(owning relationship)拍谐。為這種屬性設(shè)置新值時烛缔,設(shè)置方法會先保留新值,并釋放舊值轩拨,然后再將新值設(shè)置上去践瓷。
  • weak 表明該屬性定義一種非擁有關(guān)系(nonowning relationship)。為這種屬性設(shè)置新值時亡蓉,設(shè)置方法既不保留新值晕翠,也不釋放舊值。此特質(zhì)同assign類似砍濒,然而在屬性所指的對象遭到摧毀時淋肾,屬性值也會清空(nil out)。
  • unsafe_unretained 同assign相同爸邢。但是它是喲用于對象類型(object type)樊卓,表達(dá)一種非擁有關(guān)系(不保留,unretained)甲棍,當(dāng)目標(biāo)對象遭到摧毀時简识,屬性值不會自動清空(不安全赶掖,unsafe)感猛,這一點與weak有區(qū)別
  • copy 所屬關(guān)系與strong類似。然而設(shè)置方法并不保留新值奢赂,而是將其拷貝(copy)陪白。當(dāng)屬性值類型為NSString*時,經(jīng)常用此特質(zhì)來保護(hù)其封裝性膳灶,因為傳遞給設(shè)置方法的新值有可能指向一個NSMutableString類的實例咱士。若是不拷貝字符串,那么設(shè)置完屬性后轧钓,字符串的值就可能會在對象不知情的情況下遭人更改序厉。所以,要拷貝一份不可變的字符串毕箍,確保對象中的字符串不會無意間變動弛房。只要實現(xiàn)屬性所用的對象是可變的,就應(yīng)該在設(shè)置新屬性時拷貝一份而柑。
4文捶、方法名

指定存取方法的方法名

  • getter=<name> 指定獲取方法名
    @property (nonatomic,getter=isOn) BOOL on;

  • setter=<name> 指定設(shè)置方法名荷逞,少見。

通過以上特質(zhì)粹排,可以微調(diào)有編譯器所合成的存取方法种远。如果是自己來實現(xiàn)這些方法,要保證其具備相關(guān)屬性所聲明的特質(zhì)顽耳。

如果想在其他方法里設(shè)置屬性值坠敷,同樣要遵守屬性定義中所宣稱的語義。


  • 可以用@property語法來定義對象中所封裝的數(shù)據(jù)射富。
  • 通過特質(zhì)類指定存儲數(shù)據(jù)所需的正確語義
  • 在設(shè)置屬性所對應(yīng)的實例變量時常拓,一定要遵從該屬性所聲明的語義。
  • 開發(fā)iOS程序時應(yīng)該使用nonatomic屬性辉浦,因為atomic屬性會嚴(yán)重影響性能弄抬。

具備atomic特性的獲取方法會通過鎖定機(jī)制來確保其操作的原子性。在iOS中使用同步鎖的開銷較大宪郊,會帶來性能問題掂恕。一般情況下并不要求屬性必須是原子的。因為這并不能保證線程安全(Thread safety)弛槐,若要實現(xiàn)線程安全的操作懊亡,還需采用更為深層的鎖定機(jī)制才行。

7乎串、在對象內(nèi)部盡量直接訪問實例變量

- (NSString *)stringOfDogInfomation {
    return [NSString stringWithFormat:@"Dog: \n name:%@ \n age:%zd",_dogName,_dogAge];
    //直接訪問實例變量
}

- (NSString *)stringOfDogInfomation {
    return [NSString stringWithFormat:@"Dog: \n name:%@ \n age:%zd",self.dogName,self.dogAge];
    //通過屬性訪問變量
}

兩種方法的區(qū)別:

  • 不經(jīng)過OC的方法派發(fā)(method dispatch)步驟店枣,所以直接訪問實例變量的速度比較快。這種情況下叹誉,編譯器所生成的代碼會直接訪問保存對象實例變量的那塊內(nèi)存鸯两。
  • 直接訪問實例變量時,不會調(diào)用其設(shè)置方法长豁,繞過了為相關(guān)屬性所定義的內(nèi)存管理語義钧唐。
  • 直接訪問實例變量,不會觸發(fā)鍵值觀察(Key-Value Observing)通知匠襟。
  • 通過屬性訪問有助于排查與之相關(guān)的錯誤钝侠,因為可以個獲取方法和設(shè)置方法增加斷點。進(jìn)行調(diào)試酸舍。

合理的這種方案是帅韧,在寫入實例變量時,通過設(shè)置方法啃勉,在讀取實例變量時忽舟,直接訪問。此辦法技能提高讀取操作的速度,又能控制對屬性的寫入操作萧诫。

用這種方法有幾個問題:

  • 初始化方法中如何設(shè)置屬性值斥难。這種情況下,直接訪問實例變量帘饶,因為子類可能會覆寫設(shè)置方法哑诊。在某些情況下必須在初始化方法中調(diào)用設(shè)置方法:如果待初始化的實例變量聲明在超類中来农,而我們又無法在子類中直接訪問次實例標(biāo)量的話剔桨,就需要調(diào)用設(shè)置方法众眨。
  • 惰性初始化(lazy initialization齐饮,懶加載)。這種情況下医瘫,必須通過獲取方法訪問屬性铜邮,否則實例變量永遠(yuǎn)不會被初始化萨蚕。
- (NSString *)dogOwner {
    if (!_dogOwner) {
        _dogOwner = [NSString stringWithFormat:@"Smith"];
    }
    return _dogOwner;
}


  • 在對象內(nèi)部讀取數(shù)據(jù)是颗搂,應(yīng)該直接通過實例變量担猛,而寫入數(shù)據(jù)時,應(yīng)通過屬性來寫丢氢。
  • 在初始化方法及dealloc方法中傅联,總是應(yīng)該通過實例變量來讀寫數(shù)據(jù)。
  • 在使用惰性初始化數(shù)據(jù)的情況下疚察,要通過屬性來讀取數(shù)據(jù)蒸走。

8、理解“對象同等性”概念

==比較兩個指針本身貌嫡,而不是其所指的對象比驻。

根據(jù)等同性來比較對象,一般使用NSObject協(xié)議中聲明的isEqual:方法來判斷兩個對象的等同性岛抄。一般來說别惦,兩個不同類型的對象總是不相等的。某些類對象提供了特殊的等同性判定方法(equality-checking method)弦撩,如果已經(jīng)知道兩個受測對象都屬于同一個類步咪,可以使用這種方法。

NSString *stringA = @"string 123";
NSString *stringB = [NSString stringWithFormat:@"string %d",123];
BOOL equalA = stringA == stringB;
BOOL equalB = [stringA isEqual:stringB];
BOOL equalC = [stringA isEqualToString:stringB];
    
NSLog(@"value:%d %d %d",equalA,equalB,equalC);
// value:0 1 1
    

NSString實現(xiàn)了一個自己獨有的等同性判斷方法益楼,叫做isEqualToString:。傳遞給該方法的對象必須是NSString点晴,否則結(jié)果undefined感凤。調(diào)用該方法比isEqual:快,后者還要執(zhí)行額外的步驟粒督,因為它不知道受測對象的類型陪竿。

NSObject協(xié)議中有兩個用于判斷等同性的關(guān)鍵方法:

- (BOOL)isEqual:(id)object;
- (NSInteger)hash;

NSObject對兩個方法的默認(rèn)實現(xiàn)是:當(dāng)且僅當(dāng)去指針值完全相等時,這兩個對象才相等。

若想在自定義的對象中正確覆寫這些方法族跛,必須先理解其約定闰挡。如果isEqual:方法判定兩個對象相等,那么其hash方法也必須返回同一個值礁哄。如果兩個對象的hash方法返回同一個值长酗,那么isEqual:未必會認(rèn)為兩者相等。

假定實現(xiàn)一個自定義判斷方法桐绒。

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }
    if ([self class] != [object class]) {
        return NO ;
    }
    
    DogObject *oDog = (DogObject *)object;
    if (![_dogName isEqualToString:oDog.dogName]) {
        return NO ;
    }
    if (_dogAge != oDog.dogAge) {
        return NO ;
    }
    
    return YES;
}

下面是hash方法夺脾,規(guī)則:若兩對象相等,則hash碼相等茉继,但是兩個hash碼相等的對象未必相等咧叭。所以,hash方法與isEqual:關(guān)聯(lián)烁竭。假定hash算法為:

- (NSUInteger)hash {
    NSUInteger dognameHash = [_dogName hash];
    NSUInteger dogAgeHash = _dogAge;
    return dognameHash ^ dogAgeHash;
}

編寫hash方法時菲茬,應(yīng)當(dāng)用當(dāng)前的對象做實驗,一邊在減少碰撞頻度與降低運算復(fù)雜度之間取舍派撕。

特定類所具有的等同性判定方法

除了NSString生均,NSArray和NSDictionary也具有特殊的等同性判定方法。isEqualToArray:isEqualToDictionary:腥刹。如果比較的對象不是數(shù)組或字典马胧,就會拋出異常。

如果經(jīng)常需要判斷等同性衔峰,需要自己來創(chuàng)建等同性判斷方法佩脊,因為無需檢測參數(shù)類型,能大大提升檢測速度垫卤。自己編寫判定方法的另一個原因是威彰,代碼更易讀、更美觀穴肘。

在寫判定方法時歇盼,也應(yīng)一并覆寫isEqual:方法。

- (BOOL)isEqualToDog:(DogObject *)oDog{
    if (self == oDog) {
        return YES;
    }
    if (![_dogName isEqualToString:oDog.dogName]) {
        return NO ;
    }
    if (_dogAge != oDog.dogAge) {
        return NO ;
    }
    return YES;
}

- (BOOL)isEqual:(id)object {
    if ([self class] == [object class]) {
        return [self isEqual:(DogObject *)object];
    } else {
        return [super isEqual:object];
    }
}

等同性判定的執(zhí)行深度

確定等同性比較的因素评抚,是否需要在等同性判定方法中檢測全部字段豹缀,取決于受測對象。只有類的編寫者才可以確定兩個對象實例在何種情況下判定為相等慨代。


  • 若想檢測對象的等同性邢笙,提供isEqual:hash方法
  • 相同的對象必須具有相同的哈希碼,但兩個哈希碼相同的對象未必相同
  • 不要盲目地逐個檢測每條屬性侍匙,而是應(yīng)該依照具體要求來制定檢測方案
  • 編寫hash方法時氮惯,應(yīng)使用計算速度快而且哈希碼碰撞幾率低的算法。

9、以“類族模式”隱藏實現(xiàn)細(xì)節(jié)

類族(class cluster)是一種很有用的模式妇汗,可以隱藏抽象基類(abstract base class)背后的實現(xiàn)細(xì)節(jié)帘不。OC系統(tǒng)框架中普遍使用此模式。比如UIButton杨箭,類方法buttonWithType:所返回的對象寞焙,其類型取決于輸入的按鈕類型。然而不管是什么類型對象告唆,它們都繼承自同一個基類:UIButon棺弊。

創(chuàng)建類族

首先定義抽象基類,在從基類中集成實體子類(concrete subclass)擒悬。并通過類方法模她,通過不同類型,創(chuàng)建不同的實例對象懂牧。

typedef NS_ENUM(NSUInteger, ZYDEmployeeType) {
    ZYDEmployeeTypeDeveloper,
    ZYDEmployeeTypeDesigner, 
};

@interface ZYDEmployee : NSObject
@property (nonatomic,copy) NSString *name;

+ (ZYDEmployee *)employeeWithType:(ZYDEmployeeType )type;

- (void)doADaysWork;

@end


@implementation ZYDEmployee

+ (ZYDEmployee *)employeeWithType:(ZYDEmployeeType)type {
    switch (type) {
        case ZYDEmployeeTypeDeveloper:
            return [ZYDEmployeeDeveloper new];
            break;
        case ZYDEmployeeTypeDesigner:
            return [ZYDEmoloyeeDesigner new];
            break;
    }
}

- (void)doADaysWork {
    
}
@end

每個實體子類都是從基類繼承而來

@interface ZYDEmployeeDeveloper : ZYDEmployee

@end


@implementation ZYDEmployeeDeveloper

- (void)doADaysWork {
    NSLog(@"write code");
}

@end

OC中沒有辦法指明某個基類是抽象的侈净,所以一般是在文檔中寫明類的用法。這種情況下一般沒有名為init的成員方法僧凤,這暗示該類的實例不應(yīng)該由用戶直接創(chuàng)建畜侦。

如果對象所屬的類位與某個類族中,那么在查詢類型信息時就要當(dāng)心躯保⌒牛可能創(chuàng)建了某個類的實例,但是實際上創(chuàng)建的確實其子類的實例途事。

Cocoa里的類族

系統(tǒng)框架中有很多類族验懊,大部分的collection類都是類族。例如NSArray與NSMutableArray尸变。

若要判斷某對象是否位于類族中义图,不要直接檢測兩個類對象是否等同,而應(yīng)通過isKindOfClass:方法判斷召烂。


  • 類族模式可以把實現(xiàn)細(xì)節(jié)隱藏在一套簡單的公共接口后面
  • 從類族的公共抽象基類中集成子類時要當(dāng)心碱工,若有開發(fā)文檔,首先閱讀奏夫。

10怕篷、在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)

關(guān)聯(lián)對象

有時需要在對象中存放相關(guān)信息⊥盎祝可以從對象所屬的類中繼承一個子類匙头,然后改用子類對象。

但是并非所有情況都能這么做仔雷,有時候類的實例可能是由某種機(jī)制所創(chuàng)建的,無法令這種機(jī)制創(chuàng)建出自己縮寫的子類實例。(例如碟婆,如果要個NSArray添加一個屬性(不能繼承)电抚,分類只能添加方法不能添加屬性(分類可以添加屬性,同樣適用@property大師不會生成帶下劃線變量竖共,也不會生成getter和setter方法蝙叛,即,即使添加了屬性公给,也無法用釣點語法調(diào)用getter和setter方法借帘。))就可以使用OC的一項特性解決這個問題,就是關(guān)聯(lián)對象(Associated Object)淌铐。

可以給對象關(guān)聯(lián)其他對象肺然,這些對象通過“鍵”來區(qū)分。通過鍵來object綁定對象腿准,也可以通過鍵獲取object綁定的對象际起。

可以把對象相像成一個NSDictionary,鍵對應(yīng)key吐葱,關(guān)聯(lián)對象對應(yīng)value街望。存取關(guān)聯(lián)對象就相當(dāng)于在字典對象上調(diào)用[object setObject: forKey:];[object objectForKey:];。不同的是:

  • 設(shè)置關(guān)聯(lián)對象時用的鍵是不透明的指針(opaque pointer)弟跑。
  • 如果在兩個鍵上調(diào)用isEqual:方法的返回值是YES(key內(nèi)容相同灾前,不論指針),那么Dictionary就認(rèn)為二者相等孟辑;但設(shè)置關(guān)聯(lián)對象時哎甲,若想令兩個鍵匹配到同一個值,則二者必須是完全相同的指針才行扑浸。鑒于此烧给,設(shè)置關(guān)聯(lián)對象值時,通常使用靜態(tài)全局變量做鍵喝噪。

使用關(guān)聯(lián)對象引入頭文件#import <objc/runtime.h>

存儲對象值的時候础嫡,可以指明存儲策略,用以維護(hù)相應(yīng)的內(nèi)存管理語義酝惧。

  • 存儲策略

存儲策略由枚舉objc_AssociationPolicy定義榴鼎,與@property類似。

/* Associative References */

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,          // assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // nonatomic,retain
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   // nonatomic copy
    OBJC_ASSOCIATION_RETAIN = 01401,       // retain
    OBJC_ASSOCIATION_COPY = 01403          // copy
};


  • 關(guān)聯(lián)對象的管理方法
//以給定的鍵和策略為某對象設(shè)置關(guān)聯(lián)對象值晚唇。傳入nil可達(dá)到移除某個關(guān)聯(lián)對象的效果巫财。
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
   
// 根據(jù)給定的鍵從某對象中獲取相應(yīng)的關(guān)聯(lián)對象值。 
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    

// 移除指定對象的全部關(guān)聯(lián)對象
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)

示例

自定義DogObject類代表狗哩陕,又添加類別(category)田園犬特殊屬性DogObject+TianYuanDog平项。

#import "DogObject.h"

@interface DogObject (TianYuanDog)

// 為類別添加屬性赫舒,但是類別不會生成_tianYuanHome。
// 不會生成setter和getter方法闽瓢,使用這兩個方法就會崩潰
@property (nonatomic,copy) NSString *tianYuanHome;

@end

調(diào)用settergetter方法報錯

DogObject *littleDog = [[DogObject alloc] initWithDogName:@"John" age:12];   
ittleDog.tianYuanHome = @"The Great Wall";
NSLog(@"%@",littleDog.tianYuanHome);


-[DogObject setTianYuanHome:]: unrecognized selector sent to instance 0x604000269840

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DogObject setTianYuanHome:]: unrecognized selector sent to instance 0x604000269840'

在類別中通過關(guān)聯(lián)對象接癌,實現(xiàn)對應(yīng)setter和getter方法。

#import "DogObject+TianYuanDog.h"
#import <objc/runtime.h>

static char * const kDogObject_TianYuan = "kDogObject_TianYuan";

@implementation DogObject (TianYuanDog)

// 實現(xiàn)getter方法扣讼,獲取key對應(yīng)的對象
- (NSString *)tianYuanHome {
    return objc_getAssociatedObject(self, kDogObject_TianYuan);
}

// 實現(xiàn)setter方法缺猛,設(shè)置key對應(yīng)的對象
- (void)setTianYuanHome:(NSString *)tianYuanHome {
    objc_setAssociatedObject(self, kDogObject_TianYuan, tianYuanHome, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

The Great Wall 一切順利執(zhí)行,也實現(xiàn)了為類別添加屬性的目的椭符。


  • 可以通過關(guān)聯(lián)對象機(jī)制把這兩個對象連起來荔燎。
  • 定義關(guān)聯(lián)對象時可指定內(nèi)存管理語義,用以模仿定義屬性時所采用的“擁有關(guān)系”和“非擁有關(guān)系”
  • 只有在其他方法不可行時才應(yīng)選用關(guān)聯(lián)對象销钝,因為這種做法會引入難于查找的bug有咨。

11、理解objc_msgSend的作用

在對象上調(diào)用方法曙搬,用OC術(shù)語來說摔吏,叫做傳遞消息(pass a message)。消息有名稱(name)或選擇子(selector)纵装≌鹘玻可以接受參數(shù),而且可能還有返回值橡娄。

C語言函數(shù)調(diào)用方式

C語言使用靜態(tài)綁定(static building)诗箍,也就是說,在編譯期就能決定運行時所應(yīng)調(diào)用的函數(shù)挽唉。

void printHello() {
    printf("hello world \n");
}

void printGoodbye() {
    printf("Goodbye world~ \n");
}

void doTheThing(int type) {
    if (type == 0) {
        printHello();
    } else {
        printGoodbye();
    }
}

不考慮內(nèi)聯(lián)(inline)編譯器在編譯代碼的時候滤祖,就已經(jīng)知道函數(shù)printHelloprintGoodbye存在,并直接生成調(diào)用這些函數(shù)的指令瓶籽。而函數(shù)地址實際是硬編碼在指令之中匠童。

void printHello() {
    printf("hello world \n");
}

void printGoodbye() {
    printf("Goodbye world~ \n");
}

void doTheThing(int type) {
    void (*func)(void);
    if (type == 0) {
        func = printHello;
    } else {
        func = printGoodbye;
    }
    func();
}

如果是第二種方式,就要使用動態(tài)綁定(dynamic binding)塑顺。因為所要調(diào)用的函數(shù)之道運行期才能確定汤求。待調(diào)用的函數(shù)地址無法硬編碼在指令之中,而是要在運行期讀取出來严拒。

OC的消息傳遞

OC中,如果向某對象傳遞消息裤唠,就會使用動態(tài)綁定機(jī)制來決定需要調(diào)用的方法。

在底層种蘸,所有方法都是普通的C函數(shù)竞膳,然而對象收到消息之后,究竟該調(diào)用哪個方法則完全于運行期決定竣蹦,甚至可以在程序運行期改變顶猜,這些特性是的OC成為一門真正的動態(tài)語言痘括。

id returnValue = [littleDog changeDogAgeWithSpecialMethod:age];

littleDog是接收者滔吠,changeDogAgeWithSpecialMethod:叫做選擇子纲菌,選擇子和參數(shù)合起來稱為消息。編譯器看到此消息后疮绷,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語言函數(shù)調(diào)用翰舌,所調(diào)用的函數(shù)乃是消息傳遞機(jī)制中心的核心函數(shù),叫做objc_msgSend椅贱,原型如下:

void objc_msgSend(id self, SEL cmd,...)

這個參數(shù)個數(shù)可變的函數(shù),能接受兩個或兩個以上的參數(shù)庇麦,第一個參數(shù)表示接收者喜德,第二個表示選擇子,后續(xù)參數(shù)就是消息中的參數(shù)舍悯,順序不變。選擇子就是方法的名字萌衬。

編譯器會把剛剛的消息轉(zhuǎn)換為如下函數(shù):

id returnValue = objc_msgSend(littleDog, @selector(changeDogAgeWithSpecialMethod:),age);

objc_msgSend函數(shù)會根據(jù)接收者與選擇子的類型來調(diào)用適當(dāng)?shù)姆椒ā橥瓿纱瞬僮髌蛹瑁摲椒ㄐ枰诮邮照咚鶎俚念愔兴褜て浞椒斜?list of methods)馁蒂,如果能找到與選擇子相符的方法,就調(diào)至其實現(xiàn)代碼沫屡。若找不到,就沿著繼承體系繼續(xù)向上查找沮脖,等找到合適的方法之后再跳轉(zhuǎn)芯急。如果最終找不到相符的方法驶俊,就執(zhí)行消息轉(zhuǎn)發(fā)(message forwarding)。

objc_msgSend會將匹配結(jié)果緩存在快速映射表里(fast map)榕酒。每個類都有一塊緩存故俐,若是稍后還向該類發(fā)送與選擇子相同的消息想鹰,執(zhí)行速度就會提升药版。

邊界情況(edge case)需要OC中另一些函數(shù)來處理:

  • objc_msgSend_stret。如果待發(fā)送消息要返回結(jié)構(gòu)體槽片,可交由此函數(shù)處理。只有當(dāng)CPU的寄存器能夠容納消息返回類型時还栓,才能處理此消息。如果值無法容納與CPU寄存器中(比如返回的結(jié)構(gòu)體太大了)氓皱,那么就有另外一個函數(shù)執(zhí)行派發(fā)勃刨。此時那個函數(shù)會通過分配在棧上的某個變量來處理消息所返回的結(jié)構(gòu)體。
  • objc_msgSend_fpret身隐。如果消息返回浮點數(shù),交友此函數(shù)處理隙轻。在某些架構(gòu)的CPU中調(diào)用函數(shù)時垢揩,需要對浮點數(shù)寄存器做特殊處理,也就是說通常所用的objc_msgSend在這種情況下并不合適叁巨。這個合數(shù)是為了處理x86等架構(gòu)CPU中某些令人稍覺驚訝的奇怪問題。
  • objc_msgSendSuper蚀瘸。如果給超類發(fā)消息,例如[super message:par]贮勃,交由此函數(shù)處理。

objc_msgSend等函數(shù)一旦找到應(yīng)該調(diào)用的方法實現(xiàn)之后奏瞬,就會跳轉(zhuǎn)。之所以能這樣做丝格,因為OC對象的每個方法都可以視為簡單的C函數(shù)棵譬。

尾調(diào)用優(yōu)化(tail-call optimization)

如果某函數(shù)最后一項操作時調(diào)用另外一個函數(shù)预伺,就可以運用尾調(diào)用優(yōu)化技術(shù),編譯器會生成掉專職另一函數(shù)所需的指令碼酬诀,而且不會向調(diào)用堆棧中推入新的棧幀(frame stack)。只有當(dāng)某函數(shù)的最后一個操作僅僅是調(diào)用其他函數(shù)而不會將其返回值用作他用時父叙,才能執(zhí)行尾調(diào)用優(yōu)化肴裙。

這項優(yōu)化對objc_msgSend非常關(guān)鍵,不這么做蜻懦,那么每次調(diào)用OC方法之前,都需要為調(diào)用objc_msgSend函數(shù)準(zhǔn)備棧幀悠咱,在棧蹤跡(stack trace)中可以卡到這種棧幀征炼。此外,若不優(yōu)化谆奥,會過早地發(fā)生棧溢出(stack overflow)現(xiàn)象。


  • 消息由接收者空骚、選擇子及參數(shù)構(gòu)成,給某對象發(fā)送消息(invoke a message)也就是相當(dāng)于在該對象上調(diào)用方法(call a method)
  • 發(fā)給某對象的全部消息都要由動態(tài)消息派發(fā)系統(tǒng)(dynamic message dispatch system)來處理囤屹,該系統(tǒng)會查出對應(yīng)的方法,并執(zhí)行其代碼乡括。

12智厌、理解消息轉(zhuǎn)發(fā)機(jī)制

在編譯期向類發(fā)送了其無法解讀的消息并不會報錯,因為在運行期可以繼續(xù)向類中添加方法铣鹏,所以編譯器在編譯期時還無法確知類中到底會不會有某個方法實現(xiàn)。當(dāng)對象接收到無法解決的消息后葵第,就會啟動消息轉(zhuǎn)發(fā)(message forwarding)機(jī)制合溺,在此過程中告訴對象應(yīng)該如何處理未知消息。

如下面錯誤日志:

-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0xb000000000000022
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0xb000000000000022'
*** First throw call stack:

就是想某個對象發(fā)送過一條其無法解讀的消息棠赛,從而啟動了消息轉(zhuǎn)發(fā)機(jī)制,并將此消息轉(zhuǎn)發(fā)給NSObject的默認(rèn)實現(xiàn)鼎俘。

上面錯誤中__NSCFNumber是為了實現(xiàn)無縫橋接(toll-free bridging)而是用的內(nèi)部類(internal class)痰腮,配置NSNumber對象時,也會一并創(chuàng)建此對象膀值。

消息轉(zhuǎn)發(fā)兩個階段

  • 正序接收者,所屬的類歌逢,是否能動態(tài)添加方法翘狱,已處理當(dāng)前這個未知的選擇子(unknown selector),這叫做動態(tài)方法解析(dynamic method resolution)。
  • 第二階段赚导,完整的消息轉(zhuǎn)發(fā)機(jī)制(full forwarding mechanism)

如果運行期系統(tǒng)已經(jīng)把第一階段執(zhí)行完了赤惊,那么接收者自己就無法再以動態(tài)新增方法的手段來相應(yīng)包含該選擇子的消息了。此時未舟,運行期系統(tǒng)會請求接收者以看看有沒有其他對象能處理這條消息。如果則運行期系統(tǒng)會把消息轉(zhuǎn)給那個對象员串,于是消息轉(zhuǎn)發(fā)過程結(jié)束昼扛。若沒有備援的接收者(replacement receiver)則啟動完整的消息轉(zhuǎn)發(fā)機(jī)制寸齐,運行期會把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對象中访忿,再給接收者最后一次機(jī)會痴荐,令其設(shè)法解決當(dāng)前還未處理的消息。

動態(tài)方法解析

對象在收到無法解讀的消息后挣惰,首先將調(diào)用其所屬類的方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
該方法的參數(shù)就是未知的選擇子殴边,返回值為布爾類型。表示這個類是否能新增一個實例方法用以處理此選擇子竖幔。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機(jī)制之前是偷,奔雷有機(jī)會新增一個處理此選擇子的方法。

加入尚未實現(xiàn)的方法不是實例方法而是類方法蛋铆,那么運行期系統(tǒng)就會調(diào)用另外一個方法+ (BOOL)resolveClassMethod:(SEL)sel;.

使用這種方法的前提是:相關(guān)的實現(xiàn)代碼已經(jīng)寫好,只等著運行的時候動態(tài)插在類里就可以了刺啦。

此方案常用來實現(xiàn)@dynamic屬性。

首先將選擇子華為字符串蜕青,檢測其是否表示設(shè)置方法。若前綴為set慧脱,則表示設(shè)置方法蒙兰,否則就是獲取方法。不管哪種情況搜变,都會吧處理該選擇子的方法加到類里,所添加的方法是純C函數(shù)實現(xiàn)的挠他。C函數(shù)可能會用代碼來操作相關(guān)的數(shù)據(jù)結(jié)構(gòu),類之中的屬性數(shù)據(jù)就存放在那些數(shù)據(jù)結(jié)構(gòu)里面贸呢。

備援接收者

當(dāng)接收者還有第二次機(jī)會能處理未知的選擇子拢军,在這一步中運行期系統(tǒng)會問他:能不能吧這條消息轉(zhuǎn)給其他接收者來處理。對應(yīng)的處理方法
- (id)forwardingTargetForSelector:(SEL)aSelector;

方法參數(shù)代表未知選擇子固蛾,若當(dāng)前接收者能找到備援對象度陆,則將其返回,若找不到就返回nil懂傀。

通過此方案,可以用組合來模擬出多重繼承(multiple inheritance)的某些特性恃泪。

在一個對象內(nèi)部缚忧,可能還有一些列其他對象,該對象經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對象返回闪水,這樣蒙具,在外界卡奈朽肥,好像是該對象親自處理了這些消息。

這一步所轉(zhuǎn)發(fā)的消息篱昔,無法操作始腾。若是箱子啊發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過完整的消息轉(zhuǎn)發(fā)機(jī)制來做浪箭。

完整的消息轉(zhuǎn)發(fā)

創(chuàng)建NSInvocation對象,把尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中匹表。此對象包含選擇子宣鄙、目標(biāo)及參數(shù)。在觸發(fā)NSInvocation對象時冻晤,消息派發(fā)系統(tǒng)(message-dispatch system)把消息指派給目標(biāo)對象。

調(diào)用方法- (void)forwardInvocation:(NSInvocation *)anInvocation;轉(zhuǎn)發(fā)消息宣虾。

這個方法可以實現(xiàn)得很簡單:只需要改變調(diào)用目標(biāo)温数,使消息在新目標(biāo)上得以調(diào)用即可(與備援接收者實現(xiàn)的方法等效蜻势,很少人使用這么簡單的實現(xiàn)方式)。

比較有用的方式:在觸發(fā)消息前握玛,先以某種方式改變消息內(nèi)容,不如追加另外一個參數(shù)冕屯,或者改換選擇子拂苹,等等。

如果發(fā)現(xiàn)調(diào)用操作不應(yīng)該由本類處理浴韭,則需要調(diào)用超類的同名方法。這樣的話念颈,繼承體系中的每個類都有機(jī)會處理此調(diào)用請求,直至NSObject嗡靡。

消息轉(zhuǎn)發(fā)全流程

resolveInstanceMethod (返回NO)-> forwardingTargetForSelector (返回nil)—> forwardInvocation(無法處理) -> 消息未能處理窟感。

接收者在每一步中均有機(jī)會處理消息。步驟越往后肌括,處理消息的代價就越大。最好能在第一步就處理完黑滴,這樣的話紧索,運行期系統(tǒng)就可以將此方法緩存起來。如果這類的實例稍后還收到同名選擇子珠漂,那么根本無需啟動消息轉(zhuǎn)發(fā)流程。若想在第三步把消息轉(zhuǎn)給備援的接收者荞彼,那不如把轉(zhuǎn)發(fā)操作提前第二步待笑。因為第三步只是修改了調(diào)用目標(biāo),這項改動放在第二步執(zhí)行會更為簡單寞缝,不然的話仰泻,還得創(chuàng)建并處理完整的NSInvocation。


  • 若對象無法響應(yīng)某個選擇子集侯,則進(jìn)入消息轉(zhuǎn)發(fā)流程
  • 通過運行期的動態(tài)方法解析功能帜消,我們可以在需要用到某個方法時再將其加入類中趟据。
  • 對象可以把其無法解讀的某些選擇子轉(zhuǎn)交給其他對象來處理。
  • 經(jīng)過上述兩步之后粘衬,如果還沒有辦法處理選擇子咳促,就啟動完整的消息轉(zhuǎn)發(fā)機(jī)制。

13跪腹、用方法調(diào)配技術(shù)調(diào)試黑盒方法

OC對象接收消息后冲茸,究竟會調(diào)用何種方法需要在運行期才能解析出來。給定的選擇子名稱相對應(yīng)的方法可以再運行期改變轴术,這樣我們既不需要源代碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能盖袭。這樣一來彼宠,新功能將在本類的所有示例中生效,而不是僅限于覆寫了相關(guān)方法的那些子類實例凭峡。此方案經(jīng)常稱為方法調(diào)配(method swizzling)摧冀。

類的方法列表會把選擇子的名稱映射到相關(guān)的方法實現(xiàn)之上,使的動態(tài)消息派發(fā)系統(tǒng)能夠據(jù)此找到應(yīng)該調(diào)用的方法按价。這些方法均已函數(shù)指針的形式來表示笙瑟,這種指針叫做IMP。id (*IMP)(id,SLE,...)

OC運行期系統(tǒng)提供的幾個方法框产,能夠用來操作這張表”蓿可以新增選擇子,可以改變某選擇子所對應(yīng)的方法實現(xiàn)膊存,還可以交換兩個選擇子所映射到的指針忱叭。

交換方法實現(xiàn):

void method_exchangeImplementations(<#Method _Nonnull m1#>, <#Method _Nonnull m2#>)

此函數(shù)兩個參數(shù)表示待交換的兩份方法實現(xiàn),而方法實現(xiàn)可通過下面方法獲得:

Method class_getInstanceMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)

  • 交換方法實例
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
    
NSString *sstring = @"This is a Cat.";
NSLog(@"lowercaseString :%@",[sstring lowercaseString]);
NSLog(@"uppercaseString :%@",[sstring uppercaseString]);

// 打印結(jié)果
// lowercaseString : THIS IS A CAT.
// uppercaseString : this is a cat.

不過這種交換沒什么意義爵卒,因為兩種方法已經(jīng)實現(xiàn)得很好撵彻。

  • 為既有方法添加新功能

給NSString的lowercaseString方法添加一個日志功能。

#import <Foundation/Foundation.h>
@interface NSString (Http)

- (NSString *)zyd_myLowercaseString;
@end
#import "NSString+Http.h"
@implementation NSString (Http)
- (NSString *)zyd_myLowercaseString {
    NSString *lowercase = [self zyd_myLowercaseString];
    NSLog(@"%@ => %@",self,lowercase);
    return lowercase;
}

@end
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(zyd_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
    
NSString *sstring = @"This is a Cat.";
NSLog(@"lowercaseString : %@",[sstring lowercaseString]);

在執(zhí)行lowercaseString方法的時候轴合,就會打印一行記錄消息

This is a Cat. => this is a cat.

lowercaseString : this is a cat.

通過此方法拾弃,可以為那些完全不知道其具體實現(xiàn)的(completely opaque 完全不透明的)黑盒方法增加日志記錄功能,有助于程序調(diào)試奔坟。

此做法只在調(diào)試程序時游泳搭盾,很少有人在調(diào)試程序之外的場合用方法調(diào)配技術(shù)來永久改動某個類的功能。不能僅僅因為OC有這個特定就一定要用它鸯隅,若是濫用反而會另代碼變得不易讀懂且難于維護(hù)。


  • 在運行期炕舵,可以向類中新增或者替換選擇子所對應(yīng)的方法實現(xiàn)跟畅。
  • 使用另一份實現(xiàn)來替換原有的方法實現(xiàn),這道工序叫做方法調(diào)配徊件,經(jīng)常用此方法向原有實現(xiàn)中農(nóng)添加新功能。
  • 只要調(diào)試程序的時候才需要在運行期修改方法實現(xiàn)睹耐,這種做法不宜濫用。

14响委、理解類對象的用意

OC是一門極其動態(tài)的語言捎迫。對象類型并非在編譯期就綁定好了,而是要在運行期查找窄绒。而且還有個特殊的類型id,他能指代任意的OC對象類型蛔翅。

一般情況下位谋,要指明消息接收者的具體類型,如果向其發(fā)送了無法解讀的消息掏父,那么編譯器就產(chǎn)生警告信息赊淑。而類型為id的對象,編譯器假定它能響應(yīng)所有消息陶缺。

編譯器無法確定某類型對象到底能解讀多少種選擇子,因為運行期還可以向其中動態(tài)新增掺出。然而苫费,幾遍使用了動態(tài)新增技術(shù),編譯器也覺得應(yīng)該能在頭文件中找到方法原型的定義泥畅,據(jù)此可了解完整的方法簽名,并生成拍發(fā)消息所需的正確代碼位仁。

OC對象的本質(zhì)

OC對象實例是指向某塊內(nèi)存數(shù)據(jù)的指針方椎,所以在聲明變量時棠众,類型后面要跟一個*字符。

描述OC對象所用的數(shù)據(jù)結(jié)構(gòu)定義在運行期程序庫的頭文件里闸拿,id類型本身也定義在這里:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

沒個結(jié)構(gòu)體的首個成員是Class類的變量。該變量定義了對象所述的類揽趾,通常稱為is a指針苛骨。

在類繼承體系中查詢類型信息

可以用類型信息查詢方法來檢視類集成體系。isMemberOfClass:能夠判斷出對象是否為某個特定類的實例俐筋。isKindOfClass:能判斷出對象是否為某類或其派生類的實例严衬。

這樣的類型信息查詢方法使用isa指針獲取對象所屬的類,然后通過super_class指針在繼承體系中游走请琳。由于對象是動態(tài)的,所以cite習(xí)慣顯得極為重要抱怔。OC中嘀倒,必須查詢類型信息,方能完全了解對象的真實類型灌危。

在程序中不要直接比較對象所屬的類碳胳,明智的做法是調(diào)動類型查詢方法。

OC使用動態(tài)類型系統(tǒng)(dynamic typing)挨约,所以用于查詢對象所屬類的類型信息查詢功能非常有用。從collection中獲取對象時翁锡,通常會查詢類型信息,這些對象不是強(qiáng)類型的瘟判,從collection中取出來時角溃,其類型通常是id。如果想知道具體類型就可以使用類型信息查詢方法减细。

NSNumber *ageNumber = @2;
NSArray *array = @[ageNumber];

if ([array[0] isKindOfClass:[NSString class]]) {
    NSLog(@"string :%@",[array[0] lowercaseString]);
} else if ([array[0] isKindOfClass:[NSNumber class]]) {
    NSLog(@"number :%@",array[0]);
}

// 打印結(jié)果:
// number :2

  • 每個實例都以一個指向Class對象的指針匆瓜,用以表明其類型,而這些Class對象則構(gòu)成了類的集成體系邪财。
  • 如果對象類型無法在編譯期確定陕壹,那么久應(yīng)該使用類型信息查詢方法來探知。
  • 盡量使用類型信息查詢方法類確定對象類型树埠,而不要直接比較類對象糠馆,因為某些類對象可能實現(xiàn)了消息轉(zhuǎn)發(fā)功能。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怎憋,一起剝皮案震驚了整個濱河市又碌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绊袋,老刑警劉巖毕匀,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件癌别,死亡現(xiàn)場離奇詭異皂岔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)展姐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門躁垛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人圾笨,你說我怎么就攤上這事教馆。” “怎么了擂达?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵土铺,是天一觀的道長。 經(jīng)常有香客問我,道長悲敷,這世上最難降的妖魔是什么究恤? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮后德,結(jié)果婚禮上丁溅,老公的妹妹穿的比我還像新娘。我一直安慰自己探遵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布妓柜。 她就那樣靜靜地躺著箱季,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棍掐。 梳的紋絲不亂的頭發(fā)上藏雏,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機(jī)與錄音作煌,去河邊找鬼掘殴。 笑死,一個胖子當(dāng)著我的面吹牛粟誓,可吹牛的內(nèi)容都是我干的奏寨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鹰服,長吁一口氣:“原來是場噩夢啊……” “哼病瞳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悲酷,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤套菜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后设易,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逗柴,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年顿肺,在試婚紗的時候發(fā)現(xiàn)自己被綠了戏溺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡挟冠,死狀恐怖于购,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情知染,我是刑警寧澤肋僧,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響嫌吠,放射性物質(zhì)發(fā)生泄漏止潘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一辫诅、第九天 我趴在偏房一處隱蔽的房頂上張望凭戴。 院中可真熱鬧,春花似錦炕矮、人聲如沸么夫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽档痪。三九已至,卻和暖如春邢滑,著一層夾襖步出監(jiān)牢的瞬間腐螟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工困后, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留乐纸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓摇予,卻偏偏與公主長得像汽绢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子趾盐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容