Runtime objc4-779.1 为什么不能向一个已存在的类添加成员变量?有什么办法达到相同的效果?

Posted Jsen_Wang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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冲突的操作,值得我们学习一下.

以上是关于Runtime objc4-779.1 为什么不能向一个已存在的类添加成员变量?有什么办法达到相同的效果?的主要内容,如果未能解决你的问题,请参考以下文章

Runtime objc4-779.1 为什么不能向一个已存在的类添加成员变量?有什么办法达到相同的效果?

Runtime objc4-779.1 App启动过程中Runtime都干了什么?

Runtime objc4-779.1 OC中,为什么swizzleMethod时要先addMethod?

Runtime objc4-779.1 App启动过程中Runtime都干了什么?

Runtime objc4-779.1 一图看懂iOS Runtime消息转发

Runtime objc4-779.1 通过runtime源码对OC对象销毁过程解析