objc-msg-arm64源碼深入分析

在 Objective-C 語言中笨觅,實(shí)例對象執(zhí)行方法蓉媳,而執(zhí)行方法的過程也可以稱為給實(shí)例對象發(fā)送消息。發(fā)送消息的過程執(zhí)行在編譯階段會轉(zhuǎn)化成對 objc_msgSend 函數(shù)的調(diào)用。本文將分析 objc_msgSend 匯編部分主要部分(fast path)牵触。

文章中用到的匯編指令可以參考我個(gè)人的匯編學(xué)習(xí)筆記

Objective-C 實(shí)例對象執(zhí)行方法步驟

objc_msgSend 前2個(gè)傳入?yún)?shù)有對象實(shí)例 receiver 和方法名 selector,執(zhí)行過程可以簡單概括為:

  1. 獲取 receiver 對應(yīng)的類 Class
  2. 在 Class 緩存列表中根據(jù)選擇子 selector 查找 IMP
  3. 若緩存中沒有找到咐低,則在方法列表中繼續(xù)查找
  4. 若方法列表沒有揽思,則從父類查找,重復(fù)以上步驟
  5. 若最終沒有找到见擦,則進(jìn)行消息轉(zhuǎn)發(fā)操作

objc_msgSend 匯編源碼內(nèi)部邏輯

    ENTRY _objc_msgSend     // _objc_msgSend 入口
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check, cmp 指令執(zhí)行完钉汗,設(shè)置Z-flag(零標(biāo)志)
    b.le    LNilOrTagged        // 如果 x0 的值==0,CPSR寄存器的 Z 標(biāo)識==1鲤屡,跳轉(zhuǎn)標(biāo)簽判斷是否 self 是否為 nil 或者是 tagged pointer 類型
                                //  跳轉(zhuǎn)之前 lr 寄存器會保存 pc 寄存器當(dāng)前內(nèi)容
    ldr x13, [x0]       // x13 = isa损痰,把 self 指針賦值到 x13,self 是 objc_object 結(jié)構(gòu)體酒来,結(jié)構(gòu)體第一個(gè)屬性是 isa卢未,所以這里 x13 指向了 isa
    and x9, x13, #ISA_MASK  // x9 = class,與運(yùn)算來移除掉這些多余的信息堰汉,將一個(gè)真實(shí)指向類的指針保存在 x9 里    
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:           // 執(zhí)行到這里說明 self 的值等于 0辽社。小于零則代表為 Tagged Pointer 情況,等于說明為 nil
    b.eq    LReturnZero     // nil check衡奥,判斷 self 是否為 nil

    // tagged
    // 這里加載了 _objc_debug_taggedpointer_classes 的地址爹袁,即 Tagged Pointer 主表
    // ARM64 需要兩條指令來加載一個(gè)符號的地址。這是 RISC 樣架構(gòu)上的一個(gè)標(biāo)準(zhǔn)技術(shù)矮固。
    // AMR64 上的指針是 64 位寬的失息,指令是 32 位寬。所以一個(gè)指令無法保存一個(gè)完整的指針
    
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE     // 將頁(前半部分)的基址存在 x10
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 將頁(后半部分)的基址存在 x10
    ubfx    x11, x0, #60, #4    // 它從 self 中的第 60 位開始档址,提取 4 位盹兢,保存到 x11 中。
    ldr x9, [x10, x11, LSL #3]  // x9 = x10 + (x11<<3)守伸,這里通過 x11 里的索引到 x10 所指向的 Tagged Pointer 表中查找具體的類
    b   LGetIsaDone

LReturnZero:
    // x0 is already zero
    // 因?yàn)榻鼇碇耙呀?jīng)通過 `cmp x0, #0` 判斷绎秒,所以 x0 寄存器的值是0

    // 整型的返回值保存在 x0 和 x1 中
    // 浮點(diǎn)型的返回值會被保存在 v0 到 v3 這幾個(gè)向量寄存器中,
    // d0 到 d3這幾個(gè)寄存器是相關(guān)v寄存器的后半部分尼摹,向他們存值的時(shí)候會將對應(yīng) v 寄存器的前半部分置 0
    
    mov x1, #0      // 1见芹、首先先把 x1 清空剂娄,x0 這里是 self,已經(jīng)是0玄呛,所以不需要清空阅懦,
    movi    d0, #0  // 2、清空 v 寄存器
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend

_objc_msgSend 函數(shù)可以分解2個(gè)主線

  1. receiver 為 nil 或者屬于 tagged pointer 類型
  2. receiver 不為空徘铝,正常查找 IMP

receiver 不為空

首先先分析 receiver 不為空的情況

1. ldr  x13, [x0]
2. and  x9, x13, #ISA_MASK  
3. LGetIsaDone:
        CacheLookup NORMAL
  1. x0 當(dāng)前存儲的是 self 指針耳胎,ldr 指令 self 指針?biāo)赶虻膬?nèi)存位置讀取數(shù)據(jù)并保存到 x13 寄存器中,這時(shí)候 x13 存儲了 isa

  2. isa 和 ISA_MASK 做與運(yùn)算惕它,移除掉這些多余的信息得到 Class 并存儲到 x9

  3. 開始從 Class 緩存中查找 IMP

