Objective-C Runtime

Objective-C語言總是盡可能地將工作從編譯鏈接時推遲到運行時。只要有可能,Objective-C總是使用動態的方式來解決問題。這意味著Objective-C語言不僅需要一個編譯器,同時也需要一個運行時系統來執行編譯好的代碼。Runtime扮演的角色類似于Objective-C語言的操作系統,Objective-C基于該系統來工作。

1、與Runtime的交互

Objective-C程序有三種途徑和運行時系統交互:

  1. 通過Objective-C源代碼;
  2. 通過Foundation框架中NSObject的方法;
  3. 通過直接調用Runtime的函數。
1.1、通過Objective-C源代碼

大部分情況下,你只需編寫和編譯Objective-C源代碼,運行時系統在后臺自動運行。

當編譯Objective-C類和方法時,編譯器為實現語言動態特性將自動創建一些數據結構和函數。這些數據結構包含類定義和協議定義的信息(如objc_msgSend函數)。

1.2、通過NSObject的方法

Cocoa中絕大部分類都是NSObject的子類,都繼承了NSObject的方法。

NSObject的某些方法可以從運行時系統中獲取信息,對對象進行一定程度的自我檢查,如class返回對象的類;isKindOfClass:isMemberOfClass:檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應指定的消息;conformsToProtocol:檢查對象是否實現了指定協議類的方法;methodForSelector:返回指定方法實現的地址。

1.3、通過Runtime的函數

Runtime 系統是一個由一系列函數和數據結構組成,具有公共接口的動態庫,頭文件存放在/usr/include/objc中。這些函數支持用純C的函數來實現 Objective-C 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是我們在寫 Objc 代碼時一般不會直接用到這些函數的。在Objective-C Runtime Reference中有對 Runtime 函數的詳細文檔。

2、Runtime術語

  • SEL

SEL是映射到方法的C字符串,它不同于C語言中的函數指針,函數指針直接保存了方法的地址,但SEL只是方法編號。它的數據結構是這樣的:

typedef struct objc_selector *SEL;

可以用 Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器。

  • id

概括來說,id是一個指向實例的指針,定義如下:

typedef struct objc_object *id;

objc_object的定義是:

struct objc_object { Class isa; };
struct objc_class {
    Class isa;

#if !__OBJC2__
    ......
#endif
};

可以看到,id 是指向 objc_object 結構體的指針,而 objc_object 包含一個 Class 的結構體指針 isa。

不過isa指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而應該用class方法來確定實例對象的類。因為KVO的實現原理就是將被觀察對象的isa指針指向一個動態創建的中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文檔

  • Class

Class是一個指向objc_class結構體的指針:

typedef struct objc_class *Class;

在 runtime.h 中可以看到

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;

objc_class結構體就是 Objective-C 的對象系統的基石,其中第一個字段isaobjc_class結構體指針,指向該對象所屬的類型對象。實際上,在 Objective-C 中,類本身也是一個對象,而一個類的 isa 指針指向它的元類(Meta Class)。元類是一個類對象的類,元類中存儲著類方法。

向一個對象發送消息時,runtime會在這個對象所屬的那個類的方法列表中查找。而向一個類發送消息時,runtime會在這個類的元類的方法列表中查找。每個類都會有一個單獨的元類,因為每個類的類方法基本不可能完全相同。

那么元類是什么?

和類一樣,元類也是一個對象,它也有一個 isa 指針指向其所屬的類。所有的元類都使用 NSObject 的元類作為它們的所屬類。NSObject 的元類是它自己。

與類一樣,元類也有自己的父類。meta class 的 super class 是 super class 的 meta class。直到基類的 meta class,它的 super class 指向基類自身。關系如下:

類關系圖
  • Method

Method是一種代表類中某個方法的類型。

typedef struct objc_method *Method;

objc_method儲了方法名,方法類型和方法實現:

struct objc_method {
    SEL method_name                         OBJC2_UNAVAILABLE;
    char *method_types                      OBJC2_UNAVAILABLE;
    IMP method_imp                          OBJC2_UNAVAILABLE;
}                                           OBJC2_UNAVAILABLE;

方法名類型為SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。

