runtime 和 runloop

runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, 所以大家有必要進行研究驴一,有能力的童鞋可以和下面作者一樣, 親歷實踐一下灶壶。

在簡書里發(fā)現了兩篇非常好的文章介紹 runtime和runloop的肝断,在這里合二為一了, 把原版作者的東西拿了過來驰凛, 為了尊重作者胸懈,在這里注明一下 @sam_lau 是runtime的作者, @tripleCC是runloop的作者

RunTime

Objective-C是基于C語言加入了面向對象特性消息轉發(fā)機制的動態(tài)語言恰响,這意味著它不僅需要一個編譯器趣钱,還需要Runtime系統來動態(tài)創(chuàng)建類和對象,進行消息發(fā)送和轉發(fā)胚宦。下面通過分析Apple開源的Runtime代碼(我使用的版本是objc4-646.tar)來深入理解Objective-C的Runtime機制首有。

Runtime數據結構

在Objective-C中,使用[receiver message]語法并不會馬上執(zhí)行receiver對象的message方法的代碼枢劝,而是向receiver發(fā)送一條message消息井联,這條消息可能由receiver來處理,也可能由轉發(fā)給其他對象來處理呈野,也有可能假裝沒有接收到這條消息而沒有處理低矮。其實[receiver message]被編譯器轉化為:

idobjc_msgSend (idself, SEL op, ... );

下面從兩個數據結構id和SEL來逐步分析和理解Runtime有哪些重要的數據結構印叁。

SEL

SEL是函數objc_msgSend第二個參數的數據類型被冒,表示方法選擇器,按下面路徑打開objc.h文件

SEL Data Structure

查看到SEL數據結構如下:

typedef struct objc_selector *SEL;

其實它就是映射到方法的C字符串轮蜕,你可以通過Objc編譯器命令@selector()或者Runtime系統的sel_registerName函數來獲取一個SEL類型的方法選擇器昨悼。

如果你知道selector對應的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)方法將SEL轉化為字符串跃洛,再用NSLog打印率触。

id

接下來看objc_msgSend第一個參數的數據類型id,id是通用類型指針汇竭,能夠表示任何對象葱蝗。按下面路徑打開objc.h文件

id Data Structure.png

查看到id數據結構如下:

/// Represents an instance of a class.structobjc_object { ?

Class isa ?OBJC_ISA_AVAILABILITY;

};

/// A pointer to an instance of a class.typedefstructobjc_object *id;

id其實就是一個指向objc_object結構體指針,它包含一個Class isa成員细燎,根據isa指針就可以順藤摸瓜找到對象所屬的類两曼。

注意:根據Apple的官方文檔Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技術實現的玻驻,isa指針在運行時被修改悼凑,指向一個中間類而不是真正的類。所以,你不應該使用isa指針來確定類的關系户辫,而是使用class方法來確定實例對象的類渐夸。

Class

isa指針的數據類型是Class,Class表示對象所屬的類渔欢,按下面路徑打開objc.h文件

Class Data Structure

/// An opaque type that represents an Objective-C class.typedefstructobjc_class *Class;

可以查看到Class其實就是一個objc_class結構體指針墓塌,但這個頭文件找不到它的定義,需要在runtime.h才能找到objc_class結構體的定義奥额。

按下面路徑打開runtime.h文件

objc_class Data Structure

查看到objc_class結構體定義如下:

struct objc_class { ? ?

Class isa ?OBJC_ISA_AVAILABILITY;

#if !__OBJC2__Class super_class ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

const char *name ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

long version ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

long info ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

long instance_size ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

struct objc_ivar_list *ivars ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

struct objc_method_list *methodLists ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

struct objc_cache *cache ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

struct objc_protocol_list *protocols ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#endif

}

OBJC2_UNAVAILABLE;

/* Use `Class` instead of `struct objc_class *` */

注意:OBJC2_UNAVAILABLE是一個Apple對Objc系統運行版本進行約束的宏定義桃纯,主要為了兼容非Objective-C 2.0的遺留版本,但我們仍能從中獲取一些有用信息披坏。

讓我們分析一些重要的成員變量表示什么意思和對應使用哪些數據結構态坦。

isa表示一個Class對象的Class,也就是Meta Class棒拂。在面向對象設計中伞梯,一切都是對象,Class在設計中本身也是一個對象帚屉。我們會在objc-runtime-new.h文件找到證據谜诫,發(fā)現objc_class有以下定義:

structobjc_class : objc_object {

// Class ISA;

Class superclass;

cache_tcache;

// formerly cache pointer and vtableclass_data_bits_tbits;

// class_rw_t * plus custom rr/alloc flags......

}

由此可見,結構體objc_class也是繼承objc_object攻旦,說明Class在設計中本身也是一個對象喻旷。

其實Meta Class也是一個Class,那么它也跟其他Class一樣有自己的isa和super_class指針牢屋,關系如下:

Class isa and superclass relationship from Google

上圖實線是super_class指針且预,虛線是isa指針。有幾個關鍵點需要解釋以下:

Root class (class)其實就是NSObject烙无,NSObject是沒有超類的锋谐,所以Root class(class)的superclass指向nil。

每個Class都有一個isa指針指向唯一的Meta class

Root class(meta)的superclass指向Root class(class)截酷,也就是NSObject涮拗,形成一個回路。

每個Meta class的isa指針都指向Root class (meta)迂苛。

super_class表示實例對象對應的父類

name表示類名

ivars表示多個成員變量三热,它指向objc_ivar_list結構體。在runtime.h可以看到它的定義:

