【iOS 底層原理】Runtime

runtime 介紹

Objective-C 是一門動態(tài)性比較強(qiáng)的編程語言辛辨,跟 C、C++ 等語言有著很大的不同励背,Objective-C 的動態(tài)性是由 Runtime API 來支撐的春霍,Runtime API 提供的接口基本都是C語言的,源碼由C\C++\匯編語言編寫叶眉。

一址儒、isa 本質(zhì)

每個OC對象都含有一個 isa 指針芹枷,arm64 之前,isa僅僅是一個指針莲趣,保存著對象或類對象內(nèi)存地址鸳慈,在 arm64 架構(gòu)之后,apple對isa進(jìn)行了優(yōu)化喧伞,變成了一個共用體(union)結(jié)構(gòu)走芋,同時使用位域來存儲更多的信息。

image.png

所以 arm64 之后潘鲫,isa 指針并不是直接指向類對象或者元類對象翁逞,而是需要 &ISA_MASK 通過位運(yùn)算才能獲取到類對象或者元類對象的地址。

1.背景知識介紹

如下次舌,char 類型占據(jù) 8 字節(jié)熄攘,使用它的后三位來表示 tall rich handsome 三個 bool 值。例如 _tallRichHansome 的值為 0b 0000 0010 彼念,那么只使用8個二進(jìn)制位中的最后3個挪圾,分別為其賦值0或者1來代表 tall、rich逐沙、handsome 的值哲思。

@interface Person()
{
    char _tallRichHandsome;
}
image.png
取值

假如 char 類型的成員變量中存儲的二進(jìn)制為 0b 0000 0010 如果想將倒數(shù)第2位的值也就是 rich 的值取出來,可以使用&進(jìn)行按位與運(yùn)算進(jìn)而取出相應(yīng)位置的值吩案。
&:按位與棚赔,同真為真,其他都為假徘郭。

// 示例
// 取出倒數(shù)第三位 tall
  0000 0010
& 0000 0100
------------
  0000 0000  // 取出倒數(shù)第三位的值為0靠益,其他位都置為0

// 取出倒數(shù)第二位 rich
  0000 0010
& 0000 0010
------------
  0000 0010 // 取出倒數(shù)第二位的值為1,其他位都置為0

按位與可以用來取出特定的位残揉,想取出哪一位就將那一位置為1胧后,其他為都置為0,然后同原數(shù)據(jù)進(jìn)行按位與計算抱环,即可取出特定的位壳快。

getter 方法改寫如下

#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1

- (BOOL)tall
{
    return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
    return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
    return !!(_tallRichHandsome & HandsomeMask);
}

上述代碼中(_tallRichHandsome & TallMask)的值為0000 0010也就是2,但是我們需要的是一個BOOL類型的值 0 或者 1 镇草,那么!!2就將 2 先轉(zhuǎn)化為 0 眶痰,之后又轉(zhuǎn)化為 1。相反如果按位與取得的值為 0 時梯啤,!!0將 0 先轉(zhuǎn)化為 1 之后又轉(zhuǎn)化為 0竖伯。
因此使用!!兩個非操作將值轉(zhuǎn)化為 0 或者 1 來表示相應(yīng)的值。

掩碼 : 上述代碼中定義了三個宏,用來分別進(jìn)行按位與運(yùn)算而取出相應(yīng)的值黔夭,一般用來按位與(&)運(yùn)算的值稱之為掩碼宏胯。

上述宏定義可以使用<<(左移)優(yōu)化成如下代碼

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
設(shè)值

設(shè)值即是將某一位設(shè)值為0或者1,可以使用|(按位或)操作符本姥。 | : 按位或肩袍,只要有一個1即為1,否則為0婚惫。
如果想將某一位置為1的話氛赐,那么將原本的值與掩碼進(jìn)行按位或的操作即可,例如我們想將tall置為1

// 將倒數(shù)第三位 tall置為1
  0000 0010  // _tallRichHandsome
| 0000 0100  // TallMask
------------
  0000 0110 // 將tall置為1先舷,其他位值都不變

如果想將某一位置為0的話艰管,需要將掩碼按位取反(~ : 按位取反符),之后在與原本的值進(jìn)行按位與操作即可蒋川。

// 將倒數(shù)第二位 rich置為0
  0000 0010  // _tallRichHandsome
& 1111 1101  // RichMask按位取反
------------
  0000 0000 // 將rich置為0牲芋,其他位值都不變

set 代碼改寫如下

- (void)setTall:(BOOL)tall
{
    if (tall) { // 如果需要將值置為1  // 按位或掩碼
        _tallRichHandsome |= TallMask;
    }else{ // 如果需要將值置為0 // 按位與(按位取反的掩碼)
        _tallRichHandsome &= ~TallMask; 
    }
}
- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome |= RichMask;
    }else{
        _tallRichHandsome &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome |= HandsomeMask;
    }else{
        _tallRichHandsome &= ~HandsomeMask;
    }
}
位域

將上述代碼進(jìn)行優(yōu)化,使用結(jié)構(gòu)體位域捺球,可以使代碼可讀性更高缸浦。 位域聲明 位域名 : 位域長度;

使用位域需要注意以下3點(diǎn):

  1. 如果一個字節(jié)所剩空間不夠存放另一位域時氮兵,應(yīng)從下一單元起存放該位域裂逐。也可以有意使某位域從下一單元開始。
  2. 位域的長度不能大于數(shù)據(jù)類型本身的長度泣栈,比如int類型就不能超過32位二進(jìn)位卜高。
  3. 位域可以無位域名,這時它只用來作填充或調(diào)整位置南片。無名的位域是不能使用的掺涛。

使用位域進(jìn)行優(yōu)化

@interface Person()
{
    struct {
        char handsome : 1; // 位域,代表占用一位空間
        char rich : 1;  // 按照順序只占一位空間
        char tall : 1; 
    }_tallRichHandsome;
}
共用體

為了使代碼存儲數(shù)據(jù)高效率的同時疼进,有較強(qiáng)的可讀性薪缆,可以使用共用體來增強(qiáng)代碼可讀性,同時使用位運(yùn)算來提高數(shù)據(jù)存取的效率颠悬。

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1

@interface Person()
{
    union {
        char bits;
       // 結(jié)構(gòu)體僅僅是為了增強(qiáng)代碼可讀性,無實質(zhì)用處
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}
@end

上述代碼中使用位運(yùn)算這種比較高效的方式存取值定血,使用union共用體來對數(shù)據(jù)進(jìn)行存儲赔癌。增加讀取效率的同時增強(qiáng)代碼可讀性。
其中 _tallRichHandsome 共用體只占用一個字節(jié)澜沟,因為結(jié)構(gòu)體中tall灾票、rich、handsome都只占一位二進(jìn)制空間茫虽,所以結(jié)構(gòu)體只占一個字節(jié)刊苍,而char類型的bits也只占一個字節(jié)既们,他們都在共用體中,因此共用一個字節(jié)的內(nèi)存即可正什。
其中_tallRichHandsome共用體只占用一個字節(jié)啥纸,因為結(jié)構(gòu)體中tall、rich婴氮、handsome都只占一位二進(jìn)制空間斯棒,所以結(jié)構(gòu)體只占一個字節(jié),而char類型的bits也只占一個字節(jié)主经,他們都在共用體中荣暮,因此共用一個字節(jié)的內(nèi)存即可。

2.isa 源碼分析

isa 源碼如下

// 截取objc_object內(nèi)部分代碼
struct objc_object {
private:
    isa_t isa;
}

isa指針其實是一個isa_t類型的共用體罩驻,來到isa_t內(nèi)部查看其結(jié)構(gòu)

// 精簡過的isa_t共用體
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA
# if __arm64__      
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__     
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };

# else
#   error unknown architecture for packed isa
# endif
#endif

isa_t 是 union 類型共同體穗酥。共用體中有一個結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部分別定義了一些變量惠遏,變量后面的值代表的是該變量占用多少個字節(jié)砾跃,也就是位域。
共用體:共用體是一種特殊的數(shù)據(jù)類型爽哎,允許您在相同的內(nèi)存位置存儲不同的數(shù)據(jù)類型蜓席。您可以定義一個帶有多成員的共用體,但是任何時候只能有一個成員帶有值课锌。共用體提供了一種使用相同的內(nèi)存位置的有效方式厨内。

源碼中通過共用體的形式存儲了64位的值,這些值在結(jié)構(gòu)體中被展示出來渺贤,通過對bits進(jìn)行位運(yùn)算而取出相應(yīng)位置的值雏胃。

3.獲取 Class、Meta-Class 信息

isa 位域中的 shiftcls 位中存儲著 Class志鞍、Meta-Class 對象的內(nèi)存地址信息瞭亮,我們之前在 OC 對象的本質(zhì)中提到過,對象的 isa 指針需要同 ISA_MASK 經(jīng)過一次&(按位與)運(yùn)算才能得出真正的 Class 對象地址固棚。

image.png

ISA_MASK 的值為 0x0000000ffffffff8ULL统翩,ISA_MASK 的值轉(zhuǎn)化為二進(jìn)制中有33位都為1。另外 ISA_MASK 最后三位的值為0此洲,那么任何數(shù)同ISA_MASK按位與運(yùn)算之后厂汗,得到的最后三位必定都為0,因此任何類對象或元類對象的內(nèi)存地址最后三位必定為0呜师,轉(zhuǎn)化為十六進(jìn)制末位必定為8或者0娶桦。

4.isa 中存儲的信息及作用

struct {
    // 0代表普通的指針,存儲著Class,Meta-Class對象的內(nèi)存地址衷畦。
    // 1代表優(yōu)化后的使用位域存儲更多的信息栗涂。
    uintptr_t nonpointer        : 1; 

   // 是否有設(shè)置過關(guān)聯(lián)對象,如果沒有祈争,釋放時會更快
    uintptr_t has_assoc         : 1;

    // 是否有C++析構(gòu)函數(shù)斤程,如果沒有,釋放時會更快
    uintptr_t has_cxx_dtor      : 1;

    // 存儲著Class铛嘱、Meta-Class對象的內(nèi)存地址信息
    uintptr_t shiftcls          : 33; 

    // 用于在調(diào)試時分辨對象是否未完成初始化
    uintptr_t magic             : 6;

    // 是否有被弱引用指向過暖释。
    uintptr_t weakly_referenced : 1;

    // 對象是否正在釋放
    uintptr_t deallocating      : 1;

    // 引用計數(shù)器是否過大無法存儲在isa中
    // 如果為1,那么引用計數(shù)會存儲在一個叫SideTable的類的屬性中
    uintptr_t has_sidetable_rc  : 1;

    // 里面存儲的值是引用計數(shù)器減1
    uintptr_t extra_rc          : 19;
};

注意:只要設(shè)置過關(guān)聯(lián)對象或者弱引用引用過對象 has_assoc 和 weakly_referenced 的值就會變成1墨吓,不論之后是否將關(guān)聯(lián)對象置為 nil 或斷開弱引用球匕。

如果沒有設(shè)置過關(guān)聯(lián)對象,對象釋放時會更快帖烘,這是因為對象在銷毀時會判斷是否有關(guān)聯(lián)對象進(jìn)而對關(guān)聯(lián)對象釋放亮曹。參考對象銷毀的源碼:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        Class isa = obj->getIsa();
        // 是否有c++析構(gòu)函數(shù)
        if (isa->hasCxxDtor()) {
            object_cxxDestruct(obj);
        }
        // 是否有關(guān)聯(lián)對象,如果有則移除
        if (isa->instancesHaveAssociatedObjects()) {
            _object_remove_assocations(obj);
        }
        objc_clear_deallocating(obj);
    }
    return obj;
}

二秘症、class 的結(jié)構(gòu)

class 內(nèi)部結(jié)構(gòu)如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

image.png

1.class_rw_t

bits & FAST_DATA_MASK位運(yùn)算之后照卦,可以得到class_rw_t,而class_rw_t中存儲著方法列表乡摹、屬性列表以及協(xié)議列表役耕,來看一下class_rw_t部分代碼

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods; // 方法列表
    property_array_t properties; // 屬性列表
    protocol_array_t protocols; // 協(xié)議列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};

上述源碼中,method_array_t聪廉、property_array_t瞬痘、protocol_array_t其實都是二維數(shù)組,來到method_array_t板熊、property_array_t框全、protocol_array_t內(nèi)部看一下。這里以method_array_t為例干签,method_array_t本身就是一個數(shù)組津辩,數(shù)組里面存放的是數(shù)組method_list_t,method_list_t里面最終存放的是method_t

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};


class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

 public:
    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};


class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t> 
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;

 public:
    protocol_array_t duplicate() {
        return Super::duplicate<protocol_array_t>();
    }
};

class_rw_t里面的methods容劳、properties喘沿、protocols是二維數(shù)組,是可讀可寫的竭贩,其中包含了類的初始內(nèi)容以及分類的內(nèi)容蚜印。
以method_array_t為例,圖示其中的結(jié)構(gòu)娶视。

image.png

image.png

2.class_ro_t

class_ro_t 中也有存儲方法晒哄、屬性、協(xié)議列表肪获,另外還有成員變量列表寝凌。接著來看一下 class_ro_t 部分代碼

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

class_ro_t *ro是只讀的,內(nèi)部直接存儲的直接就是method_list_t孝赫、protocol_list_t 较木、property_list_t類型的一維數(shù)組,數(shù)組里面分別存放的是類的初始信息青柄,以method_list_t為例伐债,method_list_t中直接存放的就是method_t,但是是只讀的致开,不允許增加刪除修改峰锁。

image.png

注意:class_ro_t 中包含 ivar_list_t,但 class_rw_t 中不包含双戳,說明不允許動態(tài)添加成員變量虹蒋,初始化時就確定了成員變量。但 property_list_t 可以飒货。

3.class_ro_t 與 class_rw_t 的合并