方法類型method_types是個 char 指針,其實存儲著方法的參數類型和返回值類型。

method_imp指向了方法的實現,本質上是一個函數指針,后面會詳細講到。

  • Ivar

Ivar是一種代表類中實例變量的類型。

typedef struct objc_ivar *Ivar;

可以根據實例查找其在類中的名字,也就是“反射”:

-(NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}

class_copyIvarList 函數獲取的不僅有實例變量,還有屬性。但會在原本的屬性名前加上一個下劃線。

  • IMP

IMP是一個函數指針,它的定義是:

typedef id (*IMP)(id, SEL, ...);

當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在后面會提到。

  • Cache

在 runtime.h 中Cache的定義如下:

typedef struct objc_cache *Cache;

objc_class結構體中有一個struct objc_cache *cache,它到底是緩存啥的呢?

Cache為方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優先在Cache中查找。Runtime 系統會把被調用的方法存到Cache中(理論上講一個方法如果被調用,那么它有可能今后還會被調用),下次查找的時候效率更高。

  • Property

@property標記了類中的屬性,它是一個指向objc_property結構體的指針:

typedef struct objc_property *Property;

可以通過class_copyPropertyListprotocol_copyPropertyList方法來獲取類和協議中的屬性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回類型為指向指針的指針,因為屬性列表是個數組,每個元素內容都是一個objc_property_t指針,而這兩個函數返回的值是指向這個數組的指針。

相對于class_copyIvarList函數,使用class_copyPropertyList 函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。

你可以用property_getAttributes函數來發掘屬性的名稱和@encode類型字符串:

  1. property_getAttributes 返回的字符串以字母 T 開始,接著是@encode 編碼和逗號。
  2. 如果屬性有 readonly 修飾,則字符串中含有 R 和逗號。
  3. 如果屬性有 copy 或者 retain 修飾,則字符串分別含有 C 或者&,然后是逗號。
  4. 如果屬性定義有定制的 getter 和 setter 方法,則字符串中有 G 或者 S 跟著相應的方法名以及逗號(例如,GcustomGetter,ScustomSetter:,,)。
  5. 如果屬性是只讀的,且有定制的 get 訪問方法,則描述到此為止。
  6. 字符串以 V 然后是屬性的名字結束。

3、消息

本段主要描述如何將發消息轉換為objc_msgSend函數調用,如何通過名字來指定一個方法,以及如何使用objc_msgSend函數。

3.1、獲得方法地址

使用NSObject類中的methodForSelector:方法,可以獲得一個指向方法實現的指針,通過該指針可以直接調用方法實現。例:

void (*setter) (id, SEL, BOOL);
// methodForSelector:方法會返回一個函數指針
setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
for (int i = 0; i < 1000; i ++) {
    // 使用函數指針直接調函數,參數一:函數對象,參數二:函數簽名,參數三:函數參數
    setter(self, @selector(setFilled:), NO);
}

注意,methodForSelector:是Runtime提供的功能,不是Objective-C語言本身的功能。

3.2、objc_msgSend函數

Objective-C中,消息是在運行的時候才和方法實現綁定的。[receiver message]會被編譯器轉換成對objc_msgSend函數的調用。該函數有兩個主要參數:receiverselector,再加上參數的話就是objc_msgSend(receiver, selector, arg1, arg2, ...)

消息發送的詳細步驟:

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain ,release 這些函數了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行。
  4. 如果 cache 找不到就找一下方法分發表。
  5. 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
  6. 找到方法實現之后,然后將消息接收者對象及方法中指定的參數傳給找到的方法實現,最后,將方法實現的返回值作為該函數的返回值返回。
  7. 如果還找不到就要開始進入動態方法解析了,后面會提到。

消息機制的關鍵在于編譯器為類和對象生成的結構,每個類的結構中至少包含兩個基本元素:isa指針和方法列表。

當對象被創建時,它會被分配內存,并初始化實例變量。對象的第一個實例變量是一個指向該對象的類結構體的指針,即isa。通過isa指針可以訪問它對應的類及相應的父類。方法列表存放方法名字和對應的實現地址。