struct objc_ivar_list {

int ivar_count ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#ifdef __LP64__intspace ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#endif

/* variable length structure */

structobjc_ivar ivar_list[1] ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

}

objc_ivar_list其實就是一個鏈表三幻,存儲多個objc_ivar就漾,而objc_ivar結構體存儲類的單個成員變量信息。

methodLists表示方法列表赌髓,它指向objc_method_list結構體的二級指針从藤,可以動態(tài)修改*methodLists的值來添加成員方法催跪,也是Category實現原理,同樣也解釋Category不能添加實例變量的原因夷野。在runtime.h可以看到它的定義:

struct objc_method_list {

struct objc_method_list *obsolete ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

int method_count ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#ifdef __LP64__intspace ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#endif

/* variable length structure */

struct objc_method method_list[1] ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;}

同理懊蒸,objc_method_list也是一個鏈表,存儲多個objc_method悯搔,而objc_method結構體存儲類的某個方法的信息骑丸。

cache用來緩存經常訪問的方法,它指向objc_cache結構體妒貌,后面會重點講到通危。

protocols表示類遵循哪些協議

Method

Method表示類中的某個方法,在runtime.h文件中找到它的定義:

/// An opaque type that represents a method in a class definition.

typedef struct objc_method *Method;

structobjc_method { ? ?

SEL method_name ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

char *method_types ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE; ?

IMP method_imp ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

}

其實Method就是一個指向objc_method結構體指針灌曙,它存儲了方法名(method_name)菊碟、方法類型(method_types)和方法實現(method_imp)等信息。而method_imp的數據類型是IMP在刺,它是一個函數指針逆害,后面會重點提及空免。

Ivar

Ivar表示類中的實例變量吓歇,在runtime.h文件中找到它的定義:

/// An opaque type that represents an instance variable.

typedef struct objc_ivar *Ivar;

struct objc_ivar {

char *ivar_name ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

char *ivar_type ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

int ivar_offset ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#ifdef __LP64__intspace ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

#endif

}

Ivar其實就是一個指向objc_ivar結構體指針,它包含了變量名(ivar_name)偶妖、變量類型(ivar_type)等信息颖杏。

IMP

在上面講Method時就說過纯陨,IMP本質上就是一個函數指針,指向方法的實現留储,在objc.h找到它的定義:

/// A pointer to the function of a method implementation.

#if!OBJC_OLD_DISPATCH_PROTOTYPES

typede fvoid(*IMP)(void/* id, SEL, ... */);

#else typedefid(*IMP)(id, SEL, ...);

#endif

當你向某個對象發(fā)送一條信息翼抠,可以由這個函數指針來指定方法的實現,它最終就會執(zhí)行那段代碼欲鹏,這樣可以繞開消息傳遞階段而去執(zhí)行另一個方法實現机久。

Cache

顧名思義臭墨,Cache主要用來緩存赔嚎,那它緩存什么呢?我們先在runtime.h文件看看它的定義:

typedef struct objc_cache *Cache ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

struct objc_cache {

unsigned int mask

/* total = mask + 1 */

OBJC2_UNAVAILABLE;

unsigned int occupied ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE; ?

Method buckets[1] ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

OBJC2_UNAVAILABLE;

};

Cache其實就是一個存儲Method的鏈表胧弛,主要是為了優(yōu)化方法調用的性能尤误。當對象receiver調用方法message時,首先根據對象receiver的isa指針查找到它對應的類结缚,然后在類的methodLists中搜索方法损晤,如果沒有找到,就使用super_class指針到父類中的methodLists查找红竭,一旦找到就調用方法尤勋。如果沒有找到喘落,有可能消息轉發(fā),也可能忽略它最冰。但這樣查找方式效率太低瘦棋,因為往往一個類大概只有20%的方法經常被調用,占總調用次數的80%暖哨。所以使用Cache來緩存經常調用的方法赌朋,當調用方法時,優(yōu)先在Cache查找篇裁,如果沒有找到沛慢,再到methodLists查找。

消息發(fā)送

前面從objc_msgSend作為入口达布,逐步深入分析Runtime的數據結構团甲,了解每個數據結構的作用和它們之間關系后,我們正式轉入消息發(fā)送這個正題黍聂。

objc_msgSend函數

在前面已經提過伐庭,當某個對象使用語法[receiver message]來調用某個方法時,其實[receiver message]被編譯器轉化為:

id objc_msgSend (id self, SEL op, ... );

現在讓我們看一下objc_msgSend它具體是如何發(fā)送消息:

首先根據receiver對象的isa指針獲取它對應的class

優(yōu)先在class的cache查找message方法分冈,如果找不到圾另,再到methodLists查找

如果沒有在class找到,再到super_class查找

一旦找到message這個方法雕沉,就執(zhí)行它實現的IMP集乔。

Objc Message.gif

self與super

為了讓大家更好地理解self和super,借用sunnyxx博客的ios程序員6級考試一道題目:下面的代碼分別輸出什么坡椒?

@implementationSon:Father

- (id)init{self= [superinit];

if(self) ? ?{

NSLog(@"%@",NSStringFromClass([selfclass]));

NSLog(@"%@",NSStringFromClass([superclass])); ? ?

}

return self;

}

@end

self表示當前這個類的對象扰路,而super是一個編譯器標示符,和self指向同一個消息接受者倔叼。在本例中汗唱,無論是[self class]還是[super class],接受消息者都是Son對象丈攒,但super與self不同的是哩罪,self調用class方法時,是在子類Son中查找方法巡验,而super調用class方法時际插,是在父類Father中查找方法。

