<簡書 — 劉小壯> https://www.jianshu.com/p/3019605a4fc9html
本文基於objc-723
版本,在Apple Github和Apple OpenSource上有源碼,可是須要本身編譯。git
重點來了~,能夠到個人Github
上下載編譯好的源碼,源碼中已經寫了大量的註釋,方便讀者研究。(若是以爲還不錯,各位大佬麻煩點個Star
😁) Runtime Analyzegithub
在對象初始化的時候,通常都會調用alloc+init
方法實例化,或者經過new
方法進行實例化。下面將會分析經過alloc+init
的方式實例化的過程,如下代碼都是關鍵代碼。數組
前面兩步很簡單,都是直接進行函數調用。app
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
複製代碼
在建立對象的地方有兩種方式,一種是經過calloc
開闢內存,而後經過initInstanceIsa
函數初始化這塊內存。第二種是直接調用class_createInstance
函數,由內部實現初始化邏輯。框架
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (fastpath(cls->canAllocFast())) {
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
複製代碼
可是在最新版的objc-723
中,調用canAllocFast
函數直接返回false
,因此只會執行上面第二個else
代碼塊。ide
bool canAllocFast() {
return false;
}
複製代碼
初始化代碼最終會調用到_class_createInstanceFromZone
函數,這個函數是初始化的關鍵代碼。下面代碼中會進入if
語句內,根據instanceSize
函數返回的size
,經過calloc
函數分配內存,並初始化isa_t
指針。函數
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
obj->initIsa(cls);
}
return obj;
}
複製代碼
在instanceSize()
函數中,會經過alignedInstanceSize
函數獲取對象原始大小,在class_ro_t
結構體中的instanceSize
變量中定義。這個變量中存儲了對象實例化時,全部變量所佔的內存大小,這個大小是在編譯器就已經決定的,不能在運行時進行動態改變。佈局
獲取到instanceSize
後,對獲取到的size
進行地址對其。須要注意的是,CF
框架要求全部對象大小最少是16字節,若是不夠則直接定義爲16字節。ui
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;
}
複製代碼
這也是很關鍵的一步,因爲調用initIsa
函數時,nonpointer
字段傳入true
,因此直接執行if
語句,設置isa
的cls
爲傳入的Class
。isa
是objc_object
的結構體成員變量,也就是isa_t
的類型。
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
複製代碼
經過new
函數建立對象實際上是同樣的,內部經過callAlloc
函數執行建立操做,若是調用alloc
方法的話也是調用的callAlloc
函數。因此調用new
函數初始化對象時,能夠等同於alloc+init
的調用。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
複製代碼
在runtime源碼中,執行init操做本質上就是直接把self返回。
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj)
{
return obj;
}
複製代碼
在對象銷燬時,運行時環境會調用NSObject
的dealloc
方法執行銷燬代碼,並不須要咱們手動去調用。接着會調用到Runtime
內部的objc_object::rootDealloc
(C++命名空間)函數。
在rootDealloc
函數中會執行一些釋放前的操做,例如將對象全部的引用指向nil
,而且調用free
函數釋放內存空間等。
下面的if-else
語句中有判斷條件,若是是ARC
環境,而且當前對象定義了實例變量,纔會進入else
中執行object_dispose
函數,不然進入上面的if
語句。上面的if
語句表示當前對象沒有實例變量,則直接將當前對象free
。
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
複製代碼
在object_dispose
函數中,主要是經過objc_destructInstance
函數實現的。在函數內部主要作了三件事:
.cxx_destruct
函數,在函數內部還會進行對應的release
操做。clear
操做。// dealloc方法的核心實現,內部會作判斷和析構操做
void *objc_destructInstance(id obj)
{
if (obj) {
// 判斷是否有OC或C++的析構函數
bool cxx = obj->hasCxxDtor();
// 對象是否有相關聯的引用
bool assoc = obj->hasAssociatedObjects();
// 對當前對象進行析構
if (cxx) object_cxxDestruct(obj);
// 移除全部對象的關聯,例如把weak指針置nil
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
上面的函數中會調用object_cxxDestruct
函數進行析構,而函數內部是經過object_cxxDestructFromClass
函數實現的。
函數內部會從當前對象所屬的類開始遍歷,一直遍歷到根類位置。在遍歷的過程當中,會不斷執行.cxx_destruct
函數,對傳入的對象進行析構。
由於在繼承者鏈中,每一個類都會有本身的析構代碼,因此須要將當前對象傳入,並逐個執行析構操做,將對象的全部析構操做都執行完成才能夠。
// 調用C++的析構函數
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);
// 從當前類開始遍歷,直到遍歷到根類
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
// SEL_cxx_destruct就是.cxx_destruct的selector
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
// 獲取到.cxx_destruct的函數指針並調用
(*dtor)(obj);
}
}
}
複製代碼
在對象被執行.cxx_destruct
析構函數後,析構函數內部還會調用一次release
函數,完成最後的釋放操做。
在項目中常常會動態對方法列表進行操做,例如動態添加或替換一個方法,這時候會用到下面兩個Runtime
函數。在下面兩個函數中,本質上都是經過addMethod
函數實現的,在class_addMethod
中對返回值進行了一個取反,因此若是此函數返回NO
則表示方法已存在,不要重複添加。
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
rwlock_writer_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
rwlock_writer_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
複製代碼
下面咱們就分析一下addMethod
函數的實現,依然只保留核心源碼。
在addMethod
函數中會先判斷須要添加的方法是否存在,若是已經存在則直接返回對應的IMP
,不然就動態添加一個方法。在class_addMethod
函數中有一個replace
字段,表示區別是否class_replaceMethod
函數調用過來的。若是replace
是NO
則直接返回IMP
,若是是YES
則替換方法原有實現。
若是添加的方法不存在,則建立一個method_list_t
結構體指針,並設置三個基本參數name
、types
、imp
,而後經過attachLists
函數將新建立的method_list_t
結構體添加到方法列表中。
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
method_t *m;
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
if (!replace) {
result = m->imp;
} else {
result = _method_setImplementation(cls, m, imp);
}
} else {
// fixme optimize
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(method_t) | fixed_up_method_list;
newlist->count = 1;
newlist->first.name = name;
newlist->first.types = strdupIfMutable(types);
newlist->first.imp = imp;
prepareMethodLists(cls, &newlist, 1, NO, NO);
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls);
result = nil;
}
return result;
}
複製代碼
在attachLists
函數中實現比較簡單,經過對原有地址作位移,並將新建立的method_list_t
結構體copy
到方法列表中。
void attachLists(List* const * addedLists, uint32_t addedCount) {
// ...
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
// ...
}
複製代碼
在Runtime
中能夠經過class_addIvar
函數,向一個類添加實例對象。可是須要注意的是,這個函數不能向一個已經存在的類添加實例變量,只能想經過Runtime API
建立的類動態添加實例變量。
函數應該在調用objc_allocateClassPair
函數建立類以後,以及調用objc_registerClassPair
函數註冊的類之間添加實例變量,不然就會失敗。也不能向一個元類添加實例變量,只能想類添加實例變量。
下面是動態建立一個類,並向新建立的類添加實例變量的代碼。
Class testClass = objc_allocateClassPair([NSObject class], "TestObject", 0);
BOOL isAdded = class_addIvar(testClass, "password", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
objc_registerClassPair(testClass);
if (isAdded) {
id object = [[testClass alloc] init];
[object setValue:@"lxz" forKey:@"password"];
}
複製代碼
那麼,爲何須要把動態添加實例變量的代碼放在這兩個函數中間呢?讓咱們一塊兒來探究一下吧。
首先經過objc_allocateClassPair
函數來建立類,建立時經過getClass
函數判斷類名是否已用,而後經過verifySuperclass
函數判斷superclass
是否合適,若是任意條件不符合則建立類失敗。
下面經過alloc_class_for_subclass
函數建立類和元類,在alloc
函數內部本質上是經過calloc
函數分配內存空間,沒有作其餘操做。而後就執行objc_initializeClassPair_internal
函數,initialize
函數內部都是初始化操做,用來初始化剛剛建立的Class
和metaClass
。
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
{
Class cls, meta;
if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
return nil;
}
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);
objc_initializeClassPair_internal(superclass, name, cls, meta);
return cls;
}
複製代碼
這就是initialize
函數內部的實現,都是各類初始化代碼,沒有作其餘邏輯操做。至此,類的初始化完成,能夠在外面經過class_addIvar
函數添加實例變量了。
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
class_ro_t *cls_ro_w, *meta_ro_w;
cls->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
meta->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
cls_ro_w = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
meta_ro_w = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
cls->data()->ro = cls_ro_w;
meta->data()->ro = meta_ro_w;
// Set basic info
cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
cls->data()->version = 0;
meta->data()->version = 7;
// ....
}
複製代碼
在建立類以後,會經過objc_registerClassPair
函數註冊新類。和建立新類同樣,註冊新類也分爲註冊類和註冊元類。經過下面的addNonMetaClass
函數註冊元類,經過直接調用NXMapInsert
函數註冊類。
void objc_registerClassPair(Class cls)
{
cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
addNamedClass(cls, cls->data()->ro->name);
}
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
Class old;
if ((old = getClass(name)) && old != replacing) {
inform_duplicate(name, old, cls);
addNonMetaClass(cls);
} else {
NXMapInsert(gdb_objc_realized_classes, name, cls);
}
}
複製代碼
不管是註冊類仍是註冊元類,內部都是經過NXMapInsert
函數實現的。在Runtime
中,全部類都是存在一個哈希表中的,在table
的buckets
中存儲。每次新建立類以後,都須要把該類加入到哈希表中,下面是向哈希表插入的邏輯。
void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {
MapPair *pairs = (MapPair *)table->buckets;
// 計算key在當前hash表中的下標,hash下標不必定是最後
unsigned index = bucketOf(table, key);
// 找到buckets的首地址,並經過index下標計算對應位置,獲取到index對應的MapPair
MapPair *pair = pairs + index;
// 若是key爲空,則返回
if (key == NX_MAPNOTAKEY) {
_objc_inform("*** NXMapInsert: invalid key: -1\n");
return NULL;
}
unsigned numBuckets = table->nbBucketsMinusOne + 1;
// 若是當前地址未衝突,則直接對pair賦值
if (pair->key == NX_MAPNOTAKEY) {
pair->key = key; pair->value = value;
table->count++;
if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
return NULL;
}
/* 到這一步,則表示hash表衝突了 */
// 若是同名,則將舊類換爲新類
if (isEqual(table, pair->key, key)) {
const void *old = pair->value;
if (old != value) pair->value = value;
return (void *)old;
// hash表滿了,對hash表作重哈希,而後再次執行這個函數
} else if (table->count == numBuckets) {
/* no room: rehash and retry */
_NXMapRehash(table);
return NXMapInsert(table, key, value);
// hash表衝突了
} else {
unsigned index2 = index;
// 解決hash表衝突,這裏採用的是線性探測法,解決哈希表衝突
while ((index2 = nextIndex(table, index2)) != index) {
pair = pairs + index2;
if (pair->key == NX_MAPNOTAKEY) {
pair->key = key; pair->value = value;
table->count++;
// 在查找過程當中,發現哈希表不夠用了,則進行重哈希
if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
return NULL;
}
// 找到同名類,則用新類替換舊類,並返回
if (isEqual(table, pair->key, key)) {
const void *old = pair->value;
if (old != value) pair->value = value;
return (void *)old;
}
}
return NULL;
}
}
複製代碼
那爲何只能向運行時動態建立的類添加
ivars
,不能向已經存在的類添加ivars
呢?
這是由於在編譯時只讀結構體class_ro_t
就會被肯定,在運行時是不可更改的。ro
結構體中有一個字段是instanceSize
,表示當前類在建立對象時須要多少空間,後面的建立都根據這個size
分配類的內存。
若是對一個已經存在的類增長一個參數,改變了ivars
的結構,這樣在訪問改變以前建立的對象時,就會出現問題。
以上圖爲例,在項目中建立TestObject
類,而且添加三個成員變量,其ivars
的內存結構佔用20字節。若是在運行時動態添加一個bool
型參數,以後建立的對象ivars
都佔用21字節。
在經過ivars
結構體訪問以前建立的對象時,由於以前建立的對象沒有sex
,因此仍是按照20字節分配的內存空間,這時候訪問sex
就會致使地址越界。
定義對象時都會給其設置類型,類型本質上並非一個對象,而是用來標示當前對象所佔空間的。以C語言爲例,訪問對象都是經過地址作訪問的,而類型就是從首地址開始讀取多少位是當前對象。
int number = 18;
char text = 'i';
複製代碼
以上面代碼爲例,定義了一個int
類型的number
,佔用四字節,定義一個char
類型的text
變量,佔用一字節。在內存中訪問對象時,就是根據指針地址找到對應的內存區,而後按照指針類型取多少範圍的內存,就完成對象的讀取操做。
而在面嚮對象語言中,函數或方法的命名規則還須要保留在運行期。以C++
爲例,C++
中有一個概念叫作「函數重載」,函數重載指的是容許有一組相同函數名,但參數列表類型不一樣的函數。
原函數:void print(char c)
重載結果:_ZN4test5printEc
複製代碼
C++
函數重載是有必定規則的,例如上面就是對print
函數重載後的結果,重載結果纔是運行時真正執行的函數。函數重載發生在編譯期,會包含namespace
、class name
、function name
、返回值、參數等部分,根據這些部分從新生成函數名。
在OC中其實也存在函數重載的概念,只不過OC並非直接對原有方法名作修改,而是增長對返回值和參數按照必定規則進行編碼,而後放在method_t
結構體中。
method_t
結構體存儲着方法的信息,其中types
字段就是返回值和參數的編碼。編碼後的字符串相似於"iv@:d"
,完整的編碼規則能夠查看官方文檔。
下面就是Method
的定義,主要包含了三個關鍵信息。
struct method_t {
SEL name;
const char *types;
IMP imp;
};
複製代碼
咱們在項目中常用協議,那協議又是怎麼實現的呢?
根據Runtime
源碼能夠看出,協議都是protocol_t
結構體的對象,而protocol_t
結構體是繼承自objc_object
的,因此具有對象的特徵。
除了objc_object
中定義的一些結構體參數外,protocol_t
中還定義了一些獨有的參數,例如經常使用的name
、method list
、property list
、size
等。因此能夠看出,一個協議中能夠聲明對象方法、類方法,以及對象屬性和類屬性。
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
};
複製代碼
既然具有了對象的特徵,那也是有isa指針的。在Protocol中全部的isa都指向同一個類Protocol。 在Protocol
類中沒有作太複雜的處理,只是實現了一些基礎的方法。
@implementation Protocol
+ (void) load {
}
- (BOOL) conformsTo: (Protocol *)aProtocolObj {
return protocol_conformsToProtocol(self, aProtocolObj);
}
- (struct objc_method_description *) descriptionForInstanceMethod:(SEL)aSel {
return method_getDescription(protocol_getMethod((struct protocol_t *)self,
aSel, YES, YES, YES));
}
- (struct objc_method_description *) descriptionForClassMethod:(SEL)aSel {
return method_getDescription(protocol_getMethod((struct protocol_t *)self,
aSel, YES, NO, YES));
}
- (const char *)name {
return protocol_getName(self);
}
// Protocol重寫了isEqual方法,內部不斷查找其父類,判斷是否Protocol的子類。
- (BOOL)isEqual:other {
Class cls;
Class protoClass = objc_getClass("Protocol");
for (cls = object_getClass(other); cls; cls = cls->superclass) {
if (cls == protoClass) break;
}
if (!cls) return NO;
// check equality
return protocol_isEqual(self, other);
}
- (NSUInteger)hash {
return 23;
}
@end
複製代碼
協議的初始化也是在_read_images
函數中完成的,初始化過程主要是一個遍歷。邏輯就是獲取Protocol list
,而後遍歷這個數組,並調用readProtocol
函數進行初始化操做。
// 遍歷全部協議列表,而且將協議列表加載到Protocol的哈希表中
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
// cls = Protocol類,全部協議和對象的結構體都相似,isa都對應Protocol類
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
// 獲取protocol哈希表
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
// 從編譯器中讀取並初始化Protocol
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
複製代碼
在readProtocol
函數中,會根據傳入的協議進行初始化操做。在傳入參數中,protocol_class
就是Protocol
類,全部的協議類的isa
都指向這個類。
根據Protocol
的源碼能夠看出,其對象模型是比較簡單的,和Class
的對象模型還不太同樣。Protocol
的對象模型只有從Protocol list
中加載的對象和isa
指向的Protocol
類構成,沒有其餘的實例化過程,Protocol
類並無元類。
// 初始化傳入的全部Protocol,若是哈希表中已經存在初始化的Protocol,則不作任何處理
static void
readProtocol(protocol_t *newproto, Class protocol_class,
NXMapTable *protocol_map,
bool headerIsPreoptimized, bool headerIsBundle)
{
auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
// 根據名字得到對應的Protocol對象
protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
// 若是Protocol不爲NULL,表示已經存在相同的Protocol,則不作任何處理,進入下面if語句。
if (oldproto) {
// nothing
}
// 若是Protocol爲NULL,則對其進行簡單的初始化,並將Protocol的isa設置爲Protocol類
else if (headerIsPreoptimized) {
protocol_t *cacheproto = (protocol_t *)
getPreoptimizedProtocol(newproto->mangledName);
protocol_t *installedproto;
if (cacheproto && cacheproto != newproto) {
installedproto = cacheproto;
}
else {
installedproto = newproto;
}
// 哈希表插入函數的指針
insertFn(protocol_map, installedproto->mangledName,
installedproto);
}
// 下面兩個else都是初始化protocol_t的過程
else if (newproto->size >= sizeof(protocol_t)) {
newproto->initIsa(protocol_class);
insertFn(protocol_map, newproto->mangledName, newproto);
}
else {
size_t size = max(sizeof(protocol_t), (size_t)newproto->size);
protocol_t *installedproto = (protocol_t *)calloc(size, 1);
memcpy(installedproto, newproto, newproto->size);
installedproto->size = (__typeof__(installedproto->size))size;
installedproto->initIsa(protocol_class);
insertFn(protocol_map, installedproto->mangledName, installedproto);
}
}
複製代碼
Protocol是能夠在運行時動態建立添加的,和建立Class的過程相似,分爲建立和註冊兩部分。 建立Protocol
以後,Protocol
處於一個未完成的狀態,只有註冊後纔是可使用的Protocol
。
// 建立新的Protocol,建立後還須要調用下面的register方法
Protocol *
objc_allocateProtocol(const char *name)
{
if (getProtocol(name)) {
return nil;
}
protocol_t *result = (protocol_t *)calloc(sizeof(protocol_t), 1);
// 下面的cls是__IncompleteProtocol類,表示是未完成的Protocol
extern objc_class OBJC_CLASS_$___IncompleteProtocol;
Class cls = (Class)&OBJC_CLASS_$___IncompleteProtocol;
result->initProtocolIsa(cls);
result->size = sizeof(protocol_t);
result->mangledName = strdupIfMutable(name);
return (Protocol *)result;
}
複製代碼
註冊Protocol
。
// 向protocol的哈希表中,註冊新建立的Protocol對象
void objc_registerProtocol(Protocol *proto_gen)
{
protocol_t *proto = newprotocol(proto_gen);
extern objc_class OBJC_CLASS_$___IncompleteProtocol;
Class oldcls = (Class)&OBJC_CLASS_$___IncompleteProtocol;
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
// 若是已經被註冊到哈希表中,則直接返回
if (proto->ISA() == cls) {
return;
}
// 若是當前protocol的isa不是__IncompleteProtocol,表示這個protocol是有問題的,則返回
if (proto->ISA() != oldcls) {
return;
}
proto->changeIsa(cls);
NXMapKeyCopyingInsert(protocols(), proto->mangledName, proto);
}
複製代碼
以前SEL
是由objc_selector
結構體實現的,可是從如今的源碼來看,SEL
是一個const char*
的常量字符串,只是表明一個名字而已。
typedef struct objc_selector *SEL;
複製代碼
爲何說
SEL
只是一個常量字符串呢?咱們在Runtime
源碼中探究一下。
這是在_read_images
函數中SEL list
的實現,主要邏輯是加載SEL list
到內存中,而後經過sel_registerNameNoLock
函數,將全部SEL
都註冊到屬於SEL
的哈希表中。
可是咱們從這段代碼中能夠看出,大部分的SEL
和const char*
的轉換,都是直接進行強制類型轉換的,因此兩者是同一塊內存。
// 將全部SEL都註冊到哈希表中,是另一張哈希表
static size_t UnfixedSelectors;
sel_lock();
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
// 取出的是字符串數組,例如首地址是"class"
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
// sel_cname函數內部就是將SEL強轉爲常量字符串
const char *name = sel_cname(sels[i]);
// 註冊SEL的操做
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
複製代碼
再進入sel_registerNameNoLock
函數中能夠看出,SEL
的哈希表也是將字符串註冊到哈希表中,並非以前的objc_selector
結構體,因此能夠看出如今SEL
就是單純的const char*
常量字符串。
static SEL sel_alloc(const char *name, bool copy)
{
return (SEL)(copy ? strdupIfMutable(name) : name);
}
複製代碼
研究Apple
的源碼時,還能夠經過GNUStep
研究,GNUStep
是蘋果的一套對等交換源碼,將OC代碼以從新實現了一遍,內部實現大體和蘋果的相似。 GNUStep
簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github
上,下載Runtime PDF
合集。把全部Runtime
文章總計九篇,都寫在這個PDF
中,並且左側有目錄,方便閱讀。
下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!😁