iOS 探索isa

一、OC對象的本質(zhì)是什么?

可能有很多同學都知道答案,即對象的本質(zhì)是結(jié)構(gòu)體。但是怎么證明呢?今天我們就來一起驗證下。

1、clang編譯器

Clang 是?個由 Apple 主導編寫,基于LLVMC/C++/Objective-C編譯器 。
我們主要是將其用于底層編譯,將一些文件``輸出成c++文件,例如main.m輸出成main.cpp,其目的是為了更好的觀察底層的一些結(jié)構(gòu) 及 實現(xiàn)的邏輯,方便理解底層原理。
常見的使用方式如下:

//1、將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、將 ViewController.m 編譯成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下兩種方式是通過指定架構(gòu)模式的命令行,使用xcode工具 xcrun
//3、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真機文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

2、對象的本質(zhì)

接下來我們就來使用一下:

1)定義一個類LPPerson,LPPerson有一個屬性name

#import <Foundation/Foundation.h>
#import <objc/runtime.h>


@interface LPPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LPPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

2)接下來我們運用clangmain.m編譯成C++文件main.cpp

在終端中進入main.m所在目錄,并使用clang -rewrite-objc main.m -o main.cpp命令即可:

image.png

可以再main.m的目錄中發(fā)現(xiàn)main.cpp,雙擊打開main.cpp

image.png

image.png

可以看到,編譯出來的main.mm文件很大,11萬多行,看著就很懵逼是不是?其實我們不用看所有的代碼,記得我們剛才申明了LPPerson嗎?我們在代碼里面搜索下,找到如下代碼:

#ifndef _REWRITER_typedef_LPPerson
#define _REWRITER_typedef_LPPerson
typedef struct objc_object LPPerson;
typedef struct {} _objc_exc_LPPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LPPerson$_name;
struct LPPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};

// @property (nonatomic, copy) NSString *name;
/* @end */


// @implementation LPPerson

static NSString * _I_LPPerson_name(LPPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LPPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LPPerson_setName_(LPPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LPPerson, _name), (id)name, 0, 1); }
// @end

可以看到OC中的LPPerson變成了struct LPPerson_IMPL,這也就印證了OC對象是結(jié)構(gòu)體。
但是NSObject_IMPL是什么東東?struct NSObject_IMPL NSObject_IVARS;這個又是什么意思?搜索一下NSObject_IMPL,會發(fā)現(xiàn)有很多,無法定位,那我們直接搜索這個結(jié)構(gòu)的定義struct NSObject_IMPL {,結(jié)果出現(xiàn)了:

struct NSObject_IMPL {
    Class isa;
};

在結(jié)合Objc源碼中對NSObject的定義:

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

我們可以得到結(jié)論:

  • NSObject的底層實現(xiàn)實質(zhì)是一個結(jié)構(gòu)體
  • 結(jié)構(gòu)體中的第一個成員是 isa指針,并且isaClass類型,因為LPPerson中的第一個屬性NSObject_IVARS等效于 NSObject中的 isa

3、探索objc_setProperty原理

除了LPPersong的底層定義,我們發(fā)現(xiàn)還有屬性name對應(yīng)的setget方法,如下圖所示,其中set方法的實現(xiàn)依賴于runtime中的objc_setProperty:

image.png

所以我們接下來了解下objc_setProperty,在Objc源碼中查找objc_setProperty

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

再進入reallySetProperty中:

image.png

我們可以發(fā)現(xiàn)其方法的原理就是retain新值,release舊值,基于這段源碼我們可以了解到:

  • objc_setProperty方法的目的適用于關(guān)聯(lián) 上層的set方法 以及 底層的set方法,其本質(zhì)就是一個接口。

  • 這么設(shè)計的原因是,上層的set方法有很多,如果直接調(diào)用底層set方法中,會產(chǎn)生很多的臨時變量,當你想查找一個sel時,會非常麻煩

  • 基于上述原因,蘋果采用了適配器設(shè)計模式(即將底層接口適配為客戶端需要的接口),對外提供一個接口,供上層的set方法使用,對內(nèi)調(diào)用底層的set方法,使其相互不受影響,即無論上層怎么變,下層都是不變的,或者下層的變化也無法影響上層,主要是達到上下層接口隔離的目的

二、探索isa

在探索isa之前我們先了解點預備知識:

1、聯(lián)合體和結(jié)構(gòu)體:

構(gòu)造數(shù)據(jù)類型的方式有以下兩種:

1)結(jié)構(gòu)體(struct

結(jié)構(gòu)體是指把不同的數(shù)據(jù)組合成一個整體,其變量是共存的,變量不管是否使用,都會分配內(nèi)存。

  • 優(yōu)點:存儲容量較大,包容性強,且成員之間不會相互影響
  • 缺點:所有屬性都分配內(nèi)存,比較浪費內(nèi)存,假設(shè)有4個int成員,一共分配了16字節(jié)的內(nèi)存,但是在使用時,你只使用了4字節(jié),剩余的12字節(jié)就是屬于內(nèi)存的浪費

2)聯(lián)合體(union,也稱為共用體)

