面向對象的編程語言中有經典的話:萬物皆對象。objective-c
就是一門面向對象的語言,那麼在oc
的編程中就離不開對象的建立,下面分析oc對象的內存結構及其建立的過程c++
首先分析對象的內存結構,其實咱們知道oc
的對象指針其實就是結構體指針,也就是說oc的對象轉成c++代碼後其實就是一個結構體。定義一個簡單的類代碼以下:objective-c
@interface Person : NSObject
@property (nonatomic,assign) NSUInteger age;
@property (nonatomic,copy) NSString *name;
-(void)say;
@end
@implementation Person
-(void)say{
NSLog(@"person say");
}
@end
複製代碼
利用clang
編譯器吧這個類轉成c++
代碼後能夠發現對應的Person
類其實就是一個結構體,代碼以下:編程
struct Person_IMPL {
Class isa;
NSUInteger _age;
NSString * _Nonnull _name;
};
複製代碼
先忽略say
方法的存在(方法跟結構體的isa
指針相關,稍後再分析),能夠看出來 struct Person_IMPL
的結構體定義跟Person
類的屬性定義是吻合的。因此,其實咱們日常 建立對象其實就是給類的對應的結構體在堆上開闢一塊合適的空間,並返回這塊空間的指針給用戶,這個指針就是咱們平時操做對象(包括對象方法的調用,對象屬相的更改)的指針,只是oc把這個結構體指針包裝成一個oc類型的指針(Person *
)而已。bash
接下來利用指針強轉把oc對象相似指針轉換爲c語言結構體指針來驗證一下oc類其實底層就是c語言的結構體。數據結構
運行代碼:架構
Person *p = [[Person alloc] init];
p.age = 15;
p.name = @"Mike";
//指針強轉
struct Person_IMPL *sp = (__bridge struct Person_IMPL *)(p);
NSLog(@"經過oc對象類型指針轉爲結構體指針後訪問的結構體Person_IMPL值 : _age = %zd , _name = %@" , sp->_age , sp->_name);
複製代碼
打印結果:編程語言
經過oc對象類型指針轉爲結構體指針後訪問的結構體Person_IMPL值 : _age = 15 , _name = Mike
複製代碼
驗證結果符合預期。ide
既然oc對象的底層數據結構是c語言的結構體,那麼對象的屬性或成員的存取其實跟c語言結構體的成員變量的存取原理實際上是同樣的:經過指針的偏移操做內存的數據:用一個通俗一點的公式能夠表達爲 propertyValue(對象的成員變量值) = objcPointer(對象指針) + offset(偏移量)。 對象內存及其指針的關係用下圖表示: 函數
咱們能夠用代碼驗證經過指針的操做可否訪問到oc對象的內存數據 代碼以下:源碼分析
Person *p = [[Person alloc] init];
p.age = 15;
p.name = @"Mike";
struct Person_IMPL *sp = (__bridge struct Person_IMPL *)(p);
//經過指針的偏移操做 , 或的結構體內部的成員地址,也是oc對象指針(Person * p)的對象成員地址。
long long ageAdress = (long long)((char *)sp+8);
long long nameAdress = (long long)((char *)sp+16);
//打斷點經過lldb指令調試驗證
NSLog(@"===");
複製代碼
在NSLog處打斷點後,經過lldb打印相關指令查看到p或sp指針向上偏移8字節能夠獲取到時成員變量age的值,如圖所示
p或sp指針向上偏移8字節能夠獲取到時成員變量name
研究類的初始化過程確定是經過objc
官方源碼分析 , 本人用的是objc4-750
的版本進行分析。
經過咱們建立對象都是調用+alloc
方法進行建立的,此方法調用到了下面兩個方法,我把該方法的簡化以下
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
//此函數調用到下面的 _class_createInstanceFromZone
id obj = class_createInstance(cls, 0);
return obj;
}
//class_createInstance 調用到此方法
static __attribute__((always_inline)) id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
bool hasCxxDtor = cls->hasCxxDtor();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
return obj;
}
複製代碼
上面函數的做用有兩個
struct objc_object *
就是咱們的id
指針,這也反映了NSObjcet *
對應struct objc_object *
,下面會分析)在分析初始化isa指針前先弄清楚 oc對象指針(NSObjcet * , id
)對應在objc
源碼中那些結構體的關係可能會容易理解一點
// NSObject 定義
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//struct objc_object 定義
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
//struct 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
}
//id 指針的定義
typedef struct objc_object *id;
//Class 定義
typedef struct objc_class *Class;
複製代碼
經過上面的代碼能夠發現,咱們日常使用的 NSObject *
或者 id
指針其實底層是 struct objc_object
指針,而日常使用的Class類型其實底層就是struct objc_class
指針,並且還能夠發現Class
類型(objc_class *
)實際上是繼承自objc_class
,就是說咱們Class
類型其實也是一個對象。
在oc中對象(object),對象的父類(SuperClass),對象的類(Class),對象的元類(MetaClass)都是經過指針來進行關聯的。 SuperClass
對應的是objc_class
的superclass
指針 , Class
對應的是objc_class
的isa
指針(OBJC2中的isa指針已經不是直接指向Class的地址了,而是用來位域的技術存儲了Class的地址外還有其餘一些額外的信息)。
首先咱們開看下isa結構的定義(objc-private.h + isa.h
) 這裏緊列舉__x86__64__
架構的狀況進行分析
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;//表明是否有開啓指針isa指針優化(位域技術存儲更多信息)
uintptr_t has_assoc : 1;//是否有設置關聯對象
uintptr_t has_cxx_dtor : 1;//是否有c++析構函數
uintptr_t shiftcls : 44;//存儲Class或MetaClass的內存地址信息
uintptr_t magic : 6;//驗證對象是否初始化完成
uintptr_t weakly_referenced : 1;//是否有被弱引用指針指向
uintptr_t deallocating : 1;//對象是否正在釋放
uintptr_t has_sidetable_rc : 1;//extra_rc沒法存儲過大的數值時,次標誌位爲1,把extra_rc部分的值存儲到一個全局的SideTable中
uintptr_t extra_rc : 8//存儲引用計數存儲 (引用值 = 存儲值 - 1)
};
};
複製代碼
能夠看isa_t
實際上是一個共用體union : 一個8字節指針(64位) = cls = bits = 使用位域的struct
瞭解了isa_t
的結構後咱們看下struct objc_object
初始化isa的方法實現
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) { // Taggedpointer
isa.cls = cls;
} else { // 非Taggedpointer , 平時咱們常用的對象
isa_t newisa(0);
//對isa的 index 、magic 初始化
newisa.bits = ISA_MAGIC_VALUE;
//對isa的 has_cxx_dtor 初始化
newisa.has_cxx_dtor = hasCxxDtor;
//把傳進來的Class指針值右移3位賦值給shiftcls
newisa.shiftcls = (uintptr_t)cls >> 3;
//更新objc_object的isa指針
isa = newisa;
}
}
複製代碼
在複製Class
的指針值是爲何要右移三位在賦值,其實緣由能夠在從上面獲取內存大小時進行的對齊規則能夠看出Class的地址轉成64位二進制時指針的後三位都是0,右移3位後再存進isa
的47位的shiftcls,這樣節省可內存的空間。經過打印Class的地址值能夠看出47位的內存是能夠存放的下一個右移3位的Class的地址值的,並不必定要64d的的存儲空間。
一個對象調用它的實例方法,實際上是先經過isa指針找到類對象的內存地址,經過訪問其成員 class_data_bits_t bits
獲取到實例方法。類對象調用的類方法其實與實例方法的原理是同樣的經過isa找到元類(MetaClass
)的內存地址,經過訪問MetaClass
的class_data_bits_t bits
獲取類方法進行調用。二者的方法查找都是在當前類中若是找不到對象的方法就會沿着superClass
指針往父類的方法裏面查找,直到找到位置,若是找不到就會進行方法的動態解析或者消息的轉發,還沒解決就會拋出找不到方法的錯誤。下面的圖片很好的展現了這實例對象(objc
)與其 類對象(Class
)、元類對象(MetaClass
)、父類(SuperClass
)之間的關係。
細心觀察上面的圖片,其實能夠發現幾個注意點
那麼分析清楚了這幾個對象間的關係後,接下來開始分析對象實例方法到底是如何初始化的。
源碼定義
struct class_data_bits_t {
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
複製代碼
其實 class_data_bits_t bit
就是一個指針而已。真正的方法存儲在data()
返回的指針指向的那塊內存中。該內存實際上是一個class_rw_t
的類型值。繼續分析返回的class_rw_t *
類型值
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;//經過編譯的類信息(只讀不能寫入)
method_array_t methods; //方法列表
property_array_t properties;//屬性列表
protocol_array_t protocols; //協議列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
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;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
複製代碼
經過查閱資料及源碼,加上實踐驗證能夠知道,咱們平是定義的類屬性或者方法,通過編譯器的處理轉成C或C++代碼其實底層由多種結構體和函數共同協做生成包含只讀方法和屬性的struct class_ro_t
類型變量。就用上面的Person類做爲例子。經過clang編譯器指令轉成c++
代碼後我摘取一些重要片斷
//包含對象屬性信息的變量 , 用於初始化 _class_ro_t 變量
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"age","TQ,N,V_age"},
{"name","T@\"NSString\",C,N,V_name"}}
};
//包含對象成員變量信息的變量 , 用於初始化 _class_ro_t 變量
static struct /*_ivar_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
2,
{{(unsigned long int *)&OBJC_IVAR_$_Person$_age, "_age", "Q", 3, 8},
{(unsigned long int *)&OBJC_IVAR_$_Person$_name, "_name", "@\"NSString\"", 3, 8}}
};
//包含對象方法信息的變量 , 用於初始化 _class_ro_t 變量
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
5,
{{(struct objc_selector *)"say", "v16@0:8", (void *)_I_Person_say/**方法對應的函數指針*/},
{(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
{(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_}}
};
// _class_ro_t 類型變量
tatic struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0, __OFFSETOFIVAR__(struct Person, _age), sizeof(struct Person_IMPL),
0,
"Person",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
0,
(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
};
//下面幾個方法都是爲初始化 Person 類作準備工做
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
0, // &OBJC_METACLASS_$_Person,
0, // &OBJC_CLASS_$_NSObject,
0, // (void *)&_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_CLASS_RO_$_Person,
};
static void OBJC_CLASS_SETUP_$_Person(void ) {
OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}
static void OBJC_CLASS_SETUP_$_Person(void ) {
OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}
複製代碼
從上面的源碼能夠看出在程序編譯完成後類的信息已經被編譯器處理完了大部分的工做,剩下小部分工做是經過runtime
機制來處理的。
objc源碼中有一個函數realizeClass
,負責處理編譯信息及運行時信息的轉接返回類的真實結構體。我簡化下函數留下處理 _class_ro_t
與 class_rw_t
關係的源碼
static Class realizeClass(Class cls){
ro = (const class_ro_t *)cls->data();
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
return cls;
}
複製代碼
從上面的源碼能夠看出,類在未通過調用函數realizeClass(Class cls)
前,objc_class
結構體方法調用的class_rw_t *data()
方法返回的實際上是class_ro_t
類型的指針,在通過realizeClass
處理後才把class_rw_t
類型變量建立好,並把原來的class_ro_t
指針賦值給class_rw_t
變量的ro
成員變量,並賦值給cls。
接下來咱們經過objc
源碼驗證一下
在調試是先獲取[Person Class]
的地址,接着realizeClass
開始前打斷條件斷點(cls == Person地址值)配合lldb指令,經過指針的偏移得到class_data_bits_t
的值,再經過其調用data()方法得到對應的指針,經過把改指針強轉爲class_ro_t
類型打印出來的值符合以前定義Person類的信息。調試過程以下圖