日常咱們使用Objective-C語法來編寫代碼,可是它的底層其實都是C或C++代碼。Objective-C實際上是在C語言的基礎上增長了面向對象的特性。咱們能夠經過如下命令將Objective-C代碼轉換成C++代碼:ios
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC文件 -o 輸出目標cpp文件
複製代碼
若是OC文件須要連接其它的框架,可使用-framework參數:面試
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC文件 -o 輸出目標cpp文件 -framework 框架名稱
複製代碼
與此同時,還須要下載runtime的源碼,經過objc源碼地址下載最新版本的objc源碼,以便於後續使用。數組
在開發過程當中,最經常使用到的就是OC的對象。幾乎全部的類對象都是NSObject的子類,可是拋開OC的限制,NSObject底層是如何實現的呢?上文說到,全部的OC代碼最後都會轉換成C代碼,因此咱們經過一個例子來認識NSObject的底層實現。緩存
@interface XLPerson : NSObject
@end
@implementation XLPerson
@end
複製代碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson.m -o XLPerson_cpp.cpp
複製代碼
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
複製代碼
在XLPerson_IMPL中包含一個結構體成員NSObject_IVARS,它是NSObject_IMPL類型,查看NSObject_IMPL的代碼以下:bash
struct NSObject_IMPL {
Class isa;
};
複製代碼
由此能夠看出,OC中的對象其實就是經過結構體來實現的。在NSObject_IMPL包含了一個Class類型的成員isa。繼續查看Class的定義:app
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
複製代碼
能夠發現其實Class就是一個objc_class類型的結構體指針。在最新的objc4的源碼中的objc-runtime-new.h文件中,能夠找到最新的objc_class的定義以下:框架
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();
}
}
複製代碼
objc_class繼承自結構體objc_object,而結構體objc_object的具體定義以下,內部只有一個isa指針iphone
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
複製代碼
因爲繼承關係,結構體objc_class天然也就繼承了objc_object的isa指針,因此objc_class也能夠轉換成以下寫法:函數
struct objc_class {
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();
}
}
複製代碼
查看class_data_bits_t的具體實現以下:學習
//此處只列出核心的代碼
struct class_data_bits_t {
......
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
......
}
複製代碼
這時候發現了經過bits的內部函數data()能夠拿到class_rw_t類型的數據,查看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; //只讀的屬性ro
method_array_t methods; //方法列表
property_array_t properties; //屬性列表
protocol_array_t protocols; //協議列表
Class firstSubclass;
Class nextSiblingClass;
}
複製代碼
在結構體class_rw_t中存放着
繼續查看class_ro_t的源碼以下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //當前instance對象佔用內存的大小
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;//基本屬性列表
}
複製代碼
此處就不得不說class_rw_t和class_ro_t的區別了,class_ro_t中存放着類最原始的方法列表,屬性列表等等,這些在編譯期就已經生成了,並且它是隻讀的,在運行期沒法修改。而class_rw_t不只包含了編譯器生成的方法列表、屬性列表,還包含了運行時動態生成的方法和屬性。它是可讀可寫的。至於class_rw_t和class_ro_t更深層次的區別,我會放在介紹runtime的時候詳細說明。
在iOS中通常使用以下[[NSObject alloc] init]建立對象,其中[NSObject alloc]就是爲NSObject分配內存空間,下面,咱們就從源碼入手,來理解OC對象是如何分配內存的。
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
複製代碼
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
複製代碼
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
......
}
複製代碼
能夠看出,其中真正用來分配內存的是C函數calloc,calloc函數傳入了兩個參數,第一個參數表示對象的個數,第二個參數size表示對象佔據的內存字節數。所以size就表示當前對象所須要的內存大小。
// May be unaligned depending on class's ivars. uint32_t unalignedInstanceSize() { assert(isRealized()); return data()->ro->instanceSize; } // Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
複製代碼
其中cls->unalignedInstanceSize()表示未進行內存對齊的內存大小,cls->alignedInstanceSize()是對未對齊的內存進行內存對齊操做,獲得最終所需的內存大小。
這裏有個細節,就是執行對齊操做獲得的內存大小若是小於16個字節,那麼最後分配的內存大小爲16個字節,也就是說,咱們建立對象時,分配的內存最少是16個字節。
在iOS中,咱們能夠經過三種方式來獲取一個對象的內存大小。
sizeof,它其實不是一個函數,而是一個運算符,它和宏定義相似,在編譯期就將傳入的類型轉換成具體的佔用內存的大小。例如int是4個字節,那麼sizeof(int)在編譯期就會直接被替換成4
注意:sizeof須要傳入一個類型過去,它返回的是一個類型所佔用的內存空間
class_getInstanceSize(Class _Nullable cls),傳入一個Class類型的對象就能獲得當前Class所佔用的內存大小。例如,class_getInstanceSize([NSObject class]),最後返回的是8,也就說明NSObject對象在內存中佔用8個字節,並且因爲NSObject最後會轉化成結構體NSObject_IMPL,並且內部只有一個isa指針,因此也就能夠理解爲isa指針佔用8個字節的存儲空間。
class_getInstanceSize函數內部其實就是調用alignedInstanceSize函數獲取到對象所須要的真實內存大小。
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
複製代碼
在調用calloc函數進行內存分配的時候,是將alignedInstanceSize的值看成參數賦值給calloc函數,所以calloc函數能夠有以下寫法:
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
size_t size = class_getInstanceSize(cls);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
......
}
複製代碼
由此能夠看出,class_getInstanceSize(Class _Nullable cls)所返回的實際上是對象實際所須要的內存大小。
malloc_size(const void *ptr)函數,傳入const void *類型的參數,就能夠獲取到當前操做系統所分配的內存大小。例如:仍是利用NSObject來進行測試,malloc_size((__bridge const void *)([[NSObject alloc] init])),將NSObject類型的實例對象做爲參數,最後獲得的值爲16,和咱們以前使用class_getInstanceSize([NSObject class])獲得的8不相同。
這是由於在iOS中,在分配內存時,若是對象所須要的內存大小小於16個字節,那麼就分配給這個對象16個字節的內存空間。也就是每一個對象至少分配16個字節的內存空間
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
複製代碼
@interface XLPerson : NSObject{
int _height;
int _age;
long _num;
}
複製代碼
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface XLPerson : NSObject{
@public
int _height;
int _age;
long _num;
}
@end
@implementation XLPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p = [[XLPerson alloc] init];
p->_height = 10;
p->_age = 20;
p->_num = 25;
NSLog(@"sizeof --> %lu", sizeof(p));
NSLog(@"class_getInstanceSize --> %lu", class_getInstanceSize([XLPerson class]));
NSLog(@"malloc_size --> %lu", malloc_size((__bridge const void *)(p)));
}
return 0;
}
複製代碼
能夠看出此時sizeof(p)返回8個字節,class_getInstanceSize返回24個字節,malloc_size則返回32個字節。3個方法返回的內存大小都不同,這是爲何呢?
其實sizeof(p)返回8個字節,這個很好理解,由於sizeof傳入的是p,而p在此處表示的是一個指向XLPerson實例對象的一個指針,在iOS中,指針類型所佔用的內存大小爲8個字節。所以sizeof(p)所返回的並非XLPerson對象的內存大小。
要想使用sizeof獲取到XLPerson對象的內存大小,就須要知道XLPerson最終會轉換成什麼類型。經過上文的學習,咱們知道,XLPerson內部實際上是一個結構體,經過xcrun指令將文件轉換成.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
複製代碼
分析main.cpp文件能夠得出,XLPerson最終會轉換成以下結構體類型
struct NSObject_IMPL {
Class isa;
};
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _height;
int _age;
long _num;
};
複製代碼
此時調用sizeof(struct XLPerson_IMPL)就能夠得出struct XLPerson_IMPL類型所佔用的內存爲24字節,其實也就是XLPerson所佔用的內存是24個字節。
由此可看出運算符sizeof(struct XLPerson_IMPL)和函數class_getInstanceSize([XLPerson class]返回的是對象真正所須要的內存大小。
在瞭解malloc_size函數以前,咱們先來分析一下XLPerson內部結構體所須要的真實內存大小。
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _height;
int _age;
long _num;
};
複製代碼
因此,單純從結構體層面分析的話,咱們能夠看出XLPerson_IMPL結構體所須要的內存是24個字節,這和上文的sizeof(struct XLPerson_IMPL)以及函數class_getInstanceSize([XLPerson class]返回的內存大小一致。因而可知,XLPerson所須要的內存就是24個字節。
但是爲何malloc_size所返回的內存大小確是32個字節呢?這就要說到內存對齊操做。
首先,咱們先將上文中提到的XLPerson屬性進行修改,去掉其中的_age屬性
@interface XLPerson : NSObject{
@public
int _height;
long _num;
}
@end
複製代碼
而後從新運行項目,能夠看到XLPerson實際佔用的內存仍是24個字節,而經過分析咱們能夠發現XLPerson只須要20個字節的內存空間。
這就是結構體內存對齊操做所致使的,也就是上文中所說的alignedInstanceSize函數的做用。那麼什麼是結構體的內存對齊操做?
結構體不像數組,結構體中能夠存放不一樣類型的數據,它的大小也不是簡單的各個數據成員大小之和,限於讀取內存的要求,而是每一個成員在內存中的存儲都要按照必定偏移量來存儲,根據類型的不一樣,每一個成員都要按照必定的對齊數進行對齊存儲,最後整個結構體的大小也要按照必定的對齊數進行對齊。
結構體的內存對齊規則以下:
這就是爲什麼XLPerson的內存大小爲24個字節的緣由。
既然XLPerson的內存佔用爲24個字節,那麼爲何系統會給它分配32個字節呢?其實在iOS系統中也存在內存對齊操做。
咱們能夠經過打印內存信息來查看是否分配了32個字節,依舊是使用上面的例子
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p = [[XLPerson alloc] init];
p->_height = 1;
p->_num = 3;
}
return 0;
}
複製代碼
(lldb) po p
<XLPerson: 0x1005b4fd0>
複製代碼
其中前8個字節存儲着isa指針,藍色框中的四個字節存放着_height=1,而綠色框中的8個字節存放着_num=3,這裏由於結構體內存對齊原則,因此_num=3的內存地址從第17個字節開始,整個紅色框的32個字節,就是系統分配給XLPerson實例對象的內存空間,這也證實了malloc_size((__bridge const void *)(p))返回的確實是系統分配給p對象的內存空間。
OC對象主要分爲3種
instance對象就是經過alloc操做建立出來的對象,每次調用alloc操做都會建立出不一樣的instance對象,它們擁有各自獨立分配的內存空間。例如上文中使用的XLPerson的實例對象
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
複製代碼
其中p一、p2就是實例對象,在內存中能夠同時擁有多個同一類對象的實例對象。它們各自擁有一塊內存空間,用來存儲獨有的信息。實例對象內部存放的內容以下(以XLPerson的實例對象爲例):
XLPerson *p1 = [[XLPerson alloc] init];
p->_height = 10;
p->_num = 25;
複製代碼
由於經過[XLPerson alloc]就能建立一個實例對象,因此每一個實例對象內部會存放着一個isa指針,指向它的類對象,還存放着定義好的其它的成員變量的具體值。
類對象是將具備類似屬性和方法的對象抽象出來,從而造成類對象。它能夠定義一些類似的方法和屬性,不一樣的實例對象去引用類對象的屬性或者方法,能減小代碼的重複率。
運行以下代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Class c1 = [XLPerson class];
Class c2 = [p1 class];
Class c3 = [p2 class];
Class c4 = object_getClass(p1);
Class c5 = object_getClass(p2);
NSLog(@"\n c1 -> %p,\n c2 -> %p,\n c3 -> %p,\n c4 -> %p,\n c5 -> %p", c1, c2, c3, c4, c5);
}
return 0;
}
複製代碼
能夠獲得結果爲:
經過結果能夠發現,全部的class對象的內存地址都是相同的,這也就說明在內存中只有一個class對象,不論是使用上面的哪一種方法獲取到的class對象都是同一個。
class對象內部其實就是一個object_class的結構體,具體的結構定義在上文已經介紹過,這裏只列舉出class對象存儲的主要信息:
元類其實也是一個class類型的對象,它內部的結構和類對象一致,可是元類對象中只存放了以下信息:
元類和類同樣,在內存中只會存在一個元類對象。能夠經過runtime的方法獲取元類對象
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Class c1 = object_getClass(p1);
Class c2 = object_getClass(p2);
Class mataC1 = object_getClass(c1);
Class mataC2 = object_getClass(c2);
BOOL c1_isMataClass = class_isMetaClass(c1);
BOOL c2_isMataClass = class_isMetaClass(c2);
BOOL mataC1_isMataClass = class_isMetaClass(mataC1);
BOOL mataC2_isMataClass = class_isMetaClass(mataC2);
NSLog(@"\n c1_isMataClass:%d,\n c2_isMataClass:%d,\n mataC1_isMataClass:%d,\n mataC2_isMataClass:%d"
,c1_isMataClass, c2_isMataClass, mataC1_isMataClass, mataC2_isMataClass);
NSLog(@"\n c1 -> %p,\n c2 -> %p,\n mataC1 -> %p,\n mataC2 -> %p",
c1, c2, mataC1, mataC2);
}
return 0;
}
複製代碼
調用結果以下:
在上圖中,c1和c2都是類對象,因此返回0,mataC1和mataC2都是元類對象,因此返回1。同時mataC1和mataC2的內存地址徹底相同,這也說明了元類對象在內存中確實只存在一份。
上文屢次提到,在Class對象內部都會有一個isa指針,那麼這個isa指針的做用是什麼呢?其實isa指針是instance對象、class對象和mata-class對象之間的橋樑。
superClass其實就是指向class對象或者mata-class對象的父類,下面咱們以一個簡單的例子來具體說明:
@interface XLPerson : NSObject
- (void)run;
+ (void)sleep;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
[p1 run];
[XLPerson sleep];
}
}
複製代碼
XLPerson繼承自NSObject,而且聲明瞭一個類方法sleep()和一個對象方法run(),當p1調用對象方法run()時
仍是以上面的例子來講明,當XLPerson調用類方法sleep()時
首先先看一張很是經典的描述instance對象、類對象以及元類對象之間關係的圖片。途中虛線表明isa指針,實線表明superClass指針。
系統給一個NSObject對象分配了16個字節的內存空間(經過malloc_size函數申請內存),可是NSObject對象內部只有一個isa指針,因此它實際使用到了8個字節的內存,而因爲ios的內存對齊原則,系統最少分配16個字節的內存空間。
能夠經過class_getInstanceSize函數來獲取NSObject佔用內存大小
以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。
一塊兒學習,一塊兒進步~~~