class_rw_t中的methods是二維數(shù)組的結(jié)構(gòu)魄衅,并且可讀可寫,因此可以動態(tài)的添加方法塘辅,并且更加便于分類方法的添加晃虫。參考 category 源碼可以發(fā)現(xiàn),attachList函數(shù)內(nèi)通過memmove 和 memcpy兩個操作將分類的方法列表合并在本類的方法列表中扣墩。那么此時就將分類的方法和本類的方法統(tǒng)一整合到一起了哲银。

其實一開始類的方法,屬性沮榜,成員變量屬性協(xié)議等等都是存放在class_ro_t中的盘榨,當(dāng)程序運(yùn)行的時候哄啄,需要將分類中的列表跟類初始的列表合并在一起的時筷黔,就會將class_ro_t中的列表和分類中的列表合并起來存放在class_rw_t中,也就是說class_rw_t中有部分列表是從class_ro_t里面拿出來的眠寿。

運(yùn)行時調(diào)用的 realizeClass 部分源碼如下:

static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // 最開始cls->data是指向ro的
    ro = (const class_ro_t *)cls->data();

    if (ro->flags & RO_FUTURE) { 
        // rw已經(jīng)初始化并且分配內(nèi)存空間
        rw = cls->data();  // cls->data指向rw
        ro = cls->data()->ro;  // cls->data()->ro指向ro
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else { 
        // 如果rw并不存在型酥,則為rw分配空間
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // 分配空間
        rw->ro = ro;  // rw->ro重新指向ro
        rw->flags = RW_REALIZED|RW_REALIZING;
        // 將rw傳入setData函數(shù)山憨,等于cls->data()重新指向rw
        cls->setData(rw); 
    }
}

類的初始信息本來其實是存儲在class_ro_t中的,并且ro本來是指向cls->data()的弥喉,也就是說bits.data()得到的是ro郁竟,但是在運(yùn)行過程中創(chuàng)建了class_rw_t,并將cls->data指向rw由境,同時將初始信息ro賦值給rw中的ro棚亩。最后在通過setData(rw)設(shè)置data蓖议。那么此時bits.data()得到的就是rw,之后再去檢查是否有分類讥蟆,同時將分類的方法勒虾,屬性,協(xié)議列表整合存儲在class_rw_t的方法瘸彤,屬性及協(xié)議列表中修然。

4.class_rw_t 中是方法相關(guān)數(shù)據(jù)結(jié)構(gòu)

method_t

method_array_t中最終存儲的是method_t,method_t是對方法质况、函數(shù)的封裝愕宋,每一個方法對象就是一個method_t。通過源碼看一下method_t的結(jié)構(gòu)體

struct method_t {
    SEL name;  // 函數(shù)名
    const char *types;  // 編碼(返回值類型结榄,參數(shù)類型)
    IMP imp; // 指向函數(shù)的指針(函數(shù)地址)
};

image.png
SEL

SEL代表方法\函數(shù)名中贝,一般叫做選擇器,底層結(jié)構(gòu)跟char *類似 typedef struct objc_selector *SEL;臼朗,可以把SEL看做是方法名字符串雄妥。

SEL可以通過@selector()和sel_registerName()獲得

SEL sel1 = @selector(test);
SEL sel2 = sel_registerName("test");

或者

char *string = sel_getName(sel1);
NSString *string2 = NSStringFromSelector(sel2);

注:不同類中相同名字的方法,所對應(yīng)的方法選擇器是相同的依溯。SEL僅僅代表方法的名字老厌,并且不同類中相同的方法名的SEL是全局唯一的。

NSLog(@"%p,%p", sel1,sel2);
Runtime-test[23738:8888825] 0x1017718a3,0x1017718a3
types

types包含了函數(shù)返回值黎炉,參數(shù)編碼的字符串枝秤。通過字符串拼接的方式將返回值和參數(shù)拼接成一個字符串,來代表函數(shù)返回值及參數(shù)慷嗜。

蘋果官方對 types 的描述如下


image.png

舉例說明:v16@0:8

- (void) test;

 v    16      @     0     :     8
void         id          SEL
// 16表示參數(shù)的占用空間大小淀弹,id后面跟的0表示從0位開始存儲,id占8位空間庆械。
// SEL后面的8表示從第8位開始存儲薇溃,SEL同樣占8位空間

方法都默認(rèn)有兩個參數(shù)的,id類型的self缭乘,和SEL類型的_cmd沐序,而上述通過對types的分析同時也驗證了這個說法。

復(fù)雜例子:

- (int)testWithAge:(int)age Height:(float)height
{
    return 0;
}
  i    24    @    0    :    8    i    16    f    20
int         id        SEL       int        float
// 參數(shù)的總占用空間為 8 + 8 + 4 + 4 = 24
// id 從第0位開始占據(jù)8位空間
// SEL 從第8位開始占據(jù)8位空間
// int 從第16位開始占據(jù)4位空間
// float 從第20位開始占據(jù)4位空間

iOS提供了@encode的指令堕绩,可以將具體的類型轉(zhuǎn)化成字符串編碼策幼。

NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));

// 打印內(nèi)容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :
IMP

IMP代表函數(shù)的具體實現(xiàn),存儲的內(nèi)容是函數(shù)地址奴紧。也就是說當(dāng)找到imp的時候就可以找到函數(shù)實現(xiàn)特姐,進(jìn)而對函數(shù)進(jìn)行調(diào)用。

5.方法緩存機(jī)制 cache_t

回到類對象結(jié)構(gòu)體黍氮,成員變量cache就是用來對方法進(jìn)行緩存的唐含。

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}
image.png

cache_t cache;用來緩存曾經(jīng)調(diào)用過的方法浅浮,可以提高方法的查找速度。

回顧方法調(diào)用過程:調(diào)用方法的時候捷枯,需要去方法列表里面進(jìn)行遍歷查找脑题。如果方法不在列表里面,就會通過superclass找到父類的類對象铜靶,在去父類類對象方法列表里面遍歷查找。

如果方法需要調(diào)用很多次的話他炊,那就相當(dāng)于每次調(diào)用都需要去遍歷多次方法列表争剿,為了能夠快速查找方法,apple設(shè)計了cache_t來進(jìn)行方法緩存痊末。

每當(dāng)調(diào)用方法的時候蚕苇,會先去cache中查找是否有緩存的方法,如果沒有緩存凿叠,在去類對象方法列表中查找涩笤,以此類推直到找到方法之后,就會將方法直接存儲在cache中盒件,下一次在調(diào)用這個方法的時候蹬碧,就會在類對象的cache里面找到這個方法,直接調(diào)用了炒刁。

cache_t 如何進(jìn)行緩存(cache_t 內(nèi)部結(jié)構(gòu))
struct cache_t {
    struct bucket_t *_buckets; // 散列表 數(shù)組
    mask_t _mask; // 散列表的長度 -1
    mask_t _occupied; // 已經(jīng)緩存的方法數(shù)量
};

bucket_t是以數(shù)組的方式存儲方法列表的恩沽,看一下bucket_t內(nèi)部結(jié)構(gòu)

