這個問題在面試中經常被問起,答案也很明顯: 因爲類的結構已經在編譯期被固定,不能動態更改.
一句話很簡單,但是背後卻有很多的問題,爲什麼方法可以?爲什麼不能允許成員變量和方法一樣動態化?等等問題.
我們先來看看怎麼解決往類中添加成員變量的需求.
- 利用繼承關係,動態創建子類實現
- 利用關聯屬性實現
Func 1 利用繼承關係,動態創建子類實現
既然原來的類已經在編譯期被“固定”,那麼我們動態創建的類總可以添加變量吧,讓新創建的類繼承原來的類不就可以了?
操作一下!
Func1 Step1 創建目標類,我們要往裏邊添加一個成員變量“idCard”
#import <Foundation/Foundation.h>
#import "TestFather.h"
NS_ASSUME_NONNULL_BEGIN
@interface TestSon : TestFather
@property(nonatomic, copy) NSString *sonName;
@end
NS_ASSUME_NONNULL_END
Func1 Step2 動態創建TestSon
的的子類,並添加“idCard”成員變量
// 創建TestSon的子類RelClass
Class relClass = objc_allocateClassPair([TestSon class], "RelClass", 0);
// 向relClass中動態添加“idCard”成員變量,此步驟必須在objc_registerClassPair之前
BOOL success = class_addIvar(relClass, "idCard", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
// 註冊class,此步驟完成才能正式使用這個類
objc_registerClassPair(relClass);
if (success) {
id obj = [[relClass alloc] init];
[obj setValue:@"333333" forKey:@"idCard"];
[obj setValue:@"xxxx" forKey:@"sonName"];
NSLog(@"idCard: %@ \n sonName:%@", [obj valueForKey:@"idCard"],[obj valueForKey:@"sonName"]);
}
輸出結果爲 :
2020-03-04 15:49:06.444594+0800 XSTest[22711:937494] idCard: 333333
sonName:xxxx
可見我們已經達到我們定目的了,通過繼承,再加動態添加成員變量的API,就可以向一個“
已經存在的類”中添加成員變量
注意這裏並不是原來那個類了,我們只是通過這種繼承的方式曲線救國
問題來了,爲什麼class_addIvar
要在objc_registerClassPair
之前進行?objc_allocateClassPair又做了什麼?
以下代碼只留核心邏輯
我們從頭開始捋一捋:
---------------------------Step1---------------------------
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
{
Class cls, meta;
// 判斷名字是否被佔用,判斷父類是否合法
// Fail if the class name is in use.
// Fail if the superclass isn't kosher.
if (getClassExceptSomeSwift(name) ||
!verifySuperclass(superclass, true/*rootOK*/))
{
return nil;
}
// 分配空間
// Allocate new classes.
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);
// 給cls中的各種變量做內存非配和初始化
// fixme mangle the name if it looks swift-y?
objc_initializeClassPair_internal(superclass, name, cls, meta);
return cls;
}
---------------------------Step2各種初始化過程---------------------------
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
runtimeLock.assertLocked();
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;
cls_ro_w->flags = 0;
meta_ro_w->flags = RO_META;
if (!superclass) {
cls_ro_w->flags |= RO_ROOT;
meta_ro_w->flags |= RO_ROOT;
}
if (superclass) {
uint32_t flagsToCopy = RW_FORBIDS_ASSOCIATED_OBJECTS;
cls->data()->flags |= superclass->data()->flags & flagsToCopy;
cls_ro_w->instanceStart = superclass->unalignedInstanceSize();
meta_ro_w->instanceStart = superclass->ISA()->unalignedInstanceSize();
cls->setInstanceSize(cls_ro_w->instanceStart);
meta->setInstanceSize(meta_ro_w->instanceStart);
} else {
cls_ro_w->instanceStart = 0;
meta_ro_w->instanceStart = (uint32_t)sizeof(objc_class);
cls->setInstanceSize((uint32_t)sizeof(id)); // just an isa
meta->setInstanceSize(meta_ro_w->instanceStart);
}
cls_ro_w->name = strdupIfMutable(name);
meta_ro_w->name = strdupIfMutable(name);
cls_ro_w->ivarLayout = &UnsetLayout;
cls_ro_w->weakIvarLayout = &UnsetLayout;
meta->chooseClassArrayIndex();
cls->chooseClassArrayIndex();
// This absolutely needs to be done before addSubclass
// as initializeToEmpty() clobbers the FAST_CACHE bits
cls->cache.initializeToEmpty();
meta->cache.initializeToEmpty();
#if FAST_CACHE_META
meta->cache.setBit(FAST_CACHE_META);
#endif
meta->setInstancesRequireRawIsa();
// Connect to superclasses and metaclasses
cls->initClassIsa(meta);
if (superclass) {
meta->initClassIsa(superclass->ISA()->ISA());
cls->superclass = superclass;
meta->superclass = superclass->ISA();
addSubclass(superclass, cls);
addSubclass(superclass->ISA(), meta);
} else {
meta->initClassIsa(meta);
cls->superclass = Nil;
meta->superclass = cls;
addRootClass(cls);
addSubclass(cls, meta);
}
addClassTableEntry(cls);
}
objc_initializeClassPair_internal
中出現了我們之前分析過的class_ro_t
對此不熟悉的可以返回去看Runtime objc4-756.2 objc_class中class_ro_t與class_rw_t源碼關係分析
在class_ro_t
中有一個屬性就是用來存儲ivar的const ivar_list_t * ivars;
我們可以看到它是const
的,所以初始化後我們不能通過這個ivar_list_t
指針在修改ivars
, 並且在class_ro_t
中還有一個屬性instanceSize
這個屬性代表當前class_ro_t
的內存大小,一旦這個確定了就不能在運行時改變了,在理論上是可以改變的,但是在oc等大多數語言的設計中,這種動態的改變牽扯的問題實在太多,引發的問題也是不能夠用帶來的便捷性去彌補的.所以,在其確定了之後就不能再更改.
那麼爲什麼我們在objc_registerClassPair
之前可以改變它呢?
我們看看registerClassPair做了什麼事情、
--------------------------------Step1--------------------------------
void objc_registerClassPair(Class cls)
{
// Clear "under construction" bit, set "done constructing" bit
cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
// 重點在這!!!!
addNamedClass(cls, cls->data()->ro->name);
}
--------------------------------Step2--------------------------------
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
Class old;
if ((old = getClassExceptSomeSwift(name)) && old != replacing) {
inform_duplicate(name, old, cls);
// 註冊元類
addNonMetaClass(cls);
} else {
// 註冊本類
NXMapInsert(gdb_objc_realized_classes, name, cls);
}
}
--------------------------------Step2.1--------------------------------
static void addNonMetaClass(Class cls)
{
void *old;
old = NXMapInsert(nonMetaClasses(), cls->ISA(), cls);
}
--------------------------------Step End--------------------------------
註冊類最終核心在於將類添加到哈希表中,存儲了所有註冊的類,這個方法就是往表中插入記錄的邏輯,我們一步步解析以下.
void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {
MapPair *pairs = (MapPair *)table->buckets;
// 計算hash表中應該插入的下標
unsigned index = bucketOf(table, key);
// 從pairs開頭做指針偏移找到應該插入的位置
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++;
// 如果滿足這個條件會對hash表進行重新hash的操作,因爲這個表需要在快滿時進行加倍擴容,
// 以保持良好的性能
if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
return NULL;
}
// 如果這class重名,已經存在,並且原有value與現在的value不同,則進行覆蓋
if (isEqual(table, pair->key, key)) {
const void *old = pair->value;
if (old != value) pair->value = value;/* avoid writing unless needed! */
return (void *)old;
} else if (table->count == numBuckets) {
// 表沒有空間了,進行重hash,擴容
/* no room: rehash and retry */
_NXMapRehash(table);
// 擴容完後繼續進行插入操作
return NXMapInsert(table, key, value);
} else {
unsigned index2 = index;
// hash衝突,使用線性探測法,解決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;/* avoid writing unless needed! */
return (void *)old;
}
}
/* no room: can't happen! */
_objc_inform("**** NXMapInsert: bug\n");
return NULL;
}
}
總結一下:
- 編譯期確定了
class_ro_t
空間大小,並設定了instanceSize
,ivars
被const
修飾都決定了編譯期之後不能夠往類中動態添加成員變量. - 在運行時動態創建類時,可以在
objc_allocateClassPair
之後,objc_registerClassPair
之前進行add_Ivar
操作,因爲objc_registerClassPair
中將類信息插入到hash表中是一個註冊的過程,已經註冊,就不能更改了. - 在類信息插入到hash表的過程中有擴容動作,在保證存儲不浪費的前提下也兼顧了運行效率,也有使用線性探測法解決hash衝突的操作,值得我們學習一下.