动态实例变量:解决脆弱的基类问题

来源:互联网 发布:android开源项目源码 编辑:程序博客网 时间:2024/05/21 17:38

原文链接:http://www.cocoawithlove.com/2010/03/dynamic-ivars-solving-fragile-base.html 
作者:ani_di 
版权所有,转载务必保留此链接 http://blog.csdn.net/ani_di

动态实例变量:解决脆弱的基类问题

在现代Objective-C运行时(iPhone OS或64位Mac OS X),你可以动态的添加实例变量到一个类中而不用事先定义。动态变量还可以帮助数据隐藏或抽象,甚至可以解决子类和基类拥有相同的实例变量名而不会引用相同底层数据的混乱情况。

简介

本文是有关Objective-C中的动态实例变量。动态实例变量用于解决脆弱的基类问题,即实例变量的布局。

脆弱的基类问题是基类的很小修改就会破坏子类。实例变量的布局决定了你不能在基类中添加变量而子类不重新编译。

添加实例变量需要在对象指针位置增加变量的偏移,而子类的实例变量总是位于父类后面,而且子类变量的访问通常是在编译时就确定好了,这意味着你不能修改父类的大小,除非子类重新编译以重新计算引用。

大多数面向对象的语言都有类似的问题,添加实例变量几乎不可能在二进制上兼容。

Objectives-C结合现代Objective-C运行时是少数几个在编译语言环境去解决此问题。

动态不是意味着“任何时候”:动态只的是,实例变量的布局在编译时没有确定。事实上,这里的动态是从子类的角度(对于基类,实例变量同之前一样)。 实例变量只能在类关联注册之前(即还没有任何类实例创建)。参考苹果文档class addlvar。

脆弱的基类的实例变量布局

我们先看一个由布局引起错误的例子。

在基类添加实例变量会破坏所有子类

此例子是一个动态链接库(比如苹果写的Cocoa)中允许子类继承:

    @interface LibraryBaseObject : NSObject    {        NSString *baseObjectIVar;    }    @end

库的使用者创建他自己的子类

    @interface UserSubObject : LibraryBaseObject    {        NSString * userSubObjectIVar;    }    @end

这工作的很好,直到库作者想在 LibraryBaseObject 添加新特性

    @interface LibraryBaseObject : NSObject    {        NSString *baseObjectIVar;        id newFeatureObject;    }    @end

传统的方法会破坏所有已存在的子类,因为子类没有分配足够的空间容纳 userSubObjectIVar,它在编译时决定的偏移等于 NSObject + LibraryBaseObject。

简单的用新的头文件重新编译,所有的偏移就可以正确更新。否则没有其他办法。

Greg Parker 的 Hamster Emporium:[objc explain]有个很不错的文章,Non-fragile ivars里面的图表展示了实例变量布局的问题

以前解决脆弱的基类的方法

最常见的方法是这样定义你的基类

    @interface LibraryBaseObject : NSObject    {        id private;    }    @end

把所有的数据保存在private类中,这样 LibraryBaseObject 就只占用一个指针的大小。

当然,这种方法有三个问题:

1. 你必须一开始就添加private 
2. 两级解引用的性能问题 
3. 每次使用 private前,都要强制转换到正确的class

修复方法:让编译时的值成为动态的值

之前,我所说的访问实例变量的方法是:

1. 在对象指针前加上实例变量的偏移,得到绝对偏移  2. 从绝对偏移指向的内存中解引用

对于程序员,这种方法在Algol之后都不是新内容:大部分编译语言访问struct, record或instance都是如此。

显然,要解决此问题,一些东西要在运行时改变。Objective-C运行时的解决方法是“基类实例变量区域的大小”可以在运行时查询。

现代Objective-C运行时访问实例变量变为下面几步:

1. 在对象指针加上子类实例变量的偏移2. 加上父类实例变量区域3. 解引用

一但这样完成,基类的实例变量区域就可以自由增长,子类的偏移随着变动。

所有实例变量都是动态的:程序中所有的实例变量都遵循这样规则,这意味着现代Objective-C运行时的偏移从未在编译时确定

性能影响

你可能担心,这种改变会让一个常见的任务(访问变量)变慢。

是的,加上额外偏移会减慢一些,但非常小。

编译器已对此做了优化,父类区域大小会保存在寄存器中,而且相同变量一般不会重复计算(译注:此处省略一些原文CPU计算)

Synthesizing变量

下一个问题是,我们如何利用这种优势来给一个已存在的类添加实例变量?你可以使用 synthesized property。他可以在实现中创建出实例变量而声明中却不存在。

比如,开始是这样的

    @interface MyIvarlessObject : NSObject    {    }    @end

你可以通过修改声明动态添加变量

    @interface MyIvarlessObject : NSObject    {    }    @property (nonatomic, copy) NSString *myProperty;    @property (nonatomic, copy) NSString *anotherProperty;    @end

并在实现中添加下面的代码

    @synthesize myProperty=myIvar; // a dynamic ivar named myIvar will be generated    @synthesize anotherProperty; // a dynamic ivar named anotherProperty will be generated

由于没有匹配到myIvar或 anotherProperty,这两个实例变量将动态创建。

@synthesize 语句相当于声明。现在你可以这样写

    - (id)init    {        self = [super init];        if (self)        {            myIvar = [[NSString alloc] initWithString:@"someString"];            anotherProperty = [[NSString alloc] initWithString:@"someOtherString"];        }        return self;    }

如果你想隐藏 property 的声明或是不想改变头文件,你可以在实现中增加一个私有 category 在实现里面。像这样:

    @interface MyIvarlessObject ()    @property (nonatomic, copy) NSString *myProperty;    @property (nonatomic, copy) NSString *anotherProperty;    @end

这需要放在 @implement 上面。

名字相同的多个实例变量

想象下面的基类

    @interface BaseObject : NSObject    {    }    @property (nonatomic, copy) NSString *propertyOne;    @end    @implementation BaseObject    @synthesize propertyOne=myIvar;    @end

子类

    @interface SubObject : BaseObject    {    }    @property (nonatomic, copy) NSString *propertyTwo;    @end    @implementation SubObject    @synthesize propertyTwo=myIvar;    @end

两个类都 @synthesize 变量 myIvar。

有些令人奇怪,两个类里面的myIvar并不相同——他们是不同的实例变量,在内存中的位置不同。

为了解决种问题:如果 @synthesize 声明的实例变量与子类冲突,则基类的实例变量依然脆弱。为了支持这一想法,@synthesize 声明的总是当中 @private。

结论

通常你不用担心实例变量的内存布局。只需要在编写或升级动态库,向前兼容时注意。

动态实例变量是“现代”运行时的特性;32位的Mac OS X不支持。

 

原创粉丝点击