struct bucket_t {
private:
    cache_key_t _key; // SEL作為Key
    IMP _imp; // 函數(shù)的內(nèi)存地址
};

bucket_t中存儲著SEL和_imp,通過key->value的形式翔始,以SEL為key罗心,函數(shù)實現(xiàn)的內(nèi)存地址 _imp為value來存儲方法。

image.png

散列表:上述bucket_t列表我們稱之為散列表(哈希表)
散列表(Hash table城瞎,也叫哈希表)渤闷,是根據(jù)關(guān)鍵碼值(Key value)而直接進(jìn)行訪問的數(shù)據(jù)結(jié)構(gòu)。也就是說脖镀,它通過把關(guān)鍵碼值映射到表中一個位置來訪問記錄飒箭,以加快查找的速度。這個映射函數(shù)叫做散列函數(shù)蜒灰,存放記錄的數(shù)組叫做散列表补憾。

散列函數(shù)及散列表原理

cache_fill 及 cache_fill_nolock 函數(shù)

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    // 如果沒有initialize直接return
    if (!cls->isInitialized()) return;
    // 確保線程安全,沒有其他線程添加緩存
    if (cache_getImp(cls, sel)) return;
    // 通過類對象獲取到cache 
    cache_t *cache = getCache(cls);
    // 將SEL包裝成Key
    cache_key_t key = getKey(sel);
   // 占用空間+1
    mask_t newOccupied = cache->occupied() + 1;
   // 獲取緩存列表的緩存能力卷员,能存儲多少個鍵值對
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // 如果為空的盈匾,則創(chuàng)建空間,這里創(chuàng)建的空間為4個毕骡。
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 如果所占用的空間占總數(shù)的3/4一下削饵,則繼續(xù)使用現(xiàn)在的空間
    }
    else {
       // 如果占用空間超過3/4則擴(kuò)展空間
        cache->expand();
    }
    // 通過key查找合適的存儲空間岩瘦。
    bucket_t *bucket = cache->find(key, receiver);
    // 如果key==0則說明之前未存儲過這個key,占用空間+1
    if (bucket->key() == 0) cache->incrementOccupied();
    // 存儲key窿撬,imp 
    bucket->set(key, imp);
}
reallocate 函數(shù)

通過上述源碼看到reallocate函數(shù)負(fù)責(zé)分配散列表空間启昧,來到reallocate函數(shù)內(nèi)部。

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    // 舊的散列表能否被釋放
    bool freeOld = canBeFreed();
    // 獲取舊的散列表
    bucket_t *oldBuckets = buckets();
    // 通過新的空間需求量創(chuàng)建新的散列表
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    // 設(shè)置Buckets和Mash劈伴,Mask的值為散列表長度-1
    setBucketsAndMask(newBuckets, newCapacity - 1);
    // 釋放舊的散列表
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

上述源碼中首次傳入reallocate函數(shù)的newCapacity為INIT_CACHE_SIZE密末,INIT_CACHE_SIZE是個枚舉值,也就是4跛璧。因此散列表最初創(chuàng)建的空間就是4個严里。

expand ()函數(shù)

當(dāng)散列表的空間被占用超過3/4的時候,散列表會調(diào)用expand ()函數(shù)進(jìn)行擴(kuò)展追城,我們來看一下expand ()函數(shù)內(nèi)散列表如何進(jìn)行擴(kuò)展的刹碾。

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    // 獲取舊的散列表的存儲空間
    uint32_t oldCapacity = capacity();
    // 將舊的散列表存儲空間擴(kuò)容至兩倍
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    // 為新的存儲空間賦值
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        newCapacity = oldCapacity;
    }
    // 調(diào)用reallocate函數(shù),重新創(chuàng)建存儲空間
    reallocate(oldCapacity, newCapacity);
}

上述源碼中可以發(fā)現(xiàn)散列表進(jìn)行擴(kuò)容時會將容量增至之前的2倍座柱。

find 函數(shù)

最后來看一下散列表中如何快速的通過key找到相應(yīng)的bucket呢迷帜?我們來到find函數(shù)內(nèi)部

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);
    // 獲取散列表
    bucket_t *b = buckets();
    // 獲取mask
    mask_t m = mask();
    // 通過key找到key在散列表中存儲的下標(biāo)
    mask_t begin = cache_hash(k, m);
    // 將下標(biāo)賦值給i
    mask_t i = begin;
    // 如果下標(biāo)i中存儲的bucket的key==0說明當(dāng)前沒有存儲相應(yīng)的key,將b[i]返回出去進(jìn)行存儲
    // 如果下標(biāo)i中存儲的bucket的key==k色洞,說明當(dāng)前空間內(nèi)已經(jīng)存儲了相應(yīng)key戏锹,將b[i]返回出去進(jìn)行存儲
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            // 如果滿足條件則直接reutrn出去
            return &b[i];
        }
    // 如果走到這里說明上面不滿足,那么會往前移動一個空間重新進(jìn)行判定火诸,知道可以成功return為止
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

函數(shù)cache_hash (k, m)用來通過key找到方法在散列表中存儲的下標(biāo)景用,來到cache_hash (k, m)函數(shù)內(nèi)部

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

可以發(fā)現(xiàn)cache_hash (k, m)函數(shù)內(nèi)部僅僅是進(jìn)行了key & mask的按位與運(yùn)算,得到下標(biāo)即存儲在相應(yīng)的位置上

_mask

通過上面的分析我們知道_mask的值是散列表的長度減一惭蹂,那么任何數(shù)通過與_mask進(jìn)行按位與運(yùn)算之后獲得的值都會小于等于_mask伞插,因此不會出現(xiàn)數(shù)組溢出的情況。

總結(jié)

當(dāng)?shù)谝淮问褂梅椒〞r盾碗,消息機(jī)制通過isa找到方法之后媚污,會對方法以SEL為keyIMP為value的方式緩存在cache的_buckets中,當(dāng)?shù)谝淮未鎯Φ臅r候廷雅,會創(chuàng)建具有4個空間的散列表耗美,并將_mask的值置為散列表的長度減一,之后通過SEL & mask計算出方法存儲的下標(biāo)值航缀,并將方法存儲在散列表中商架。舉個例子,如果計算出下標(biāo)值為3芥玉,那么就將方法直接存儲在下標(biāo)為3的空間中蛇摸,前面的空間會留空。

當(dāng)散列表中存儲的方法占據(jù)散列表長度超過3/4的時候灿巧,散列表會進(jìn)行擴(kuò)容操作赶袄,將創(chuàng)建一個新的散列表并且空間擴(kuò)容至原來空間的兩倍揽涮,并重置_mask的值,最后釋放舊的散列表饿肺,此時再有方法要進(jìn)行緩存的話蒋困,就需要重新通過SEL & mask計算出下標(biāo)值之后在按照下標(biāo)進(jìn)行存儲了。