對象接收消息時,objc_msgSend先根據該對象的isa指針找到該對象對應的類的方法表,從表中尋找對應的方法。如果找不到,objc_msgSend將繼續在父類中尋找,直到NSObject類。一旦找到對應方法,objc_msgSend會以消息接收者對象為參數調用該方法。

為了加快消息的處理過程,運行時系統通常會將使用過的方法放入緩存中。每個類都有一個獨立的緩存,同時包括繼承的方法和在該類中定義的方法。objc_msgSend在尋找方法時,會優先在緩存中尋找。如果緩存中已經有了需要的方法,則消息僅僅比函數調用慢一點點。

3.3、使用隱藏的參數

我們經常使用self來表示當前方法的對象,但是為什么它能表示當前方法對象呢?實際上它是在代碼編譯時插入方法中的。

當objc_msgSend找到方法對應的實現時,它會直接調用該方法,并將消息中的參數傳遞給方法實現,同時,它還傳遞兩個隱藏的參數:接收消息的對象(self)和方法選擇器(_cmd)。

在方法中可以通過self來引用消息接收者對象,通過_cmd來引用方法本身。

3.4、動態方法解析

我們可以通過resolveInstanceMethod:resolveClassMethod:方法動態地添加實例方法和類方法的實現。當Runtime系統在緩存和方法列表中找不到要執行的方法時,會調用resolveInstanceMethod:resolveClassMethod:方法來給我們一次動態添加方法實現的機會。例如,有如下的函數:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