當調用[self class]方法時显设,會轉化為objc_msgSend函數框弛,這個函數定義如下:

id objc_msgSend(id self, SEL op, ...)

這時會從當前Son類的方法列表中查找,如果沒有捕捂,就到Father類查找瑟枫,還是沒有斗搞,最后在NSObject類查找到。我們可以從NSObject.mm文件中看到- (Class)class的實現:

- (Class)class{

return object_getClass(self);

}

所以NSLog(@"%@", NSStringFromClass([self class]));會輸出Son慷妙。

當調用[super class]方法時榜旦,會轉化為objc_msgSendSuper,這個函數定義如下:

id objc_msgSendSuper(structobjc_super *super, SEL op, ...)

objc_msgSendSuper函數第一個參數super的數據類型是一個指向objc_super的結構體景殷,從message.h文件中查看它的定義:

///Specifies the superclass of an instance.structobjc_super {

///Specifies an instance of a class.__unsafe_unretained id receiver;

///Specifies the particular superclass of the instance to message.

#if!defined(__cplusplus) ?&& ?!__OBJC2__/* For compatibility with old objc-runtime.h header */__unsafe_unretained Classclass;

#else__unsafe_unretained Class super_class;

#endif/* super_class is the first class to search */

};

#endif

結構體包含兩個成員溅呢,第一個是receiver,表示某個類的實例猿挚。第二個是super_class表示當前類的父類咐旧。

這時首先會構造出objc_super結構體,這個結構體第一個成員是self绩蜻,第二個成員是(id)class_getSuperclass(objc_getClass("Son"))铣墨,實際上該函數會輸出Father。然后在Father類查找class方法办绝,查找不到伊约,最后在NSObject查到。此時孕蝉,內部使用objc_msgSend(objc_super->receiver, @selector(class))去調用屡律,與[self class]調用相同,所以結果還是Son降淮。

隱藏參數self和_cmd

當[receiver message]調用方法時超埋,系統會在運行時偷偷地動態(tài)傳入兩個隱藏參數self和_cmd,之所以稱它們?yōu)殡[藏參數佳鳖,是因為在源代碼中沒有聲明和定義這兩個參數霍殴。至于對于self的描述,上面已經解釋非常清楚了系吩,下面我們重點講解_cmd来庭。

_cmd表示當前調用方法,其實它就是一個方法選擇器SEL穿挨。一般用于判斷方法名或在Associated Objects中唯一標識鍵名月弛,后面在Associated Objects會講到。

方法解析與消息轉發(fā)

[receiver message]調用方法時絮蒿,如果在message方法在receiver對象的類繼承體系中沒有找到方法尊搬,那怎么辦?一般情況下土涝,程序在運行時就會Crash掉,拋出unrecognized selector sent to …類似這樣的異常信息幌墓。但在拋出異常之前但壮,還有三次機會按以下順序讓你拯救程序冀泻。

Method Resolution

Fast Forwarding

Normal Forwarding

Message Forward from Google

Method Resolution

首先Objective-C在運行時調用+ resolveInstanceMethod:或+ resolveClassMethod:方法,讓你添加方法的實現蜡饵。如果你添加方法并返回YES弹渔,那系統在運行時就會重新啟動一次消息發(fā)送的過程。

舉一個簡單例子溯祸,定義一個類Message肢专,它主要定義一個方法sendMessage,下面就是它的設計與實現:

@interfaceMessage:NSObject- (void)sendMessage:(NSString*)word;@end

@implementationMessage

- (void)sendMessage:(NSString*)word{

NSLog(@"normal way : send message = %@", word);

}

@end

如果我在viewDidLoad方法中創(chuàng)建Message對象并調用sendMessage方法:

- (void)viewDidLoad { ? ?

[super viewDidLoad]; ? ?

Message *message = [Messagenew]; ? ?

[message sendMessage:@"Sam Lau"];

}

控制臺會打印以下信息:

normal way :sendmessage = Sam Lau

但現在我將原來sendMessage方法實現給注釋掉焦辅,覆蓋resolveInstanceMethod方法:

#pragma mark - Method Resolution/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation

+ (BOOL)resolveInstanceMethod:(SEL)sel{

if(sel ==@selector(sendMessage:)) {

? ? ? class_addMethod([selfclass], sel, imp_implementationWithBlock(^(idself,NSString*word{

NSLog(@"method resolution way : send message = %@", word); ? ? ? ?

}),"v@*"); ?

}

returnYES;

}

控制臺就會打印以下信息:

method resolution way :sendmessage = Sam Lau

注意到上面代碼有這樣一個字符串"v@*博杖,它表示方法的參數和返回值,詳情請參考Type Encodings

如果resolveInstanceMethod方法返回NO筷登,運行時就跳轉到下一步:消息轉發(fā)(Message Forwarding)

Fast Forwarding

如果目標對象實現- forwardingTargetForSelector:方法剃根,系統就會在運行時調用這個方法,只要這個方法返回的不是nil或self前方,也會重啟消息發(fā)送的過程狈醉,把這消息轉發(fā)給其他對象來處理。否則惠险,就會繼續(xù)Normal Fowarding苗傅。

繼續(xù)上面Message類的例子,將sendMessage和resolveInstanceMethod方法注釋掉班巩,然后添加forwardingTargetForSelector方法的實現:

#pragma mark - Fast Forwarding

- (id)forwardingTargetForSelector:(SEL)aSelector{

if(aSelector ==@selector(sendMessage:)) {

return[MessageForwarding new]; ? ?

}returnnil;

}

此時還缺一個轉發(fā)消息的類MessageForwarding金吗,這個類的設計與實現如下:

@interfaceMessageForwarding:NSObject

- (void)sendMessage:(NSString*)word;

@end

@implementationMessageForwarding

- (void)sendMessage:(NSString*)word{

NSLog(@"fast forwarding way : send message = %@", word);

}

@end

此時,控制臺會打印以下信息:

fast forwarding way :sendmessage = Sam Lau

這里叫Fast趣竣,是因為這一步不會創(chuàng)建NSInvocation對象摇庙,但Normal Forwarding會創(chuàng)建它,所以相對于更快點遥缕。

Normal Forwarding

如果沒有使用Fast Forwarding來消息轉發(fā)卫袒,最后只有使用Normal Forwarding來進行消息轉發(fā)。它首先調用methodSignatureForSelector:方法來獲取函數的參數和返回值单匣,如果返回為nil夕凝,程序會Crash掉,并拋出unrecognized selector sent to instance異常信息户秤。如果返回一個函數簽名码秉,系統就會創(chuàng)建一個NSInvocation對象并調用-forwardInvocation:方法。

繼續(xù)前面的例子鸡号,將forwardingTargetForSelector方法注釋掉转砖,添加methodSignatureForSelector和forwardInvocation方法的實現:

#pragma mark - Normal Forwarding

-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector

{

NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];

if(!methodSignature){ ? ? ? ?

methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];

} ? ?

return methodSignature;

}

