Runtime objc4-779.1 爲什麼不能向一個已存在的類添加成員變量?有什麼辦法達到相同的效果?

這個問題在面試中經常被問起,答案也很明顯: 因爲類的結構已經在編譯期被固定,不能動態更改.

一句話很簡單,但是背後卻有很多的問題,爲什麼方法可以?爲什麼不能允許成員變量和方法一樣動態化?等等問題.

我們先來看看怎麼解決往類中添加成員變量的需求.

  • 利用繼承關係,動態創建子類實現
  • 利用關聯屬性實現

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,ivarsconst修飾都決定了編譯期之後不能夠往類中動態添加成員變量.
  • 在運行時動態創建類時,可以在objc_allocateClassPair之後,objc_registerClassPair之前進行add_Ivar操作,因爲objc_registerClassPair中將類信息插入到hash表中是一個註冊的過程,已經註冊,就不能更改了.
  • 在類信息插入到hash表的過程中有擴容動作,在保證存儲不浪費的前提下也兼顧了運行效率,也有使用線性探測法解決hash衝突的操作,值得我們學習一下.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章