CacheLookup 是一個(gè)宏

.macro CacheLookup
    // x1 = SEL, x9 = isa
    // x9 保存著 objc_class 指針
    
    ...
    
.endmacro

進(jìn)入宏之前怕午,x1 保存了 SEL (ARM寄存器 x0-x7 寄存器是用來傳遞參數(shù)的,objc_msgSend 函數(shù)的前2個(gè)參數(shù)分別是 self 和 _cmd)淹魄,還有之前處理得到的 isa 保存在 x9郁惜。

    ldp x10, x11, [x9, #CACHE]  // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)
  1. ldp x10, x11, [x9, #CACHE]:CACHE 是一個(gè)常數(shù)(0x10),以 objc_class 地址為基準(zhǔn)揭北,然后讀 16 字節(jié)的數(shù)據(jù)(可以參考 objc-runtime-new.h objc_class 結(jié)構(gòu)體)扳炬,x10 = buckets,x11 = occupied|mask (高32位:occupied搔体,低32位:mask);

    mask 代表哈希表的位數(shù)半醉,它的值總是2 - 1的冪疚俱,或者用二進(jìn)制表示就是000000001111111,末尾有一個(gè)可變的1

  2. and w12, w1, w11:進(jìn)行 AND 運(yùn)算缩多,得到選擇子的查詢索引

  3. add x12, x10, x12, LSL #4: x12 左移4位也就是乘以16呆奕,這是因?yàn)槊總€(gè)哈希表的 bucket 是 16 字節(jié),計(jì)算得出要搜索位置的 第一個(gè) bucket 的地址并保存在 x12

    ldp x16, x17, [x12]     // {x16, x17} = *bucket
1:  cmp x16, x1         // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->cls == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x16, x17, [x12, #-16]!
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)
  1. ldp x16, x17, [x12]: 從 bucket 指針指向的內(nèi)存地址讀取數(shù)據(jù)衬吆,x16 存儲要查找 bucket 中的 key(選擇子)梁钾,x17 存儲了 IMP

  2. cmp x16, x1: 判斷第一個(gè) bucket 中的 sel 跟參數(shù) _cmd 是否相同

    • 相同: 跳轉(zhuǎn)到 CacheHit 繼續(xù)執(zhí)行,改標(biāo)簽中會執(zhí)行指令 br x17逊抡,也就是執(zhí)行 IMP
    • 不相同: 跳轉(zhuǎn)到 CheckMiss 繼續(xù)執(zhí)行 cbz x16, __objc_msgSend_uncached_impcache姆泻,cbz 指令比較寄存器值是否等于0,如果是0則跳轉(zhuǎn)冒嫡;這里 x16 中記錄了從 bucket 加載到的選擇子拇勃。首先先將其與 0 進(jìn)行比較,如果等于 0 則會跳轉(zhuǎn)至 C 函數(shù) __objc_msgSend_uncached_impcache 進(jìn)行更復(fù)雜的查找
  3. cmp x12, x10: 判斷當(dāng)前的 bucket 指針是不是和數(shù)組 buckets 指針相同孝凌,相同則說明在列表頭

  4. 判斷當(dāng)前 bucket 的位置:

    1. 如果 bucket == buckets方咆,則把指針指向 buckets 列表尾

      b.eq  3f
      add   x12, x12, w11, UXTW #4
      ldp   x16, x17, [x12] 
      

      cmp 指令執(zhí)行之后如果,如果 x12 - x10 == 0蟀架,csrp 寄存器 Z 標(biāo)志位置位1瓣赂,反之為0榆骚。

      b.eq 當(dāng) Z 標(biāo)志位為 1,跳轉(zhuǎn)到 3f煌集,執(zhí)行 add x12, x12, w11, UXTW #4

      x12 存儲了 buckets 指針寨躁,指向了第一個(gè) bucket,w11 是存儲表的掩碼牙勘,描述了表的大小职恳,相加之后當(dāng)前指針指向最后一個(gè) bucket

    2. 如果 bucket != buckets,

      ldp   x16, x17, [x12, #-16]!
      b 1b          // loop
      

      x12-16 獲取新的 bucket 地址并重新寫入到 x12 中 (!符號代表寄存器回寫)方面,指向前一個(gè) bucket放钦,x16 存儲要查找 bucket 中的 key(選擇子 ,x17 存儲了 IMP恭金,然后重復(fù)之前的步驟

接著上面的4.1步驟操禀,bucket 指針指向 buckets 列表尾

1:  cmp x16, x1         // if (bucket->sel != _cmd)
        b.ne    2f          //     scan more
        CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
        CheckMiss $0            // miss if bucket->cls == 0
        cmp x12, x10        // wrap if bucket == buckets
        b.eq    3f
        ldp x16, x17, [x12, #-16]!  // {x16, x17} = *--bucket
        b   1b          // loop

3:  // double wrap
        JumpMiss $0
  1. 判斷當(dāng)前 bucket 的選擇子和傳入?yún)?shù) _cmd 是否相同,相同則跳轉(zhuǎn)到 CacheHit 執(zhí)行對應(yīng)的 IMP横腿,不相同則往下走
  2. 執(zhí)行 CheckMiss 宏判斷 bucket 的選擇子是否為空颓屑,若未空跳轉(zhuǎn)執(zhí)行 __objc_msgSend_uncached_impcache C 函數(shù)
  3. cmp x12, x10 ,檢查是否在 buckets 表頭循環(huán)搜索完 或者 是hash碰撞耿焊,如果是則跳轉(zhuǎn)到 JumpMiss揪惦,最終會執(zhí)行 __objc_msgSend_uncached_impcache 函數(shù)執(zhí)行,進(jìn)行更復(fù)雜的查找
  4. 若步驟3不成立罗侯,則 bucket 指針前移器腋,重復(fù)1-3的步驟

recever 不為空的情況下, objc_msgSend 全部過程分析到此完畢

receiver 等于 nil

LNilOrTagged:   // 執(zhí)行到這里說明 self 的值小于等于 0钩杰。小于零則代表為 Tagged Pointer 情況纫塌,等于說明為 nil
    b.eq    LReturnZero     // nil check,判斷 self 是否為 nil

objc_msgSend 開始會將利用 cmp 指令將 receiver 和 0 做比較讲弄,若結(jié)果是小于等于0則會跳轉(zhuǎn)到 LNilOrTagged 執(zhí)行措左。

若 receiver == 0,則跳轉(zhuǎn)到 LReturnZero

LReturnZero:
    // x0 is already zero
    // 因?yàn)榻鼇碇耙呀?jīng)通過 `cmp x0, #0` 判斷避除,所以 x0 寄存器的值是0

    // 整型的返回值保存在 x0 和 x1 中
    // 浮點(diǎn)型的返回值會被保存在 v0 到 v3 這幾個(gè)向量寄存器中怎披,
    // d0 到 d3這幾個(gè)寄存器是相關(guān)v寄存器的后半部分,向他們存值的時(shí)候會將對應(yīng) v 寄存器的前半部分置 0
    
    mov x1, #0      // 1驹饺、首先先把 x1 清空钳枕,x0 這里是 self,已經(jīng)是0赏壹,所以不需要清空鱼炒,
    movi    d0, #0  // 2、清空 v 寄存器
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

這里先后把整形寄存器和向量寄存器都置為0蝌借,這樣做的好處是:

objc_msgSend 不知道調(diào)用者希望獲得什么類型的返回值昔瞧,是一個(gè)整型指蚁?兩個(gè)组橄?還是浮點(diǎn)類型或是其他類型钳踊?把所有返回值的寄存器都覆蓋為0尚卫,后面調(diào)用者不管是想得到整型還是浮點(diǎn)型褥符,都是0值。

那么如果調(diào)用者需要的返回值類型不是屬于整型/浮點(diǎn)型嵌赠,比如是寄存器不夠存儲的畅买,更大結(jié)構(gòu)的返回值需要調(diào)用者在內(nèi)存中分配合適的內(nèi)存空間并把內(nèi)存地址傳入 x8同仆,函數(shù)通過寫入這塊內(nèi)存來返回值混巧。

objc_msgSend 執(zhí)行過程中并不知道 x8 內(nèi)存枪向,所以在 LReturnZero 中并沒有清除內(nèi)存。解決辦法是編譯器生成代碼會 objc_msgSend 執(zhí)行前用0填滿這塊內(nèi)存咧党。

Tagged pointer 處理

Tagged Pointer 通過在其最后一個(gè) bit 位設(shè)置一個(gè)特殊標(biāo)記秘蛔,用于將數(shù)據(jù)直接保存在指針本身中。具體細(xì)節(jié)可以參考深入理解 Tagged Pointer

// tagged
// 這里加載了 _objc_debug_taggedpointer_classes 的地址傍衡,即 Tagged Pointer 主表
// ARM64 需要兩條指令來加載一個(gè)符號的地址深员。這是 RISC 樣架構(gòu)上的一個(gè)標(biāo)準(zhǔn)技術(shù)。
// AMR64 上的指針是 64 位寬的蛙埂,指令是 32 位寬倦畅。所以一個(gè)指令無法保存一個(gè)完整的指針

adrp    x10, _objc_debug_taggedpointer_classes@PAGE     // 將頁(前半部分)的基址存在 x10
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 將頁(后半部分)的基址存在 x10
ubfx    x11, x0, #60, #4    // 它從 self 中的第 60 位開始,提取 4 位箱残,保存到 x11 中滔迈。
ldr x9, [x10, x11, LSL #3]  // x9 = x10 + (x11<<3)
b   LGetIsaDone
  1. 通過 adrp 指令計(jì)算 _objc_debug_taggedpointer_classes 表(存儲可用的 Tagged Pointer 的類)的數(shù)據(jù)地址到當(dāng)前pc寄存器值相對偏移。
  2. AMR64 上的指針是 64 位寬的被辑,指令是 32 位寬。所以一個(gè)指令無法保存一個(gè)完整的指針敬惦,于是還要通過 add 指令把后半部分讀取存儲到 x10
  3. ubfx 指令讀取 x11 中最后4位數(shù)據(jù)盼理,也就是 Tagged pointer 表所以標(biāo)志
  4. x9 = x10 + (x11<<3),這里通過 x11 里的索引到 x10 所指向的 Tagged pointer 表中查找具體的 Tagged pointer 類
  5. 獲取到 isa 之后進(jìn)行 CacheLookup 步驟

自此 objc_msgSend 過程已經(jīng)全部分析完俄删,整個(gè)流程可以用下圖表示:

[圖片上傳失敗...(image-e392dd-1563026243771)]

為什么要用匯編實(shí)現(xiàn)

objc_msgSend 函數(shù)實(shí)現(xiàn)并不是用 Objective-C宏怔、C 或者 C++ 實(shí)現(xiàn)的,而是利用匯編語言開發(fā)畴椰。

那為什么會采用匯編語言實(shí)現(xiàn)呢臊诊?首先看一個(gè)例子:

NSUInteger n = [array count];
id obj = [array objectAtIndex:1];

我們可以理解上面2行代碼編譯時(shí)期會轉(zhuǎn)化為:

NSUInteger n = objc_msgSend(array,  @selector(count));
id obj = objc_msgSend(array, @selector(objectAtIndex:), 1);

假設(shè) objc_msgSend 是 C 或者 C++ 實(shí)現(xiàn)的,這里不可能編譯成功斜脂,因?yàn)榉祷刂狄膊荒芡瑫r(shí)是 NSUIntegerid抓艳;這里可以使用類型強(qiáng)制轉(zhuǎn)化來解決:

NSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array,  @selector(count));
id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6);

