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
,ivars
被const
修饰都决定了编译期之后不能够往类中动态添加成员变量. - 在运行时动态创建类时,可以在
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都干了什么?