-(void)forwardInvocation:(NSInvocation*)anInvocation

{

MessageForwarding *messageForwarding = [MessageForwarding new];

if([messageForwarding respondsToSelector:anInvocation.selector]){ ? ?

? [anInvocation invokeWithTarget:messageForwarding];

}

}

關于這個例子的示例代碼請到github下載。

三種方法的選擇

Runtime提供三種方式來將原來的方法實現代替掉,那該怎樣選擇它們呢府蔗?

Method Resolution:由于Method Resolution不能像消息轉發(fā)那樣可以交給其他對象來處理晋控,所以只適用于在原來的類中代替掉。

Fast Forwarding:它可以將消息處理轉發(fā)給其他對象姓赤,使用范圍更廣赡译,不只是限于原來的對象。

Normal Forwarding:它跟Fast Forwarding一樣可以消息轉發(fā)不铆,但它能通過NSInvocation對象獲取更多消息發(fā)送的信息蝌焚,例如:target、selector誓斥、arguments和返回值等信息只洒。

Associated Objects

Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class. (Programming with Objective-C)

當想使用Category對已存在的類進行擴展時,一般只能添加實例方法或類方法岖食,而不適合添加額外的屬性红碑。雖然可以在Category頭文件中聲明property屬性,但在實現文件中編譯器是無法synthesize任何實例變量和屬性訪問方法泡垃。這時需要自定義屬性訪問方法并且使用Associated Objects來給已存在的類Category添加自定義的屬性析珊。Associated Objects提供三個API來向對象添加、獲取和刪除關聯值:

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 )

其中objc_AssociationPolicy是個枚舉類型蔑穴,它可以指定Objc內存管理的引用計數機制忠寻。

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. */

};

下面有個關于NSObject+AssociatedObjectCategory添加屬性associatedObject的示例代碼:

NSObject+AssociatedObject.h

@interface NSObject(AssociatedObject)

@property(strong,nonatomic)id associatedObject;

@end

NSObject+AssociatedObject.m

@implementationNSObject(AssociatedObject)

