一、OC對象的本質(zhì)是什么?
可能有很多同學都知道答案,即對象的本質(zhì)是結(jié)構(gòu)體。但是怎么證明呢?今天我們就來一起驗證下。
1、clang編譯器
Clang 是?個由 Apple 主導編寫,基于LLVM的C/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)接下來我們運用clang將main.m編譯成C++文件main.cpp
在終端中進入main.m所在目錄,并使用clang -rewrite-objc main.m -o main.cpp命令即可:

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


可以看到,編譯出來的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指針,并且
isa是Class類型,因為LPPerson中的第一個屬性NSObject_IVARS等效于NSObject中的isa
3、探索objc_setProperty原理
除了LPPersong的底層定義,我們發(fā)現(xiàn)還有屬性name對應(yīng)的set和 get方法,如下圖所示,其中set方法的實現(xiàn)依賴于runtime中的objc_setProperty:

所以我們接下來了解下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中:

我們可以發(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的定義中可以看出:
提供了兩個成員,cls和 bits,由聯(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)
cls與isa關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲了類信息,其中initInstanceIsa的過程是將calloc指針和當前的類cls關(guān)聯(lián)起來了。
覺得不錯記得點贊哦!聽說看完點贊的人逢考必過,逢獎必中。?( ′???` )比心