如果一個類中方法很多敬辣,其中很可能會出現(xiàn)多個方法的SEL & mask得到的值為同一個下標(biāo)值雪标,那么會調(diào)用cache_next函數(shù)往下標(biāo)值-1位去進(jìn)行存儲,如果下標(biāo)值-1位空間中有存儲方法溉跃,并且key不與要存儲的key相同村刨,那么再到前面一位進(jìn)行比較,直到找到一位空間沒有存儲方法或者key與要存儲的key相同為止喊积,如果到下標(biāo)0的話就會到下標(biāo)為_mask的空間也就是最大空間處進(jìn)行比較。

當(dāng)要查找方法時玄妈,并不需要遍歷散列表乾吻,同樣通過SEL & mask計算出下標(biāo)值,直接去下標(biāo)值的空間取值即可拟蜻,同上绎签,如果下標(biāo)值中存儲的key與要查找的key不相同,就去前面一位查找酝锅。這樣雖然占用了少量控件诡必,但是大大節(jié)省了時間,也就是說其實apple是使用空間換取了存取的時間搔扁。

通過一張圖更清晰的看一下其中的流程爸舒。

image.png

三、方法調(diào)用本質(zhì)

OC 代碼轉(zhuǎn)換為 C++ 代碼稿蹲,探尋方法調(diào)用的本質(zhì)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
[person test];
//  --------- c++底層代碼
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));

方法調(diào)用分為3個階段

  • 消息發(fā)送階段:負(fù)責(zé)從類及父類的緩存列表及方法列表查找方法扭勉。
  • 動態(tài)解析階段:如果消息發(fā)送階段沒有找到方法,則會進(jìn)入動態(tài)解析階段苛聘,負(fù)責(zé)動態(tài)的添加方法實現(xiàn)涂炎。
  • 消息轉(zhuǎn)發(fā)階段:如果也沒有實現(xiàn)動態(tài)解析方法,則會進(jìn)行消息轉(zhuǎn)發(fā)階段设哗,將消息轉(zhuǎn)發(fā)給可以處理消息的接受者來處理唱捣。

如果消息轉(zhuǎn)發(fā)也沒有實現(xiàn),就會報方法找不到的錯誤网梢,無法識別消息震缭,unrecognzied selector sent to instance

msg_send 源碼流程:


image.png

1.消息發(fā)送

runtime 中消息發(fā)送的代碼是閉源的,只能通過匯編去查看战虏,在 runtime 源碼中搜索 objc_msgSend 查看其內(nèi)部實現(xiàn)蛀序,在 objc-msg-arm64.s 匯編文件可以知道 objc_msgSend 函數(shù)的實現(xiàn)

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

上述匯編源碼中會首先判斷消息接受者reveiver的值欢瞪。 如果傳入的消息接受者為nil則會執(zhí)行LNilOrTagged,LNilOrTagged內(nèi)部會執(zhí)行LReturnZero徐裸,而LReturnZero內(nèi)部則直接return0遣鼓。

如果傳入的消息接受者不為nill則執(zhí)行CacheLookup,內(nèi)部對方法緩存列表進(jìn)行查找重贺,如果找到則執(zhí)行CacheHit骑祟,進(jìn)而調(diào)用方法。否則執(zhí)行CheckMiss气笙,CheckMiss內(nèi)部調(diào)用__objc_msgSend_uncached次企。

__objc_msgSend_uncached內(nèi)會執(zhí)行MethodTableLookup也就是方法列表查找,MethodTableLookup內(nèi)部的核心代碼__class_lookupMethodAndLoadCache3也就是c語言函數(shù)_class_lookupMethodAndLoadCache3

c語言_class_lookupMethodAndLoadCache3函數(shù)內(nèi)部則是對方法查找的核心源代碼潜圃。

image.png
_class_lookupMethodAndLoadCache3 函數(shù)
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward 函數(shù)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    // initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    // 緩存查找, 因為cache傳入的為NO, 這里不會進(jìn)行緩存查找, 因為在匯編語言中CacheLookup已經(jīng)查找過
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    // 防止動態(tài)添加方法缸棵,緩存會變化,再次查找緩存谭期。
    imp = cache_getImp(cls, sel);
    // 如果查找到imp, 直接調(diào)用done, 返回方法地址
    if (imp) goto done;

    // 查找方法列表, 傳入類對象和方法名
    {
        // 根據(jù)sel去類對象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果方法存在堵第,則緩存方法,
            // 內(nèi)部調(diào)用的就是 cache_fill 上文中已經(jīng)詳細(xì)講解過這個方法隧出,這里不在贅述了踏志。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 方法緩存之后, 取出imp, 調(diào)用done返回imp
            imp = meth->imp;
            goto done;
        }
    }

    // 如果類方法列表中沒有找到, 則去父類的緩存中或方法列表中查找方法
    {
        unsigned attempts = unreasonableClassCount();
        // 如果父類緩存列表及方法列表均找不到方法,則去父類的父類去查找胀瞪。
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 查找父類的緩存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父類中找到方法, 在本類中緩存方法, 注意這里傳入的是cls, 將方法緩存在本類緩存列表中, 而非父類中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 執(zhí)行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出循環(huán), 停止搜索
                    break;
                }
            }
            
            // 查找父類的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 同樣拿到方法, 在本類進(jìn)行緩存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 執(zhí)行done, 返回imp
                goto done;
            }
        }
    }
    
    // ---------------- 消息發(fā)送階段完成 ---------------------

    // ---------------- 進(jìn)入動態(tài)解析階段 ---------------------
    // 上述列表中都沒有找到方法實現(xiàn), 則嘗試解析方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // ---------------- 動態(tài)解析階段完成 ---------------------

    // ---------------- 進(jìn)入消息轉(zhuǎn)發(fā)階段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    // 返回方法地址
    return imp;
}
getMethodNoSuper_nolock 函數(shù)

方法列表中查找方法

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二維數(shù)組
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
         // mlists 為 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

上述源碼中g(shù)etMethodNoSuper_nolock函數(shù)中通過遍歷方法列表拿到method_list_t最終通過search_method_list函數(shù)查找方法

search_method_list函數(shù)
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 如果方法列表是有序的针余,則使用二分法查找方法,節(jié)省時間
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 否則則遍歷列表查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}
findMethodInSortedMethodList函數(shù)內(nèi)二分查找實現(xiàn)原理
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // >>1 表示將變量n的各個二進(jìn)制位順序右移1位凄诞,最高位補(bǔ)二進(jìn)制0圆雁。
    // count >>= 1 如果count為偶數(shù)則值變?yōu)?count / 2)。如果count為奇數(shù)則值變?yōu)?count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向數(shù)組中間的值
        probe = base + (count >> 1);
        // 取出中間method_t的name帆谍,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 則折半向后查詢
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
_class_lookupMethodAndLoadCache3函數(shù)內(nèi)部消息發(fā)送的整個流程
image.png

如果消息發(fā)送階段沒有找到方法摸柄,就會進(jìn)入動態(tài)解析方法階段。

image.png

2.動態(tài)解析階段

當(dāng)本類包括父類cache包括class_rw_t中都找不到方法時既忆,就會進(jìn)入動態(tài)方法解析階段驱负。我們來看一下動態(tài)解析階段源碼。