- (void)setAssociatedObject:(id)associatedObject{ ? ?objc_setAssociatedObject(self,@selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (id)associatedObject{

return objc_getAssociatedObject(self, _cmd);

}

@end

Associated Objects的key要求是唯一并且是常量,而SEL是滿足這個要求的存和,所以上面的采用隱藏參數_cmd作為key奕剃。

Method Swizzling

Method Swizzling就是在運行時將一個方法的實現代替為另一個方法的實現。如果能夠利用好這個技巧捐腿,可以寫出簡潔纵朋、有效且維護性更好的代碼∏研洌可以參考兩篇關于Method Swizzling技巧的文章:

nshipster Method Swizzling

Method Swizzling 和 AOP 實踐

Aspect-Oriented Programming(AOP)

類似記錄日志操软、身份驗證、緩存等事務非诚芟椋瑣碎聂薪,與業(yè)務邏輯無關,很多地方都有蝗羊,又很難抽象出一個模塊藏澳,這種程序設計問題,業(yè)界給它們起了一個名字叫橫向關注點(Cross-cutting concern)耀找,AOP作用就是分離橫向關注點(Cross-cutting concern)來提高模塊復用性翔悠,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗證、緩存)而無需修改代碼凉驻。

危險性

Method Swizzling就像一把瑞士小刀腻要,如果使用得當复罐,它會有效地解決問題涝登。但使用不當,將帶來很多麻煩效诅。在stackoverflow上有人已經提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?胀滚,它的危險性主要體現以下幾個方面:

Method swizzling is not atomic

Changes behavior of un-owned code

Possible naming conflicts

Swizzling changes the method's arguments

The order of swizzles matters

Difficult to understand (looks recursive)

Difficult to debug

總結

雖然在平時項目不是經常用到Objective-C的Runtime特性,但當你閱讀一些iOS開源項目時乱投,你就會發(fā)現很多時候都會用到咽笼。所以深入理解Objective-C的Runtime數據結構、消息轉發(fā)機制有助于你更容易地閱讀和學習開源項目戚炫。

擴展閱讀

玉令天下博客的Objective-C Runtime

顧鵬博客的Objective-C Runtime

Associated Objects

Method Swizzling

Method Swizzling 和 AOP 實踐

Objective-C Runtime Reference

What are the Dangers of Method Swizzling in Objective C?

ios程序員6級考試(答案和解釋)

RunLoop

深入理解RunLoop這篇文章寫的很好剑刑!

簡介

RunLoop顧名思義,就是運行循環(huán)的意思双肤。

基本作用:

保持程序的持續(xù)運行

處理App中的各類事件(觸摸事件施掏、定時器事件、Selector事件)

節(jié)省CPU資源茅糜,提高程序性能:沒有事件時就進行睡眠狀態(tài)

內部實現:

do-while循環(huán)七芭,在這個循環(huán)內部不斷地處理各種任務(Source\Timeer\Observer)

注意點:

一個線程對應一個RunLoop(采用字典存儲,線程號為key蔑赘,RunLoop為value)

主線程的RunLoop默認已經啟動狸驳,子線程的RunLoop需要手動啟動

RunLoop只能選擇一個Mode啟動,如果當前Mode沒有任何Source缩赛、Timer耙箍、Observer,那么就不會進入RunLoop

RunLoop的主要函數調用順序為:CFRunLoopRun->CFRunLoopRunSpecific->__CFRunLoopRun

注意特殊情況酥馍,事實上辩昆,在只有Observer的情況,也不一定會進入循環(huán)物喷,因為源代碼里面只會顯式地檢測兩個東西:Source和Timer(這兩個是主動向RunLoop發(fā)送消息的)卤材;Observer是被動接收消息的

RunLoop在第一次獲取時創(chuàng)建,在線程結束時銷毀

RunLoop循環(huán)示意圖:(針對上面的__CFRunLoopRun函數峦失,Mode已經判斷非空前提)

圖1

RunLoop循環(huán)示意圖

圖2

接觸過微處理器編程的基本上都知道扇丛,在編寫微處理器程序時,我通常會在main函數中寫一個無限循環(huán)尉辑,然后在這個循環(huán)里面對外部事件進行監(jiān)聽帆精,比如外部中斷,一些傳感器的數據等,在沒有外部中斷時卓练,就讓CPU進入低功耗模式隘蝎。如果接收到了外部中斷,就恢復到正常模式襟企,對中斷進行處理嘱么。

while(1) {// 根據中斷決定是否切換模式執(zhí)行任務}// 或者for(;;) {}

RunLoop和這個相似,也是在線程的main中增加了一個循環(huán):

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

BOOLrunning =YES;

do{// 執(zhí)行各種任務顽悼,處理各種事件// ......

}

while(running);

return0;

}

所以線程在這種情況下曼振,便不會退出。

關于MainRunLoop:

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

@autoreleasepool{

return UIApplicationMain(argc, argv,nil,NSStringFromClass([AppDelegate class])); ? ?

}

}

在viewDidLoad中設置斷電蔚龙,然后得到以下主線程棧信息:

可以看到冰评,UIApplicationMain內部啟動了一個和主線程相關聯的RunLoop(_CFRunLoopRun)。在這里也可以推斷木羹,程序進入UIApplicationMain就不會退出了甲雅。我稍微對主函數進行了如下修改,并在return語句上打印了斷點:

運行程序后坑填,并不會在斷點處停下抛人,證實了上面的推斷。

上面涉及了一個_CFRunLoopRun函數穷遂,接下來說明下iOS中訪問和使用RunLoop的API:

Foundation--NSRunLoop

Core Foundation--CFRunLoopRef(開源)

因為后者是開源的函匕,且前者是在后者上針對OC的封裝,所以一般是對CFRunLoopRef進行研究蚪黑。

兩套API對應獲取RunLoop對象的方式:

Foundation

[NSRunLoop currentRunLoop]; // 當前runloop

[NSRunLoop mainRunLoop];// 主線程runloop

Core Foundation

CFRunLoopGetCurrent();// 當前runloop

CFRunLoopGetMain();// 主線程runloop

值得注意的是盅惜,獲取當前RunLoop都是進行懶加載的,也就是調用時自動創(chuàng)建線程對應的RunLoop忌穿。

RunLoop相關類:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

類之間的關系

以上圖片說明了各個類之間的關系抒寂。

CFRunLoopModeRef說明:

代表RunLoop的運行模式,一個RunLoop可以包含多個Mode掠剑,每個Mode可以包含多個Source屈芜、Timer、Observer

每次RunLoop啟動時朴译,只能指定其中一個Mode井佑,這個Mode就變成了CurrentMode

當啟動RunLoop時,如果所在Mode中沒有Source眠寿、Timer躬翁、Observer,那么將不會進入RunLoop盯拱,會直接結束

如果要切換Mode盒发,只能退出Loop例嘱,再重新制定一個Mode進入

系統默認注冊了5個Mode:

NSDefaultRunLoopMode:App的默認Mode,通常主線程是在這個Mode下運行

UITrackingRunLoopMode:界面跟蹤 Mode宁舰,用于 ScrollView 追蹤觸摸滑動拼卵,保證界面滑動時不受其他 Mode 影響

UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用

GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode蛮艰,通常用不到

NSRunLoopCommonModes: 這是一個占位用的Mode腋腮,不是一種真正的Mode

關于NSRunLoopCommonModes:

一個Mode可以將自己標記為“Common”屬性,每當 RunLoop 的內容發(fā)生變化時印荔,RunLoop會對標記有“Common”屬性的Mode進行相適應的切換低葫,并同步Source/Observer/Timer