可以通過resolveInstanceMethod:將它作為類方法 resolveThisMethodDynamically的實現:

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if (aSEL == @selector(resolveThisMethodDynamically)) {
    class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
    return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

其中 “v@:” 表示返回值和參數,這個符號涉及 Type Encoding

動態方法解析會在消息轉發之前前執行。如果 respondsToSelector:instancesRespondToSelector:方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你實現了resolveInstanceMethod:方法但是仍然希望正常進行消息轉發,只需要返回NO就可以了。

4、消息轉發

通常,給一個對象發送它不能處理的消息會得到出錯提示,不過,運行時系統在拋出錯誤之前,還有三次機會拯救程序:

  1. Method Resolution
  2. Fast Forwarding
  3. Normal Forwarding
message forward
Method Resolution(動態方法解析)

Runtime系統在運行時會先調用resolveInstanceMethod:resolveClassMethod:方法,讓我們添加方法的實現。如果添加方法并返回YES,那系統就會重新啟動一次消息發送的過程。如果沒有實現或返回NO,會執行 Fast Forwarding 操作。

Fast Forwarding(快速轉發)

如果目標對象實現forwardingTargetForSelector:方法,并且這個方法返回的不是nil或self,也會重啟消息發送的過程,把這消息轉發給指定對象來處理。否則,就會繼續 Normal Fowarding。

Normal Forwarding(“慢速”轉發)

如果沒有使用 Fast Forwarding 來轉發消息,最后只能使用 Normal Forwarding 來進行消息轉發。它會調用methodSignatureForSelector:方法來獲取函數的參數和返回值,如果返回為nil,程序會Crash掉,并拋出 unrecognized selector sent to instance 異常信息。如果返回一個函數簽名,系統就會創建一個NSInvocation對象并調用forwardInvocation:方法進行消息轉發。

4.1、轉發

如果一個對象收到一條無法處理的消息(如動態方法解析返回NO時),運行時系統會在拋出錯誤前會執行消息轉發,給該對象發送forwardInvocation:消息,我們可以重寫這個方法來定義我們的轉發邏輯:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
        [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

該消息的唯一參數是個NSInvocation類型的對象,該對象封裝了原始的消息和消息的參數。我們可以實現forwardInvocation:方法來對不能處理的消息做一些默認的處理,也可以將消息轉發給其他對象來處理,而不拋出錯誤。

這里需要注意的是參數anInvocation是從哪的來的呢?其實在forwardInvocation:消息發送前,Runtime系統會向對象發送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,否則會拋異常。

當一個對象由于沒有相應的方法實現而無法響應某消息時,運行時系統將通過forwardInvocation:消息通知該對象。每個對象都從 NSObject 類中繼承了forwardInvocation:方法。然而NSObject 中的方法實現只是簡單地調用了 doesNotRecognizeSelector:。通過實現自己的 forwardInvocation:方法,你可以在該方法實現中將消息轉發給其它對象。

forwardInvocation:方法就像一個不能識別的消息的分發中心,將這些消息轉發給不同接收對象。或者它也可以象一個運輸站將所有的消息都發送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的“吃掉”某些消息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應,這一切都取決于方法的具體實現。該方法所提供是將不同的對象鏈接到消息鏈的能力。

注意: forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調用。 所以,如果你希望你的對象將一個消息轉發給其它對象,你的對象就不能有這個方法。否則,forwardInvocation:將不會被調用。

4.2、轉發和多重繼承

消息轉發很像繼承,并且可以用來在Objective-C程序中模擬多重繼承。如下圖所示,一個對象通過轉發來響應消息,看起來就像該對象從別的類那借來了或者”繼承“了方法實現一樣。

消息轉發和多重繼承

在上圖中,Warrior 類的一個對象實例將 negotiate 消息轉發給 Diplomat 類的一個實例。看起來,Warrior 類似乎和 Diplomat 類一樣, 響應 negotiate 消息,并且行為和 Diplomat 一樣,實際上是 Diplomat 類響應了該消息。

消息轉發彌補了Objective-C不支持多重繼承的性質,也避免了因為多繼承導致單個類變得臃腫復雜。

4.3、轉發和類繼承

盡管消息轉發很像繼承,但它不是繼承。例如在 NSObject 類中,方法respondsToSelector:isKindOfClass:只會出現在繼承鏈中,而不是消息轉發鏈中。例如,如果向一個 Warrior 類的對象詢問它能否響應 negotiate 消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

返回NO,盡管該對象能夠接收和響應negotiate方法。

如果你想要讓它看起來真的像是繼承了negotiate方法,必須重新實現respondsToSelector:isKindOfClass:方法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:也必須重新實現。如果使用的是協議類,需要重新實現的還有conformsToProtocol:方法。類似地,如果一個對象轉發它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發的消息。比如一個對象能給它的替代者對象轉發消息,它需要像下面這樣實現methodSignatureForSelector:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

5、動態屬性關聯

在 OS X 10.6 之后,Runtime系統讓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 );

這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:

enum {
   OBJC_ASSOCIATION_ASSIGN  = 0,
   OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
   OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
   OBJC_ASSOCIATION_RETAIN  = 01401,
   OBJC_ASSOCIATION_COPY  = 01403
};

這些常量對應著引用關聯值的政策,也就是 Objc 內存管理的引用計數機制。你會發現這里邊沒有 weak 屬性,關于如何關聯 weak 屬性,請參考《如何使用 Runtime 給現有的類添加 weak 屬性》

6、Method Swizzling

Method Swizzling 就是方法交換,主要有兩種使用場景:hook和面向切面編程。

hook一般在+load方法中使用:

- (void)replacementReceiveMessage:(id)arg1 {
    [self replacementReceiveMessage:arg1];
}
+ (void)load {
    SEL originalSelector = @selector(ReceiveMessage:);
    SEL overrideSelector = @selector(replacementReceiveMessage:);
    Method originalMethod = class_getInstanceMethod(self, originalSelector);
    Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
    if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
        class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, overrideMethod);
    }
}

APP需要進行數據埋點時,就需要面向切面編程了。假如需要統計按鈕點擊的情況,就可以把按鈕點擊的方法進行交換,這樣就可以最大限度地減少代碼修改和入侵。

要注意的是,在+load中使用 Method Swizzling 是一件很危險的事情,因為它會影響工程中所有相同類的代碼,可能會出現意想不到的Bug。

關于 Method Swizzling 有一個輕量級的庫Aspects很值得閱讀。

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

推薦閱讀更多精彩內容

  • 本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 815評論 0 4
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 753評論 0 2
  • 文中的實驗代碼我放在了這個項目中。 以下內容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 936評論 0 6
  • 本文轉自:楊蕭玉博客 本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C ...
    oneofai閱讀 212評論 0 0
  • 預覽圖 效果圖幀數有點低...., 下面附上代碼, 大家仔細研究, 總之它會在當前可視區內彈來彈去
    劉翾閱讀 416評論 0 0