聯(lián)合體也是由不同的數(shù)據(jù)類型組成,但其變量是互斥的,所有的成員共占一段內(nèi)存。而且共用體采用了內(nèi)存覆蓋技術(shù),同一時刻只能保存一個成員的值,如果對新的成員賦值,就會將原來成員的值覆蓋掉

  • 優(yōu)點:所有成員共用一段內(nèi)存,使內(nèi)存的使用更為精細靈活,同時也節(jié)省了內(nèi)存空間
  • 缺點:包容性弱

3)兩者的區(qū)別:

  • 內(nèi)存占用情況

結(jié)構(gòu)體:各個成員會占用不同的內(nèi)存,互相之間沒有影響
聯(lián)合體:所有成員占用同一段內(nèi)存,修改一個成員會影響其余所有成員

  • 內(nèi)存分配大小

結(jié)構(gòu)體內(nèi)存: 所有成員占用的內(nèi)存總和(成員之間可能會有縫隙)
聯(lián)合體:占用的內(nèi)存等于最大的成員占用的內(nèi)存

2、isa_t

isa的類型是isa_t,我們再Objc中查看isa_t源碼:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

明顯發(fā)現(xiàn),isa_t實際上就是一個聯(lián)合體。
isa_t類型使用聯(lián)合體的原因也是基于內(nèi)存優(yōu)化的考慮,這里的內(nèi)存優(yōu)化是指在isa指針中通過char + 位域(即二進制中每一位均可表示不同的信息)的原理實現(xiàn)。通常來說,isa指針占用的內(nèi)存大小是8字節(jié),即64位,已經(jīng)足夠存儲很多的信息了,這樣可以極大的節(jié)省內(nèi)存,以提高性能

isa_t的定義中可以看出:

提供了兩個成員,clsbits,由聯(lián)合體的定義所知,這兩個成員是互斥的,也就意味著,當初始化isa指針時,有兩種初始化方式:

  • 通過cls初始化,bits無默認值
  • 通過bits初始化,cls有默認值

還提供了一個結(jié)構(gòu)體定義的位域,用于存儲類信息及其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD,這是一個宏定義,有兩個版本 __arm64__(對應(yīng)ios 移動端) 和 __x86_64__(對應(yīng)macOS),以下是它們的一些宏定義,如下圖所示

#   define ISA_BITFIELD                                                        \
        //針對isa指針的指針優(yōu)化
      uintptr_t nonpointer        : 1;                                        \
        //是否被關(guān)聯(lián)
      uintptr_t has_assoc         : 1;                                   \
        //是否有C++相關(guān)實現(xiàn)
      uintptr_t has_cxx_dtor      : 1;                                        \
        //存儲信息
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
        //對象是否已經(jīng)初始化
      uintptr_t magic             : 6;                                       \
        //是否弱引用
      uintptr_t weakly_referenced : 1;                                        \
        //是否被釋放
      uintptr_t deallocating      : 1;                                         \
        //是否有散列表
      uintptr_t has_sidetable_rc  : 1;                                        \
        //是否有額外引用計數(shù)
      uintptr_t extra_rc          : 8

具體意義:

nonpointer有兩個值,表示自定義的類等,占1位
  • 0:純isa指針
  • 1:不只是類對象地址,isa中包含了類信息、對象的引用計數(shù)等
has_assoc表示關(guān)聯(lián)對象標志位,占1位
  • 0:沒有關(guān)聯(lián)對象
  • 1:存在關(guān)聯(lián)對象
has_cxx_dtor表示該對象是否有C++/OC的析構(gòu)器(類似于dealloc),占1位
  • 如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯
  • 如果沒有,則可以更快的釋放對象
shiftclx表示存儲類的指針的值(類的地址), 即類信息
  • arm64中占 33位,開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來存儲類指針
  • x86_64中占 44位
magic 用于調(diào)試器判斷當前對象是真的對象 還是 沒有初始化的空間,占6位
weakly_refrenced是 指對象是否被指向 或者 曾經(jīng)指向一個ARC的弱變量
  • 沒有弱引用的對象可以更快釋放
deallocating 標志對象是是否正在釋放內(nèi)存
has_sidetable_rc表示 當對象引用計數(shù)大于10時,則需要借用該變量存儲進位
extra_rc(額外的引用計數(shù)) --- 表示該對象的引用計數(shù)值,實際上是引用計數(shù)值減1
  • 如果對象的引用計數(shù)為10,那么extra_rc為9,
  • 如果引用計數(shù)大于10,則需要使用到下面的has_sidetable_rc

isa的創(chuàng)建

接下里我們來看下isa是如何創(chuàng)建的,在Objc源碼中直接搜索initInstanceIsa或者,剛才搜索isa_t的時候,第一個出現(xiàn)的結(jié)果就是initInstanceIsa中:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);
#if SUPPORT_INDEXED_ISA   //等效于!nonpointer
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else                   //等效于nonpointer
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

這里也證明了我們之前說的cls和Bits是互斥的,所以isa的初始化會二選一:

  • 通過cls初始化isa
  • 通過bits初始化isa

三、isa和類的關(guān)聯(lián)

clsisa關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲了類信息,其中initInstanceIsa的過程是將calloc指針和當前的類cls關(guān)聯(lián)起來了。

覺得不錯記得點贊哦!聽說看完點贊的人逢考必過,逢獎必中。?( ′???` )比心

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容