在主線程中详羡,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode這兩個Mode都是被默認標記為“Common”屬性的仍律,從輸出的主線程RunLoop可以查看。

- 結合上面兩點实柠,當使用NSRunLoopCommonModes占位時水泉,會表明使用標記為“Common”屬性的Mode,在一定層度上窒盐,可以說是“擁有了兩個Mode”草则,可以在這兩個Mode中的其中任意一個進行工作

CFRunLoopTimerRef說明:

CFRunLoopTimerRef是基于時間的觸發(fā)器,它包含了一個時間長度和一個回調函數指針蟹漓。當它加入到RunLoop時炕横,RunLoop會注冊對應的時間點,當時間點到時葡粒,RunLoop會被喚醒以執(zhí)行那個回調

CFRunLoopTimerRef大部分指的是NSTimer份殿,它受RunLoop的Mode影響

由于NSTimer在RunLoop中處理,所以受其影響較大嗽交,有時可能會不準確卿嘲。還有一種定時器是GCD定時器,它并不在RunLoop中夫壁,所以不受其影響拾枣,也就比較精確

接下來說明各種Mode下,NSTimer的工作情況:

情況1

在對創(chuàng)建的定時器進行模式修改前盒让,scheduledTimerWithTimeInterval創(chuàng)建的定時器只在NSDefaultRunLoopMode模式下可以正常運行梅肤,當滾動UIScroolView時,模式轉換成UITrackingRunLoopMode邑茄,定時器就失效了姨蝴。

修改成NSRunLoopCommonModes后,定時器在兩個模式下都可以正常運行

// 創(chuàng)建的定時器默認添加到當前的RunLoop中(沒有就創(chuàng)建)撩扒,而且是NSDefaultRunLoopMode模式NSTimer*timer = [NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];

// 可以通過以下方法對模型進行修改

[[NSRunLoopmainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

情況2

timerWithTimeInterval創(chuàng)建的定時器并沒有手動添加進RunLoop似扔,所以需要手動進行添加吨些。當添加為以下模式時,定時器只在UITrackingRunLoopMode模式下進行工作炒辉,也就是滑動UIScrollView時就會工作豪墅,停止滑動時就不工作

如果把UITrackingRunLoopMode換成NSDefaultRunLoopMode,那么效果就和情況1沒修改Mode前的效果一樣

NSTimer*timer = [NSTimertimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];

// 在UITrackingRunLoopMode模式下定時器才會運行

[[NSRunLoopmainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

CFRunLoopSourceRef說明:

Source分類

按官方文檔

Port-Based Sources

Custom Input Sources

Cocoa Perform Selector Sources

按照函數調用棧

Source0:非基于Port的

Source0本身不能主動觸發(fā)事件黔寇,只包含了一個回調函數指針

Source1:基于Port的偶器,通過內核和其他線程通信,接收缝裤、分發(fā)系統事件

包含了mach_port和一個回調函數指針屏轰,接收到相關消息后,會分發(fā)給Source0進行處理

CFRunLoopObserverRef說明:

CFRunLoopObserverRef是觀察者憋飞,能夠監(jiān)聽RunLoop的狀態(tài)改變

能夠監(jiān)聽的狀態(tài)

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity){ ? ? ? ?

kCFRunLoopEntry = (1UL <<0),

// 進入RunLoopkCFRunLoopBeforeTimers = (1UL <<1),

//即將處理timerkCFRunLoopBeforeSources = (1UL <<2),

//即將處理SourceskCFRunLoopBeforeWaiting = (1UL <<5),

//即將進入休眠kCFRunLoopAfterWaiting = (1UL <<6),

//即將喚醒kCFRunLoopExit = (1UL <<7),

//即將退出RunLoopkCFRunLoopAllActivities =0x0FFFFFFFU//所有活動

};

添加監(jiān)聽者步驟

// 創(chuàng)建監(jiān)聽著

CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {

NSLog(@"%ld", activity); ? ?

});

// ? ?[[NSRunLoop currentRunLoop] getCFRunLoop]

// 向當前runloop添加監(jiān)聽者

CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 釋放內存CFRelease(observer);

CF的內存管理(Core Foundation):

1.凡是帶有Create霎苗、Copy、Retain等字眼的函數榛做,創(chuàng)建出來的對象唁盏,都需要在最后做一次release

比如CFRunLoopObserverCreate

2.release函數:CFRelease(對象);

自動釋放池釋放的時間和RunLoop的關系:

注意,這里的自動釋放池指的是主線程的自動釋放池检眯,我們看不見它的創(chuàng)建和銷毀厘擂。自己手動創(chuàng)建@autoreleasepool {}是根據代碼塊來的,出了這個代碼塊就釋放了锰瘸。

App啟動后刽严,蘋果在主線程 RunLoop 里注冊了兩個 Observer咸这,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()迎献。

第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)喉祭,其回調內會調用 _objc_autoreleasePoolPush()創(chuàng)建自動釋放池滔悉。其 order 是-2147483647醋安,優(yōu)先級最高谴轮,保證創(chuàng)建釋放池發(fā)生在其他所有回調之前池户。

第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池片吊;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池佩谣。這個 Observer 的 order 是 2147483647把还,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調之后茸俭。

在主線程執(zhí)行的代碼吊履,通常是寫在諸如事件回調、Timer回調內的调鬓。這些回調會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著艇炎,所以不會出現內存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了腾窝。

在自己創(chuàng)建線程時缀踪,需要手動創(chuàng)建自動釋放池AutoreleasePool

綜合上面居砖,可以得到以下結論:

@autoreleasepool {}內部實現

有以下代碼:

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

@autoreleasepool{ ? ?

}

return0;

}