動態(tài)解析的方法

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

_class_resolveMethod函數(shù)內(nèi)部患雇,根據(jù)類對象或元類對象做不同的操作

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

上述代碼中可以發(fā)現(xiàn)跃脊,動態(tài)解析方法之后,會將triedResolver = YES;那么下次就不會在進(jìn)行動態(tài)解析階段了苛吱,之后會重新執(zhí)行retry酪术,會重新對方法查找一遍。也就是說無論我們是否實現(xiàn)動態(tài)解析方法,無論動態(tài)解析方法是否成功绘雁,retry之后都不會在進(jìn)行動態(tài)的解析方法了橡疼。

動態(tài)解析流程圖


image.png

3.消息轉(zhuǎn)發(fā)

如果我們自己也沒有對方法進(jìn)行動態(tài)的解析,那么就會進(jìn)行消息轉(zhuǎn)發(fā)

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

自己沒有能力處理這個消息的時候庐舟,就會進(jìn)行消息轉(zhuǎn)發(fā)階段欣除,會調(diào)用_objc_msgForward_impcache函數(shù)。
通過搜索可以在匯編中找到__objc_msgForward_impcache函數(shù)實現(xiàn)挪略,__objc_msgForward_impcache函數(shù)中調(diào)用__objc_msgForward進(jìn)而找到__objc_forward_handler历帚。

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

我們發(fā)現(xiàn)這僅僅是一個錯誤信息的輸出。 其實消息轉(zhuǎn)發(fā)機(jī)制是不開源的杠娱,但是我們可以猜測其中可能拿返回的對象調(diào)用了objc_msgSend挽牢,重走了一遍消息發(fā)送,動態(tài)解析摊求,消息轉(zhuǎn)發(fā)的過程禽拔。最終找到方法進(jìn)行調(diào)用。

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


image.png
image.png

4.使用動態(tài)解析/轉(zhuǎn)發(fā)機(jī)制

動態(tài)解析

動態(tài)解析對象方法時室叉,會調(diào)用+(BOOL)resolveInstanceMethod:(SEL)sel方法睹栖。 動態(tài)解析類方法時,會調(diào)用+(BOOL)resolveClassMethod:(SEL)sel方法太惠。

這里以實例對象為例通過代碼來看一下動態(tài)解析的過程

@implementation Person
- (void) other {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 動態(tài)的添加方法實現(xiàn)
    if (sel == @selector(test)) {
        // 獲取其他方法 指向method_t的指針
        Method otherMethod = class_getInstanceMethod(self, @selector(other));
        
        // 動態(tài)添加test方法的實現(xiàn)
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        
        // 返回YES表示有動態(tài)添加方法
        return YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}
// 打印結(jié)果
// -[Person other]

在resolveInstanceMethod:方法內(nèi)部使用class_addMethod動態(tài)的添加方法實現(xiàn)磨淌。

這里需要注意class_addMethod用來向具有給定名稱和實現(xiàn)的類添加新方法疲憋,class_addMethod將添加一個方法實現(xiàn)的覆蓋凿渊,但是不會替換已有的實現(xiàn)。也就是說如果上述代碼中已經(jīng)實現(xiàn)了-(void)test方法缚柳,則不會再動態(tài)添加方法埃脏,這點(diǎn)在上述源碼中也可以體現(xiàn),因為一旦找到方法實現(xiàn)就直接return imp并調(diào)用方法了秋忙,不會再執(zhí)行動態(tài)解析方法了彩掐。

class_addMethod 函數(shù)

    /** 
     第一個參數(shù): cls:給哪個類添加方法
     第二個參數(shù): SEL name:添加方法的名稱
     第三個參數(shù): IMP imp: 方法的實現(xiàn),函數(shù)入口灰追,函數(shù)名可與方法名不同(建議與方法名相同)
     第四個參數(shù): types :方法類型堵幽,需要用特定符號,參考API
     */
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

class_getInstanceMethod 函數(shù)

// 獲取其他方法 指向method_t的指針
Method otherMethod = class_getInstanceMethod(self, @selector(other));

總結(jié)

image.png

消息轉(zhuǎn)發(fā)

當(dāng)本類沒有實現(xiàn)方法弹澎,并且沒有動態(tài)解析方法朴下,就會調(diào)用forwardingTargetForSelector函數(shù),進(jìn)行消息轉(zhuǎn)發(fā)苦蒿,我們可以實現(xiàn)forwardingTargetForSelector函數(shù)殴胧,在其內(nèi)部將消息轉(zhuǎn)發(fā)給可以實現(xiàn)此方法的對象。

如果forwardingTargetForSelector函數(shù)返回為nil或者沒有實現(xiàn)的話,就會調(diào)用methodSignatureForSelector方法团滥,用來返回一個方法簽名竿屹,這也是我們正確跳轉(zhuǎn)方法的最后機(jī)會。

如果methodSignatureForSelector方法返回正確的方法簽名就會調(diào)用forwardInvocation方法灸姊,forwardInvocation方法內(nèi)提供一個NSInvocation類型的參數(shù)拱燃,NSInvocation封裝了一個方法的調(diào)用,包括方法的調(diào)用者厨钻,方法名扼雏,以及方法的參數(shù)。在forwardInvocation函數(shù)內(nèi)修改方法調(diào)用對象即可夯膀。

如果methodSignatureForSelector返回的為nil诗充,就會來到doseNotRecognizeSelector:方法內(nèi)部,程序crash提示無法識別選擇器unrecognized selector sent to instance诱建。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能夠處理消息的對象
    if (aSelector == @selector(driving)) {
        // 返回nil則會調(diào)用methodSignatureForSelector方法
        return nil; 
        // return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法簽名:返回值類型蝴蜓、參數(shù)類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
       // return [NSMethodSignature signatureWithObjCTypes: "v@:"];
       // return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
       // 也可以通過調(diào)用Car的methodSignatureForSelector方法得到方法簽名,這種方式需要car對象有aSelector方法
        return [[[Car alloc] init] methodSignatureForSelector: aSelector];

    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation 封裝了一個方法調(diào)用俺猿,包括:方法調(diào)用者茎匠,方法,方法的參數(shù)
//    anInvocation.target 方法調(diào)用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument: NULL atIndex: 0]; 獲得參數(shù)
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//   anInvocation中封裝了methodSignatureForSelector函數(shù)中返回的方法押袍。
//   此時anInvocation.target 還是person對象诵冒,我們需要修改target為可以執(zhí)行方法的方法調(diào)用者。
//   anInvocation.target = [[Car alloc] init];
//   [anInvocation invoke];
    [anInvocation invokeWithTarget: [[Car alloc] init]];
}

// 打印內(nèi)容
// 消息轉(zhuǎn)發(fā)[5781:2164454] car driving

NSInvocation

methodSignatureForSelector方法中返回的方法簽名谊惭,在forwardInvocation中被包裝成NSInvocation對象汽馋,NSInvocation提供了獲取和修改方法名、參數(shù)圈盔、返回值等方法豹芯,也就是說,在forwardInvocation函數(shù)中我們可以對方法進(jìn)行最后的修改驱敲。

生成NSMethodSignature

image.png

四铁蹈、super本質(zhì)

1.舉例

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end

輸出內(nèi)容

Runtime-super[6601:1536402] [self class] = Student
Runtime-super[6601:1536402] [self superclass] = Person
Runtime-super[6601:1536402] ----------------
Runtime-super[6601:1536402] [super class] = Student
Runtime-super[6601:1536402] [super superclass] = Person

上述代碼中可以發(fā)現(xiàn)無論是self還是super調(diào)用class或superclass的結(jié)果都是相同的。

[super run]; 轉(zhuǎn)化為底層源碼內(nèi)部其實調(diào)用的是 objc_msgSendSuper 函數(shù)众眨。

objc_msgSendSuper函數(shù)內(nèi)傳遞了兩個參數(shù)握牧。__rw_objc_super結(jié)構(gòu)體和sel_registerName("run")方法名。

__rw_objc_super結(jié)構(gòu)體內(nèi)傳入的參數(shù)是self和class_getSuperclass(objc_getClass("Student"))也就是Student的父類Person

objc_msgSendSuper 內(nèi)部結(jié)構(gòu)

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

objc_msgSendSuper中傳入的結(jié)構(gòu)體是objc_super娩梨,我們來到objc_super內(nèi)部查看其內(nèi)部結(jié)構(gòu)沿腰。 我們通過源碼查找objc_super結(jié)構(gòu)體查看其內(nèi)部結(jié)構(gòu)。

// 精簡后的objc_super結(jié)構(gòu)體
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父類
    /* super_class is the first class to search */ 
    // 父類是第一個開始查找的類
};