從例子上可以看的出,objc_msgSend 有2個(gè)特點(diǎn):

  1. 可以調(diào)用任意參數(shù)類型帚戳、數(shù)量的任意函數(shù)
  2. 支持不同類型的返回值

對于特點(diǎn)1玷或,調(diào)用 objc_msgSend 的之前儡首,棧幀(stack frame)的狀態(tài)、數(shù)據(jù)偏友,和各個(gè)寄存器的組合形式蔬胯、數(shù)據(jù),跟調(diào)用具體的函數(shù)指針(IMP)時(shí)所需的狀態(tài)位他、數(shù)據(jù)氛濒,是完全一致的。

基于這個(gè)前提鹅髓,遍歷并找到 IMP 之后舞竿,只要所有的對棧、寄存器的操作回復(fù)到調(diào)用 objc_msgSend 之前的狀態(tài)迈勋,通過 jump/call 指令執(zhí)行函數(shù)即可炬灭。

在 ARM 上,IMP 函數(shù)執(zhí)行完靡菇, r0 寄存器會保存其返回值重归,能滿足其返回不同類型返回值的需求

參考文章:

為什么objc_msgSend必須用匯編實(shí)現(xiàn)

Dissecting objc_msgSend on ARM64

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市厦凤,隨后出現(xiàn)的幾起案子鼻吮,更是在濱河造成了極大的恐慌,老刑警劉巖较鼓,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椎木,死亡現(xiàn)場離奇詭異,居然都是意外死亡博烂,警方通過查閱死者的電腦和手機(jī)香椎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來禽篱,“玉大人畜伐,你說我怎么就攤上這事√陕剩” “怎么了玛界?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長悼吱。 經(jīng)常有香客問我慎框,道長,這世上最難降的妖魔是什么后添? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任笨枯,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猎醇。我一直安慰自己窥突,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布硫嘶。 她就那樣靜靜地躺著阻问,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沦疾。 梳的紋絲不亂的頭發(fā)上称近,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機(jī)與錄音哮塞,去河邊找鬼刨秆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛忆畅,可吹牛的內(nèi)容都是我干的衡未。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼家凯,長吁一口氣:“原來是場噩夢啊……” “哼缓醋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绊诲,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤送粱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后掂之,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抗俄,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年世舰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了动雹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跟压,死狀恐怖洽胶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情裆馒,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布丐怯,位于F島的核電站喷好,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏读跷。R本人自食惡果不足惜梗搅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧无切,春花似錦荡短、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至籍嘹,卻和暖如春闪盔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辱士。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工泪掀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颂碘。 一個(gè)月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓异赫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親头岔。 傳聞我的和親對象是個(gè)殘疾皇子塔拳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

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

  • 概覽 每個(gè)Objective-C對象都有相應(yīng)的類,這個(gè)類都有一個(gè)方法列表切油。類中的每個(gè)方法都有一個(gè)選擇子蝙斜、一個(gè)指向方...
    alvin_wang閱讀 760評論 0 0
  • objc_msgSend是OC中調(diào)用最為頻繁的方法,所有OC方法的調(diào)用都離不開這個(gè)它澎胡。蘋果已經(jīng)將其開源(https...
    某某香腸閱讀 766評論 0 0
  • 一.objc_msgSend函數(shù)簡介 以前去面試孕荠,有人問了這個(gè)一個(gè)問題 發(fā)生了什么?一聽這個(gè)問題攻谁,一臉懵逼稚伍。這不就...
    充滿活力的早晨閱讀 476評論 0 2
  • 天早已進(jìn)入冬季,深圳的天氣也開始微微涼了戚宦,聽說个曙,老家已經(jīng)棉襖加秋褲,等待著過冬受楼。深圳的氣候太過于舒適垦搬,導(dǎo)致我一直都...
    殷勤說閱讀 293評論 0 0
  • 茨威格的《列夫*托爾斯泰》是八年級下冊第一單元的課文,之前也講過一次艳汽,只是沒怎么細(xì)致地分析猴贰,利用。今天讀來河狐,感覺實(shí)...
    骕棋朋薇閱讀 1,884評論 0 2