查看編譯轉換后的代碼:

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

/* @autoreleasepool */{

__AtAutoreleasePool __autoreleasepool; ? ?

}

return0;

}

__AtAutoreleasePool是什么呢?找到其定義:

struct__AtAutoreleasePool {

__AtAutoreleasePool() {

atautoreleasepoolobj = objc_autoreleasePoolPush();

} ?~__AtAutoreleasePool() {

objc_autoreleasePoolPop(atautoreleasepoolobj);

}

void* atautoreleasepoolobj;

};

可以看到__AtAutoreleasePool是一個類:

其構造函數使用objc_autoreleasePoolPush創(chuàng)建了一個線程池驴娃,并保存給成員變量atautoreleasepool obj奏候。

其析構函數使用objc_autoreleasePoolPop銷毀了線程池

結合以上信息,main函數里面的__autoreleasepool是一個局部變量唇敞。當其創(chuàng)建時蔗草,會調用構造函數創(chuàng)建線程池,出了{}代碼塊時疆柔,局部變量被銷毀咒精,調用其析構函數銷毀線程池。

RunLoop實際應用

常駐線程

當創(chuàng)建一個線程旷档,并且希望它一直存在時模叙,就需要使用到RunLoop,否則線程一執(zhí)行完任務就會停止彬犯。

要向線程存在向楼,需要有強指針引用他,其他的代碼如下:

// 屬性

@property(strong,nonatomic)NSThread *thread;

// 創(chuàng)建線程

_thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(test) object:nil];

[_thread start];

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{

// 點擊時使線程_thread執(zhí)行test方法

[self performSelector:@selector(test) onThread:_thread withObject:nilwaitUntilDone:NO];

}

- (void)test{

NSLog(@"__test__");

}

就單單以上代碼谐区,是不起效果的,因為線程沒有RunLoop逻卖,執(zhí)行完test后就停止了宋列,無法再讓其執(zhí)行任務(強制start會崩潰)。

通過在子線程中給RunLoop添加監(jiān)聽者评也,可以了解下performSelector:onThread:內部做的事情:

調用performSelector:onThread: 時炼杖,實際上它會創(chuàng)建一個Source0加到對應線程的RunLoop里去,所以盗迟,如果對應的線程沒有RunLoop坤邪,這個方法就會失效

// 這句在主線程中調用// _thread就是下面的線程

[selfperformSelector:@selector(run) onThread:_thread withObject:nilwaitUntilDone:NO];

performSelecter:afterDelay:也是一樣的內部操作方法,只是創(chuàng)建的Timer添加到當前線程的RunLoop中了

// 創(chuàng)建RunLoop即將喚醒監(jiān)聽者

CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {

/ /打印喚醒前的

RunLoopNSLog(@"%ld--%@", activity, [NSRunLoopcurrentRunLoop]); ?

});

// 向當前runloop添加監(jiān)聽者

CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 釋放內存CFRelease(observer); ?

[self performSelector:@selector(setView:) withObject:nilafterDelay:2.0];

// 使model不為空

[[NSRunLoop currentRunLoop] addPort:[NSPortport] forMode:NSDefault RunLoopMode]; ?

[[NSRunLoop currentRunLoop] run];

綜合上面的解釋罚缕,可以知道performSelector:onThread:沒有起作用艇纺,是因為_thread線程內部沒有RunLoop,所以需要在線程內部創(chuàng)建RunLoop邮弹。

創(chuàng)建RunLoop并使對應線程成為常駐線程的常見方式有2:

方式1

向創(chuàng)建的RunLoop添加NSPort(Sources)黔衡,讓Mode不為空,RunLoop能進入循環(huán)不會退出

[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

[[NSRunLoop currentRunLoop]run];

方式2

讓RunLoop一直嘗試運行腌乡,判斷Mode是否為空盟劫,不是為空就進入RunLoop循環(huán)

while(1) { ?

[[NSRunLoopcurrentRunLoop] run];

}

AFNetWorking就使用到了常駐線程:

創(chuàng)建常駐線程

+ (void)networkRequestThreadEntryPoint:(id)__unused object {

@autoreleasepool{ ? ? ? ?

[[NSThread currentThread] setName:@"AFNetworking"];

// 創(chuàng)建RunLoop并向Mode添加NSMachPort,使RunLoop不會退出

NSRunLoop*runLoop = [NSRunLoopcurrentRunLoop]; ? ? ?

[runLoop addPort:[NSMachPortport] forMode:NSDefaultRunLoopMode]; ? ? ? ?

[runLoop run]; ? ?

}

}

+ (NSThread*)networkRequestThread {

static NSThread*_networkRequestThread =nil;

static dispatch_once_toncePredicate;dispatch_once(&oncePredicate, ^{ ? ? ? ?

_networkRequestThread = [[NSThreadalloc] initWithTarget:selfselector:@selector(networkRequestThreadEntryPoint:) object:nil]; ? ? ? ?[_networkRequestThread start]; ? ?

});

return_networkRequestThread;

}

使用常駐線程

- (void)start { ? ?

[self.locklock];

if([selfisCancelled]) { ? ? ? ?

[selfperformSelector:@selector(cancelConnection) onThread:[[selfclass] networkRequestThread] withObject:nilwaitUntilDone:NOmodes:[self.runLoopModesallObjects]]; ? ?

}else if([selfisReady]) {

self.state= AFOperationExecutingState; ? ? ? ?

[self performSelector:@selector(operationDidStart) onThread:[[selfclass] networkRequestThread] withObject:nilwaitUntilDone:NOmodes:[self.runLoopModesallObjects]]; ?

} ?

[self.lockunlock];

}

給子線程開啟定時器

_thread = [[NSThread alloc] initWithTarget:selfselector:@selector(test) object:nil];

[_thread start];

// 子線程添加定時器

- (void)subTimer{

// 默認創(chuàng)建RunLoop并向其model添加timer与纽,所以后續(xù)只需要讓RunLoop run起來即可

[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];

// 貌似source1不為空侣签,source0就不為空// ?

[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoopcurrentRunLoop] run];

}