從objc_super結(jié)構(gòu)體中可以發(fā)現(xiàn)receiver消息接受者仍然為self姚建,superclass僅僅是用來告知消息查找從哪一個類開始矫俺。從父類的類對象開始去查找。

image.png

super調(diào)用方法的消息接受者receiver仍然是self,只是從父類的類對象開始去查找方法厘托。

class的底層實現(xiàn)如下面代碼所示

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}
+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

class內(nèi)部實現(xiàn)是根據(jù)消息接受者返回其對應(yīng)的類對象友雳,最終會找到基類的方法列表中,而self和super的區(qū)別僅僅是self從本類類對象開始查找方法铅匹,super從父類類對象開始查找方法押赊,因此最終得到的結(jié)果都是相同的。

2.objc_msgSendSuper2 函數(shù)

super 底層真正調(diào)用的函數(shù)時 objc_msgSendSuper2 函數(shù)


image.png

3.isKindOfClass 與 isMemberOfClass

isKindOfClass isKindOfClass對象方法底層實現(xiàn)

- (BOOL)isMemberOfClass:(Class)cls {
   // 直接獲取實例類對象并判斷是否等于傳入的類對象
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
   // 向上查詢包斑,如果找到父類對象等于傳入的類對象則返回YES
   // 直到基類還不相等則返回NO
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isMemberOfClass 判斷左邊是否剛好等于右邊類型流礁。 isKindOfClass 判斷左邊或者左邊類型的父類是否剛好等于右邊類型。 注意:類方法內(nèi)部是獲取其元類對象進(jìn)行比較

五罗丰、runtime API

1.類相關(guān)API

1. 動態(tài)創(chuàng)建一個類(參數(shù):父類神帅,類名,額外的內(nèi)存空間)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

2. 注冊一個類(要在類注冊之前添加成員變量)
void objc_registerClassPair(Class cls) 

3. 銷毀一個類
void objc_disposeClassPair(Class cls)

示例:
void run(id self , SEL _cmd) {
    NSLog(@"%@ - %@", self,NSStringFromSelector(_cmd));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 創(chuàng)建類 superclass:繼承自哪個類 name:類名 size_t:格外的大小萌抵,創(chuàng)建類是否需要擴(kuò)充空間
        // 返回一個類對象
        Class newClass = objc_allocateClassPair([NSObject class], "Student", 0);
        
        // 添加成員變量 
        // cls:添加成員變量的類 name:成員變量的名字 size:占據(jù)多少字節(jié) alignment:內(nèi)存對齊找御,最好寫1 types:類型,int類型就是@encode(int) 也就是i
        class_addIvar(newClass, "_age", 4, 1, @encode(int));
        class_addIvar(newClass, "_height", 4, 1, @encode(float));
        
        // 添加方法
        class_addMethod(newClass, @selector(run), (IMP)run, "v@:");
        
        // 注冊類
        objc_registerClassPair(newClass);
        
        // 創(chuàng)建實例對象
        id student = [[newClass alloc] init];
    
        // 通過KVC訪問
        [student setValue:@10 forKey:@"_age"];
        [student setValue:@180.5 forKey:@"_height"];
        
        // 獲取成員變量
        NSLog(@"_age = %@ , _height = %@",[student valueForKey:@"_age"], [student valueForKey:@"_height"]);
        
        // 獲取類的占用空間
        NSLog(@"類對象占用空間%zd", class_getInstanceSize(newClass));
        
        // 調(diào)用動態(tài)添加的方法
        [student run];
        
    }
    return 0;
}

// 打印內(nèi)容
// Runtime應(yīng)用[25605:4723961] _age = 10 , _height = 180.5
// Runtime應(yīng)用[25605:4723961] 類對象占用空間16
// Runtime應(yīng)用[25605:4723961] <Student: 0x10072e420> - run

注意
類一旦注冊完畢绍填,就相當(dāng)于類對象和元類對象里面的結(jié)構(gòu)就已經(jīng)創(chuàng)建好了霎桅。
因此必須在注冊類之前,添加成員變量讨永。方法可以在注冊之后再添加滔驶,因為方法是可以動態(tài)添加的。
創(chuàng)建的類如果不需要使用了 卿闹,需要釋放類揭糕。

4. 獲取isa指向的Class,如果將類對象傳入獲取的就是元類對象比原,如果是實例對象則為類對象
Class object_getClass(id obj)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        NSLog(@"%p,%p,%p",object_getClass(person), [Person class],
              object_getClass([Person class]));
    }
    return 0;
}
// 打印內(nèi)容
Runtime應(yīng)用[21115:3807804] 0x100001298,0x100001298,0x100001270

5. 設(shè)置isa指向的Class插佛,可以動態(tài)的修改類型杠巡。例如修改了person對象的類型量窘,也就是說修改了person對象的isa指針的指向,中途讓對象去調(diào)用其他類的同名方法氢拥。
Class object_setClass(id obj, Class cls)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person run];
        
        object_setClass(person, [Car class]);
        [person run];
    }
    return 0;
}
// 打印內(nèi)容
Runtime應(yīng)用[21147:3815155] -[Person run]
Runtime應(yīng)用[21147:3815155] -[Car run]
最終其實調(diào)用了car的run方法

6. 用于判斷一個OC對象是否為Class
BOOL object_isClass(id obj)

