[iOS]深入理解__bridge-OC物件與C++物件的引用轉換

知兵發表於2016-08-06

0x0 crash

昨天在iOS Geekers釘釘群裡聊到一個問題, 下面的程式碼會crash:

    void* a = new char;
    id ext = (__bridge id)a;

crash現場如圖:
screenshot

看看掛的地方:
screenshot

掛在objc_retain裡面:
screenshot

objc_retain的作用是對oc物件做retain用的, 我們對指令做一下簡單的解析:

libobjc.A.dylib`objc_retain:
    0x1810d00a0 <+0>:  cbz    x0, 0x1810d00c8           ; <+40>    // 判斷x0也就是傳進來的第一個引數是不是nil, 在這裡x0是變數a, 也就是char型別的指標
    0x1810d00a4 <+4>:  tbnz   x0, #63, 0x1810d00cc      ; <+44>    // 判斷x0暫存器的第63位是不是0
    0x1810d00a8 <+8>:  ldr    x8, [x0]                             // 取x0指標的內容放入x8, 正常情況下這裡是oc物件的isa, 傳進來的並不是oc物件, 沒有isa. 這裡取出來的是0x0, 見下圖
    0x1810d00ac <+12>: and    x8, x8, #0xffffffff8                 // x8`與操作`0xffffffff8(ISA_MASK), `與`完還是0
->  0x1810d00b0 <+16>: ldrb   w8, [x8, #32]                        // 取x8為基地址偏移量32的記憶體內容. 也就是x8+0x20, 也就是0+0x20=0x20. 0x20是一個保留地址不可讀寫, 直接掛!

screenshot

0x1 解決

那麼問題來, 為什麼這裡會有一次retain操作導致掛掉呢? 看看程式碼:

   id ext = (__bridge id)a;

id ext, 這種寫法隱含了__strong id ext, ext對a做了一次強引用, 而強引用就會對被引用的物件做一次retain.

那我們就不強引用就好了:

    void* a = new char;
    __unsafe_unretained id ext = (__bridge id)a;

那這樣就僅僅只是把a指標賦值給了ext指標, 並沒有做強引用不會觸發retain而導致crash, 不過a作為一個c++物件, 記憶體管理自己要做好!

0x2 用起來

看如下程式碼:

void test_bridge_parameter (id p0) {
    
}

void test_bridge() {
    void* a = new char;
    __unsafe_unretained id ext = (__bridge id)a;
    test_bridge_parameter(ext);
}

這裡我們把ext作為引數, 傳遞給了test_bridge_parameter. 你覺得這段程式碼能夠正確執行麼?

當然不能!!!
screenshot

臥槽, 咋掛函式的第0行了? 看看除錯:
screenshot
screenshot
這裡呼叫了 objc_storeStrong, 而objc_storeStrong又呼叫了objc_retain, 也就是我們傳進來的c++物件ext又被retain了. 為什麼呢?

objc_storeStrong 有兩個引數 p0, p1, 作用是把p1賦給p0並強引用一次.

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

在函式呼叫的引數傳遞時, 會把傳進來的引數, 按照參數列裡的定義的屬性, 做相關的賦值.

void test_bridge_parameter (id p0)

裡面的id p0, 同樣隱含__strong id p0, 改成

void test_bridge_parameter (__unsafe_unretained id p0)

去掉引數賦值時的強引用即可.

0x3 CFTypeRef和OC物件的關係

前面的例子中 char類並不是一個CFTypeRef型別, 導致並不能被轉換為OC物件:

/* Base "type" of all "CF objects", and polymorphic functions on them */
typedef const CF_BRIDGED_TYPE(id) void * CFTypeRef;

typedef const struct CF_BRIDGED_TYPE(NSString) __CFString * CFStringRef;

那麼CFTypeRef型別的物件是怎麼轉為OC物件的呢?
來一段程式碼:

void test_bridge() {
    CFStringRef helloCF = CFSTR("hello, world");
}

反彙編:

testbridge`test_bridge:
    0x10000ac78 <+0>:  sub    sp, sp, #16               ; =16 
    0x10000ac7c <+4>:  adrp   x8, 2
    0x10000ac80 <+8>:  add    x8, x8, #32               ; =32 
    0x10000ac84 <+12>: str    x8, [sp, #8]
->  0x10000ac88 <+16>: add    sp, sp, #16               ; =16 
    0x10000ac8c <+20>: ret   

bridge一下, 為了避免__strong帶來的多餘指令, 這裡用__unsafe_unretained來避免:

void test_bridge() {
    CFStringRef helloCF = CFSTR("hello, world");
    __unsafe_unretained NSString *helloNS = (__bridge id)helloCF;
}

反彙編:

testbridge`test_bridge:
    0x1000f2c70 <+0>:  sub    sp, sp, #16               ; =16 
    0x1000f2c74 <+4>:  adrp   x8, 2
    0x1000f2c78 <+8>:  add    x8, x8, #32               ; =32 
    0x1000f2c7c <+12>: str    x8, [sp, #8]
    0x1000f2c80 <+16>: ldr    x8, [sp, #8]
    0x1000f2c84 <+20>: str    x8, [sp]
->  0x1000f2c88 <+24>: add    sp, sp, #16               ; =16 
    0x1000f2c8c <+28>: ret    

對比兩段彙編, 會發現區別僅僅在與多出來兩調指令, 一條是把x8從棧偏移量8位置裡面弄出來, 另一條把x8扔到棧的偏移量0的位置, 僅僅只做了簡單的賦值, 而並沒有任何對資料進行任何的修改. 那不就意味著CFTypeRef和對應的OC型別的資料結構是一樣的?

screenshot

圖中兩者的內容都是”hello, world”, 但是資料型別卻不同!

我們看看sp(棧)的偏移量0和偏移量8的內容, 先用reg re讀出sp的地址:

   sp = 0x000000016fd13aa0

screenshot

圖中高亮的位置, 可以看出, 偏移量0和偏移量8的兩個64位地址裡面存的內容是一模一樣的(指向string物件的指標). 因吹絲挺!!!

並不是所有的類都可以無損轉換, 只有toll-free bridged types 才可以

0x4 __bridge_transfer 和 __bridge_retained

授人以魚不如受人以漁, 大家自己動手看看彙編程式碼差別! (其實是我懶… -_-!!)

切彙編程式碼除錯的方法是: Xcode頂部導航 -> Debug -> Debug Workflow -> Always Show Disassembly

__bridge在llvm文件的說明如下:

A bridged cast is a C-style cast annotated with one of three keywords:

    (__bridge T) op casts the operand to the destination type T. If T is a retainable object pointer type, then op must have a non-retainable pointer type. If T is a non-retainable pointer type, then op must have a retainable object pointer type. Otherwise the cast is ill-formed. There is no transfer of ownership, and ARC inserts no retain operations.
    (__bridge_retained T) op casts the operand, which must have retainable object pointer type, to the destination type, which must be a non-retainable pointer type. ARC retains the value, subject to the usual optimizations on local values, and the recipient is responsible for balancing that +1.
    (__bridge_transfer T) op casts the operand, which must have non-retainable pointer type, to the destination type, which must be a retainable object pointer type. ARC will release the value at the end of the enclosing full-expression, subject to the usual optimizations on local values.

0x5 參考

  1. objc_storeStrong: http://opensource.apple.com/source/objc4/objc4-647/runtime/NSObject.mm
  2. bridged casts: http://clang.llvm.org/docs/AutomaticReferenceCounting.html#bridged-casts
  3. toll-free bridged types: https://developer.apple.com/library/ios/documentation/CoreFoundation/Conceptual/CFDesignConcepts/Articles/tollFreeBridgedTypes.html


相關文章