讓某些事件(行為塘装、任務)在特定模式下執(zhí)行

比如圖片的設置,在UIScrollView滾動的情況下影所,我不希望設置圖片氢哮,等停止?jié)L動了再設置圖片,可以用以下代碼:

// 圖片只在NSDefaultRunLoopMode模式下會進行設置顯示

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20150712_39"] afterDelay:2.0inModes:@[NSDefault RunLoopMode]];

先設置任務在NSDefaultRunLoopMode模式在執(zhí)行型檀,這樣冗尤,在滾動使RunLoop進入UITrackingRunLoopMode時,就不會進行圖片的設置了胀溺。

控制定時器在特定模式下執(zhí)行

上文的《CFRunLoopTimerRef說明:》中已經指出

添加Observer監(jiān)聽RunLoop的狀態(tài)

監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)

具體步驟在《CFRunLoopObserverRef說明:》中已寫明

GCD定時器

注意:

dispatch_source_t是個類裂七,這點比較特殊

// ? ?dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());

dispatch_source_ttimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0, dispatch_get_global_queue(0,0)); ?

dispatch_source_set_timer(timer, DISPATCH_TIME_NOW,1.0* NSEC_PER_SEC,0* NSEC_PER_SEC); ? ?dispatch_source_set_event_handler(timer, ^{ ? ? ? ?

NSLog(@"__"); ? ? ? ?

NSLog(@"%@", [NSThread currentThread]);

staticNSInteger count =0;

if(count++ ==3) {

// 為什么dispatch_cancel不能用_timer?/

// Controlling expression type '__strong dispatch_source_t' (aka 'NSObject *__strong') not compatible with any generic association type

// 類型錯誤,可能dispatch_cancel是宏定義仓坞,需要的就是方法調用背零,而不是變量// ? ? ? ? ? ?dispatch_cancel(self.timer);dispatch_source_cancel(_timer); ? ? ? ?

} ? ?

});

// 定時器默認是停止的,需要手動恢復

dispatch_resume(timer);

// 需要一個強引用保證timer不被釋放

_timer = timer;

最后一點需要說明的是无埃,SDWebImage框架的下載圖片業(yè)務中也使用到了RunLoop徙瓶,老確保圖片下載成功后才關閉任務子線程。

原文鏈接:http://www.reibang.com/p/ebc6e20b84cf

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末嫉称,一起剝皮案震驚了整個濱河市侦镇,隨后出現的幾起案子,更是在濱河造成了極大的恐慌织阅,老刑警劉巖壳繁,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異荔棉,居然都是意外死亡闹炉,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門润樱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渣触,“玉大人,你說我怎么就攤上這事壹若⌒嶙辏” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵舌稀,是天一觀的道長啊犬。 經常有香客問我,道長壁查,這世上最難降的妖魔是什么觉至? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮睡腿,結果婚禮上语御,老公的妹妹穿的比我還像新娘峻贮。我一直安慰自己,他們只是感情好应闯,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布纤控。 她就那樣靜靜地躺著,像睡著了一般碉纺。 火紅的嫁衣襯著肌膚如雪船万。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天骨田,我揣著相機與錄音耿导,去河邊找鬼。 笑死态贤,一個胖子當著我的面吹牛舱呻,可吹牛的內容都是我干的。 我是一名探鬼主播悠汽,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼箱吕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了柿冲?” 一聲冷哼從身側響起茬高,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎姻采,沒想到半個月后雅采,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡慨亲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了宝鼓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刑棵。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖愚铡,靈堂內的尸體忽然破棺而出蛉签,到底是詐尸還是另有隱情,我是刑警寧澤沥寥,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布碍舍,位于F島的核電站,受9級特大地震影響邑雅,放射性物質發(fā)生泄漏片橡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一淮野、第九天 我趴在偏房一處隱蔽的房頂上張望捧书。 院中可真熱鬧吹泡,春花似錦、人聲如沸经瓷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舆吮。三九已至揭朝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間色冀,已是汗流浹背潭袱。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呐伞,地道東北人敌卓。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像伶氢,于是被迫代替她去往敵國和親趟径。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容

  • runtime 和 runloop 作為一個程序員進階是必須的癣防,也是非常重要的蜗巧, 在面試過程中是經常會被問到的, ...
    SOI閱讀 21,780評論 3 63
  • Runtime和Runloop的區(qū)別 一.RunLoop機制: Runloop是事件接收和分發(fā)機制的一個實現蕾盯。 R...
    codeshow閱讀 705評論 0 0
  • 轉至元數據結尾創(chuàng)建: 董瀟偉幕屹,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,682評論 0 9
  • 對于從事 iOS 開發(fā)人員來說,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,697評論 7 64
  • 還記髫年時 青梅尚酸卻偷嘗 竹馬竿短仍成雙 再憶幼學時 書聲瑯瑯環(huán)屋房 閑修詩詞笑語談 待君束發(fā)時 青衫頎長風儀翩...
    梓萏閱讀 385評論 0 0