// 判斷OC對象是實例對象還是類對象
NSLog(@"%d",object_isClass(person)); // 0
NSLog(@"%d",object_isClass([person class])); // 1
NSLog(@"%d",object_isClass(object_getClass([person class]))); // 1 
// 元類對象也是特殊的類對象

7. 判斷一個Class是否為元類
BOOL class_isMetaClass(Class cls)
8. 獲取類對象父類
Class class_getSuperclass(Class cls)

2.成員變量 API

1. 獲取一個實例變量信息蚌铜,描述信息變量的名字,占用多少字節(jié)等
Ivar class_getInstanceVariable(Class cls, const char *name)

2. 拷貝實例變量列表(最后需要調(diào)用free釋放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

3. 設(shè)置和獲取成員變量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

4. 動態(tài)添加成員變量(已經(jīng)注冊的類是不能動態(tài)添加成員變量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

5. 獲取成員變量的相關(guān)信息嫩海,傳入成員變量信息冬殃,返回C語言字符串
const char *ivar_getName(Ivar v)
6. 獲取成員變量的編碼,types
const char *ivar_getTypeEncoding(Ivar v)

示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 獲取成員變量的信息
        Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
        // 獲取成員變量的名字和編碼
        NSLog(@"%s, %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar));
        
        Person *person = [[Person alloc] init];
        // 設(shè)置和獲取成員變量的值
        object_setIvar(person, nameIvar, @"xx_cc");
        // 獲取成員變量的值
        object_getIvar(person, nameIvar);
        NSLog(@"%@", object_getIvar(person, nameIvar));
        NSLog(@"%@", person.name);
        
        // 拷貝實例變量列表
        unsigned int count ;
        Ivar *ivars = class_copyIvarList([Person class], &count);

        for (int i = 0; i < count; i ++) {
            // 取出成員變量
            Ivar ivar = ivars[i];
            NSLog(@"%s, %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
        }
        
        free(ivars);

    }
    return 0;
}

// 打印內(nèi)容
// Runtime應(yīng)用[25783:4778679] _name, @"NSString"
// Runtime應(yīng)用[25783:4778679] xx_cc
// Runtime應(yīng)用[25783:4778679] xx_cc
// Runtime應(yīng)用[25783:4778679] _name, @"NSString"

3.屬性 API

1. 獲取一個屬性
objc_property_t class_getProperty(Class cls, const char *name)

2. 拷貝屬性列表(最后需要調(diào)用free釋放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

3. 動態(tài)添加屬性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)

4. 動態(tài)替換屬性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)

5. 獲取屬性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

4.方法 API

1. 獲得一個實例方法叁怪、類方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

2. 方法實現(xiàn)相關(guān)操作
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 

3. 拷貝方法列表(最后需要調(diào)用free釋放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

4. 動態(tài)添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

5. 動態(tài)替換方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

6. 獲取方法的相關(guān)信息(帶有copy的需要調(diào)用free去釋放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

7. 選擇器相關(guān)
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

8. 用block作為方法實現(xiàn)
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

六审葬、runtime 應(yīng)用

1.設(shè)置UITextField占位文字的顏色

image.png

2.字典轉(zhuǎn)模型

  • 利用Runtime遍歷所有的屬性或者成員變量
  • 利用KVC設(shè)值

3.替換方法實現(xiàn)

  • class_replaceMethod
  • method_exchangeImplementations

七、面試題

1.下列代碼中Person繼承自NSObject,Student繼承自Person涣觉,寫出下列代碼輸出內(nèi)容痴荐。

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end

輸出內(nèi)容

Runtime-super[6601:1536402] [self class] = Student
Runtime-super[6601:1536402] [self superclass] = Person
Runtime-super[6601:1536402] ----------------
Runtime-super[6601:1536402] [super class] = Student
Runtime-super[6601:1536402] [super superclass] = Person

上述代碼中可以發(fā)現(xiàn)無論是self還是super調(diào)用class或superclass的結(jié)果都是相同的。

2.講一下 OC 的消息機(jī)制

OC中的方法調(diào)用其實都是轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用官册,給receiver(方法調(diào)用者)發(fā)送了一條消息(selector方法名)
objc_msgSend底層有3大階段
消息發(fā)送(當(dāng)前類生兆、父類中查找)、動態(tài)方法解析膝宁、消息轉(zhuǎn)發(fā)

3.具體應(yīng)用

  • 利用關(guān)聯(lián)對象(AssociatedObject)給分類添加屬性
  • 遍歷類的所有成員變量(修改textfield的占位文字顏色鸦难、字典轉(zhuǎn)模型、自動歸檔解檔)
  • 交換方法實現(xiàn)(交換系統(tǒng)的方法)
  • 利用消息轉(zhuǎn)發(fā)機(jī)制解決方法找不到的異常問題
  • ......

4.如下

image.png

isMemberOfClass 判斷左邊是否剛好等于右邊類型员淫。 isKindOfClass 判斷左邊或者左邊類型的父類是否剛好等于右邊類型合蔽。 注意:類方法內(nèi)部是獲取其元類對象進(jìn)行比較

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市介返,隨后出現(xiàn)的幾起案子辈末,更是在濱河造成了極大的恐慌,老刑警劉巖映皆,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挤聘,死亡現(xiàn)場離奇詭異,居然都是意外死亡捅彻,警方通過查閱死者的電腦和手機(jī)组去,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來步淹,“玉大人从隆,你說我怎么就攤上這事$择桑” “怎么了键闺?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長澈驼。 經(jīng)常有香客問我辛燥,道長,這世上最難降的妖魔是什么缝其? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任挎塌,我火速辦了婚禮,結(jié)果婚禮上内边,老公的妹妹穿的比我還像新娘榴都。我一直安慰自己,他們只是感情好漠其,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布嘴高。 她就那樣靜靜地躺著竿音,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拴驮。 梳的紋絲不亂的頭發(fā)上谍失,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音莹汤,去河邊找鬼快鱼。 笑死,一個胖子當(dāng)著我的面吹牛纲岭,可吹牛的內(nèi)容都是我干的抹竹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼止潮,長吁一口氣:“原來是場噩夢啊……” “哼窃判!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喇闸,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤否灾,失蹤者是張志新(化名)和其女友劉穎哑梳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冬筒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年闪水,在試婚紗的時候發(fā)現(xiàn)自己被綠了午衰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淘捡。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖舆瘪,靈堂內(nèi)的尸體忽然破棺而出片效,到底是詐尸還是另有隱情,我是刑警寧澤英古,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布淀衣,位于F島的核電站,受9級特大地震影響召调,放射性物質(zhì)發(fā)生泄漏膨桥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一某残、第九天 我趴在偏房一處隱蔽的房頂上張望国撵。 院中可真熱鬧陵吸,春花似錦玻墅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽环础。三九已至,卻和暖如春剩拢,著一層夾襖步出監(jiān)牢的瞬間线得,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工徐伐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贯钩,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓办素,卻偏偏與公主長得像角雷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子性穿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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