<簡書 — 劉小壯> https://www.jianshu.com/p/5b7e7c8075efgit
在OC1.0
中,Runtime
不少定義都寫在NSObject.h
文件中,若是以前研究過Runtime
的同窗能夠應該見過下面的定義,定義了一些基礎的信息。github
// 聲明Class和id
typedef struct objc_class *Class;
typedef struct objc_object *id;
// 聲明經常使用變量
typedef struct objc_method *Method;
typedef struct objc_ivar *Ivar;
typedef struct objc_category *Category;
typedef struct objc_property *objc_property_t;
// objc_object和objc_class
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製代碼
以前的Runtime
結構也比較簡單,都是一些很直接的結構體定義,如今新版的Runtime
在操做的時候,各類地址偏移操做和位運算。swift
後來可能蘋果也不太想讓開發者知道Runtime
內部的實現,因此就把源碼定義從NSObject
中搬到Runtime
中了。並且以前的定義也不用了,經過OBJC_TYPES_DEFINED
預編譯指令,將以前的代碼廢棄調了。緩存
如今NSObject
中的定義很是簡單,直接就是一個Class
類型的isa
變量,其餘信息都隱藏起來了。架構
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製代碼
這是最新的一些經常使用Runtime
定義,和以前的定義也不太同樣了,用了最新的結構體對象,以前的結構體也都廢棄了。ide
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
複製代碼
在OC中每一個對象都是一個結構體,結構體中都包含一個isa的成員變量,其位於成員變量的第一位。isa
的成員變量以前都是Class
類型的,後來蘋果將其改成isa_t
。函數
struct objc_object {
private:
isa_t isa;
};
複製代碼
OC中的類和元類也是同樣,都是結構體構成的。因爲類的結構體定義繼承自objc_object
,因此其也是一個對象,而且具備對象的isa
特徵。oop
因此能夠經過isa_t
來查找對應的類或元類,查找方法應該是經過uintptr_t
類型的bits
,經過按位操做來查找isa_t
指向的類的地址。佈局
實例對象或類對象的方法,並不會定義在各個對象中,而是都定義在isa_t
指向的類中。查找到對應的類後,經過類的class_data_bits_t
類型的bits
結構體查找方法,對象、類、元類都是一樣的查找原理。優化
isa_t
是一個union
的結構對象,union
相似於C++
結構體,其內部能夠定義成員變量和函數。在isa_t
中定義了cls
、bits
、isa_t
三部分,下面的struct
結構體就是isa_t
的結構體構成。
下面對isa_t
中的結構體進行了位域聲明,地址從nonpointer
起到extra_rc
結束,從低到高進行排列。位域也是對結構體內存佈局進行了一個聲明,經過下面的結構體成員變量能夠直接操做某個地址。位域總共佔8字節,全部的位域加在一塊兒正好是64位。
小提示:union
中bits
能夠操做整個內存區,而位域只能操做對應的位。
下面的代碼是不完整代碼,只保留了arm64
部分,其餘部分被忽略掉了。
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 是32位仍是64位
uintptr_t has_assoc : 1; // 對象是否含有或曾經含有關聯引用,若是沒有關聯引用,能夠更快的釋放對象
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析構函數或OC的析構函數
uintptr_t shiftcls : 33; // 對象指向類的內存地址,也就是isa指向的地址
uintptr_t magic : 6; // 對象是否初始化完成
uintptr_t weakly_referenced : 1; // 對象是否被弱引用或曾經被弱引用
uintptr_t deallocating : 1; // 對象是否被釋放中
uintptr_t has_sidetable_rc : 1; // 對象引用計數太大,是否超出存儲區域
uintptr_t extra_rc : 19; // 對象引用計數
# define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; # elif __x86_64__ // ···· # else // ···· # endif }; 複製代碼
在ARM64
架構下,isa_t
以如下結構進行佈局。在不一樣的CPU
架構下,佈局方式會有所不一樣,但參數都是同樣的。
在Runtime
中類也是一個對象,類的結構體objc_class
是繼承自objc_object
的,具有對象全部的特徵。在objc_class
中定義了三個成員變量,superclass
是一個objc_class
類型的指針,指向其父類的objc_class
結構體。cache
用來處理已調用方法的緩存。
bits
是objc_class
的主角,其內部只定義了一個uintptr_t
類型的bits
成員變量,存儲了class_rw_t
的地址。bits
中還定義了一些基本操做,例如獲取class_rw_t
、raw isa
狀態、是否swift
等函數。objc_class
結構體中定義的一些函數,其內部都是經過bits
實現的。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// .....
}
複製代碼
從objc_class
的源碼能夠看出,能夠經過bits
結構體的data()
函數,獲取class_rw_t
指針。咱們進入源代碼中看一下,能夠看出是經過對uintptr_t
類型的bits
變量,作位運算查找對應的值。
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
複製代碼
uintptr_t
本質上是一個unsigned long
的typedef
,unsigned long
在64位處理器中佔8字節,正好是64位二進制。經過FAST_DATA_MASK
轉換爲二進制後,是取bits
中的47-3的位置,正好是取出class_rw_t
指針。
在OC中一個指針的長度是47,例如打印一個UIViewController
的地址是0x7faf1b580450
,轉換爲二進制是11111111010111100011011010110000000010001010000
,最後面三位是佔位的,因此在取地址的時候會忽略最後三位。
// 查找第0位,表示是否swift
#define FAST_IS_SWIFT (1UL<<0) // 當前類或父類是否認義了retain、release等方法 #define FAST_HAS_DEFAULT_RR (1UL<<1) // 類或父類須要初始化isa #define FAST_REQUIRES_RAW_ISA (1UL<<2) // 數據段的指針 #define FAST_DATA_MASK 0x00007ffffffffff8UL // 11111111111111111111111111111111111111111111000 總共47位 複製代碼
由於在bits
中最後三位是沒用的,因此能夠用來存儲一些其餘信息。在class_data_bits_t
還定義了三個宏,用來對後三位作位運算。
和class_data_bits_t
相關的有兩個很重要結構體,class_rw_t
和class_ro_t
,其中都定義着method list
、protocol list
、property list
等關鍵信息。
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;
};
複製代碼
在編譯後class_data_bits_t
指向的是一個class_ro_t
的地址,這個結構體是不可變的(只讀)。在運行時,纔會經過realizeClass
函數將bits
指向class_rw_t
。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
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
,在這個過程當中,會把編譯器存儲在bits
中的class_ro_t
取出,而後建立class_rw_t
,並把ro
賦值給rw
,成爲rw
的一個成員變量,最後把rw
設置給bits
,替代以前bits
中存儲的ro
。除了這些操做外,還會有一些其餘賦值的操做,下面是初始化Class
的精簡版代碼。
static Class realizeClass(Class cls)
{
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return 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);
isMeta = ro->flags & RO_META;
rw->version = isMeta ? 7 : 0;
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()))
cls->superclass = supercls;
cls->initClassIsa(metacls);
cls->setInstanceSize(ro->instanceSize);
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
methodizeClass(cls);
return cls;
}
複製代碼
在上面的代碼中咱們還發現了兩個函數,addRootClass
和addSubclass
函數,這兩個函數的職責是將某個類的子類串成一個列表,大體是下面的連接順序。由此可知,咱們是能夠經過class_rw_t
,獲取到當前類的全部子類。
superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...
複製代碼
初始化rw
和ro
以後,rw
的method list
、protocol list
、property list
都是空的,須要在下面methodizeClass
函數中進行賦值。函數中會把ro
的list
都取出來,而後賦值給rw
,若是在運行時動態修改,也是對rw
作的操做。因此ro
中存儲的是編譯時就已經決定的原數據,rw
纔是運行時動態修改的數據。
static void methodizeClass(Class cls)
{
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro;
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
}
複製代碼
假設建立一個類LXZObject
,繼承自NSObject
,併爲其加入一個testMethod
方法,不作其餘操做。由於在編譯後objc_class
的bits
對應的是class_ro_t
結構體,因此咱們打印一下結構體的成員變量,看一下編譯後的class_ro_t
是什麼樣的。
struct class_ro_t {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "LXZObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
複製代碼
通過打印能夠看出,一個類的class_ro_t
中只會包含當前類的信息,不會包含其父類的信息,在LXZObject
類中只會包含name
和baseMethodList
兩個字段,而baseMethodList
中只有一個testMethod
方法。由此可知,class_rw_t
結構體也是同樣的。
下面是已經初始化後的isa_t
結構體的佈局,以及各個結構體成員在結構體中的位置。
union
常常配合結構體使用,第一次使用union
就是對結構體區域作初始化。在對象初始化時,會對isa_t
的bits
字段賦值爲ISA_MAGIC_VALUE
,這就是對union
聯合體初始化的過程。
// 在objc-723中已經沒有了
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
if (!indexed) {
isa.cls = cls;
} else {
isa.bits = ISA_MAGIC_VALUE;
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
}
複製代碼
在對象經過initIsa()
函數初始化時,會經過ISA_MAGIC_VALUE
對isa
進行初始化。ISA_MAGIC_VALUE
是一個16進制的值,將其轉換爲二進制後,會發現ISA_MAGIC_VALUE
是對nonpointer
和magic
作初始化。
nonpointer
是對以前32位處理器的兼容。在訪問對象所屬的類時,若是是32位則返回以前的isa
指針地址,不然表示是64位處理器,則返回isa_t
結構體。
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
二進制:11010000000000000000000000000000000000001
補全二進制:23個零+11010000000000000000000000000000000000001
複製代碼
隨後會經過位域,對has_cxx_dtor
和shiftcls
作初始化,這時候就已經有四個字段被初始化了。has_cxx_dtor
表示是否有C++
或OC的析構方法,在打印方法列表時,常常能看到一個名爲.cxx_destruct
的方法,就和這個字段有關係。
在計算機中爲了對存儲區(Memory or Disk)
讀取方便,因此在寫入和讀取時,會對內存有對其操做。通常是以字節爲單位進行對其,這樣也是對讀寫速度的優化。在對shiftcls
進行賦值時,對Class
的指針進行了位移操做,向右位移三位。這是由於類指針爲了內存對其,將最後三位用0填充,因此這三位是沒有意義的。
isa結構體
0000000001011101100000000000000100000000001110101110000011111001
0x5d8001003ae0f8
類對象地址
100000000001110101110000011111000
0x1003ae0f8
將類對象地址右移三位爲100000000001110101110000011111,正好符合isa_t地址中shiftcls的部分,前面不足補零。
複製代碼
外界獲取Class
時,應該經過ISA()
函數,而不是像以前同樣直接訪問isa
指針。在ISA()
函數中,是對isa_t
的結構體作與運算,是經過ISA_MASK
宏進行的,轉換爲二進制的話,正好是把shiftcls
的地址取出來。
inline Class
objc_object::ISA()
{
return (Class)(isa.bits & ISA_MASK);
}
#define ISA_MASK 0x0000000ffffffff8ULL
111111111111111111111111111111111000
複製代碼
從iPhone5s
開始,iOS
設備開始引入了64位處理器,以前的處理器一直都是32位的。
可是在64位處理器中,指針長度以及一些變量所佔內存都發生了改變,32位一個指針佔用4字節,但64位一個指針佔用8字節;32位一個long
佔用4字節,64位一個long
佔用8字節等,因此在64位上內存佔用會多出不少。
蘋果爲了優化這個問題,推出了Tagged Pointer新特性。以前一個指針指向一個地址,而Tagged Pointer中一個指針就表明一個值, 以NSNumber爲例。
NSNumber *number1 = @1;
NSNumber *number2 = @3;
NSNumber *number3 = @54;
// 輸出
(lldb) p number1
(__NSCFNumber *) $3 = 0xb000000000000012 (int)1
(lldb) p number2
(__NSCFNumber *) $4 = 0xb000000000000032 (int)3
(lldb) p number3
(__NSCFNumber *) $5 = 0xb000000000000362 (int)54
複製代碼
經過上面代碼能夠看出,使用了Tagged Pointer
新特性後,指針中就存儲着對象的值。例如一個值爲1的NSNumber
,指針就是0xb000000000000012
,若是拋去前面的0xb
和後面的2,中間正好就是16進制的值。
蘋果經過Tagged Pointer
的特性,明顯的提高了執行效率並節省了不少內存。在64位處理器下,內存佔用減小了將近一半,執行效率也大大提高。因爲經過指針來直接表示數值,因此沒有了malloc
和free
的過程,對象的建立和銷燬速度提高几十倍。
對於對象指針也是同樣,在OC1.0
時代isa
是一個真的指針,指向一個堆區的地址。而OC2.0
時代,一個指針長度是八字節也就是64位,在64位中直接存儲着對象的信息。當查找對象所屬的類時,直接在isa
指針中進行位運算便可,並且因爲是在棧區進行操做,查找速度是很是快的。
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
複製代碼
例如isa_t
本質上是一個結構體,若是建立結構體再用指針指向這個結構體,內存佔用是很大的。可是Tagged Pointer
特性中,直接把結構體的值都存儲到指針中,這就至關節省內存了。
蘋果不容許直接訪問isa
指針,和Tagged Pointer
也是有關係的。由於在Tagged Pointer
的狀況下,isa
並非一個指針指向另外一塊內存區,而是直接表示對象的值,因此經過直接訪問isa
獲取到的信息是錯誤的。
簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github
上,下載Runtime PDF
合集。把全部Runtime
文章總計九篇,都寫在這個PDF
中,並且左側有目錄,方便閱讀。
下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!😁