对象、消息、运行期

2017-03-20  本文已影响0人  飞行员suke

对象 --- 在Objective-C等面向对象语言编程时,“对象”(object)就是“基本构造单元”,开发者可以通过对象来存储并传递数据。
消息 --- 在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)
运行期 --- 当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objectice-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。

六、理解“属性”这一概念

属性(property)是Objective-C的一项特性,用户封装对象中的数据,iOS开发中最常用最方便的变量声明方式,允许我们用点语法来访问对象的实例变量。实质上类似如下

属性 = 成员变量 + set方法 + get方法。

属性特质

@property(nonatomic,readwrite,copy,setter=<name>) NSString *firstName;
  1. 原子性
    如果属性具备noatomic特质,则不使用同步锁,iOS由于性能原因,都是使用noatomic的,MacOS则多用atomic,使用同步锁。

  2. 读写权限
    拥有readwrite特质的属性拥有获取方法getter和设置方法setter。
    拥有readonly特质的属性仅拥有获取方法。可以在.h头文件中对外公开为只读属性,然后再.m的class-continuation分类中将其重新定义为读写属性。

  3. 内存管理语义
    属性用于封装数据,而数据则要有“具体的所有权语义。内存管理语义这个特质仅会影响设置方法。

    1. assign 设置方法只会执行针对纯量类型(非OC对象)的简单赋值操作,如CGFloat,NSInteger

    2. strong 定义了一种拥有关系。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。对象引用计数+1,功能等价于MRC里面的retain

    3. weak 非拥有关系,为这种属性设置新值时,既不保留新值,也不释放旧值。然而在属性值所指对象遭到摧毁时,属性值也会被清空为nil。weak是为打破循环引用而生的

    4. unsafe_unretained 类似weak,但是区别于在当目标对象遭到摧毁时,属性值不会自动清空,这是不安全的。不建议使用!

    5. copy 拥有关系,实质上是设置为传入对象的copy对象的指针。设置完后,与传入的对象无关联。

        @property (nonatomic, copy) NSString *stringCopy  
        -(void)setStringCopy:(NSString *)stringCopy{
            [_stringCopy release];
            _stringCopy = [stringCopy copy];
        }
      

    当属性类型为NSString *时,经常用此特质来保护其封装性

  4. 方法名

     @property(nonatomic,getter=isOn)Bool on;
     //getter=<name> 指定获取方法的方法名
     //setter=<name>,这种用法一般不常用,没必要
    

特别注意:
如果想在其他方法里设置属性值,那么同样要遵守属性定义中所宣称的语义。如下代码:

@interface EOCPerson:NSManagedObject
@property(copy,readonly) NSString *firstName;
@property(copy,readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString*)lastName;
@end

//.m文件中
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString*)lastName{
    if((self = [super init])){
        _firstName = [firstName copy];
        _lastName = [lastName copy];
    }
}

在实现这个自定义初始化方法时,一定要遵循属性定义中宣称的”copy“语义,因为属性定义就相当于类和待设置的属性值之间达成的契约。

七、在对象内部尽量直接访问实例变量

  1. "A:在对象内部读取数据时,应该直接通过实例变量来读。B:写入数据时,则应该通过属性来写。" 这种方案的优点是:既能提高读取操作的速度,又能控制对属性的写入操作,使得相关属性的内存管理语义得以贯彻。
  2. 针对1中A读取内部数据的例外情况是,在使用懒加载初始化技术配置某个数据的话,应该通过属性来读取数据
  3. 针对1中B,写入数据的例外情况是,初始化方法和dealloc方法中,总是应该直接通过实例变量来读写数据

八、理解"对象等同性"这一概念

根据“等同性”(equality)来比较对象是一个非常有用的功能。按照==操作符比较出来的结果未必是我们想要的,应为该操作比较的是两个指针本身,而不是其所指的对象。 我们应该使用NSObject协议中声明的"isEqual":方法来判断两个对象的等同性。

-(BOOL)isEqual:(id)object{
    if(self == object)
        return YES;
    if([self class] != [object class])
        return NO;
    
    EOCPerson *otherPerson = (EOCPerson *)object;
    if(![_firstName isEqualToString:otherPerson.firstName])
        return NO;
    if(![_lastName isEqualToString:otherPerson.lastName])
        return NO;
    if(_age != otherPerson.age)
        return NO;
    return YES;
}

-(NSUInterger)hash {
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}
  1. 若想检测对象的等同性,请提供“isEqual:”与hash方法
  2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
  3. 不要盲目地逐个检测每条属性,而是应该依照具体需求来定制检测方案。
  4. 编写hash方法时,应该使用计算速度快而哈希码碰撞几率低的算法

九、以“类族模式”隐藏实现细节

创建如NSArray类族的子类时,需要遵守几条规则:

  1. 子类应该继承自类族中的抽象基类
  2. 子类应该定义自己的数据存储方式
  3. 子类应当覆写超类文档中指明需要覆写的方法

要点:

  1. 类族模式可以把实现细节隐藏在一套简单的公共接口后面
  2. 系统框架中经常使用类族
  3. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读

十、在既有类中使用关联对象存放自定义数据

“关联对象”(Associated Object)是一个黑科技,在某些类无法继承出子类来存放额外信息时,可以用关联对象的方法,来增加键值对,达到给既定类存放额外信息的目的。

关联类型 等效的@property属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic,copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

有以下方法管理关联对象:

void objc_setAssociatedObject(id object,void *key,id value,objc_AssociationPolicy)  
//该方法以给定的键和策略为某对象设置关联对象值

id objc_getAssociatedObject(id object, void *key)
//此方法根据给定的键从某对象中获取相应的关联对象值

void objc_removeAssociatedObjects(id object)
//此方法移除指定对象的全部关联对象  

特别注意: 设置关联对象时用的键key是个不透明的指针。设置关联对象值时,通常使用静态全局变量做键。

staic void *EOCMyALertViewKey = "EOCMyAlertViewKey";  

//关联对象,绑定block  
objc_setAssociatedObject(alertView,EOCMyAlertViewKey,block,OBJC_ASSOCIATION_COPY);   
//取值,取出block
void(^block)(NSInteger) = objc_getAssociatedObject(alertView,EOCMyAlertViewKey);
  1. 可以通过“关联对象”机制来把两个对象连起来
  2. 定义关联对象时可以指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”
  3. 只有在其他做法不可行时才应选用 关联对象,因为这种做法通常会引入难以查找的bug,如循环引用

十一、理解objc_msgSend的作用

id returnValue = [someObject messageName:parameter];

someObject叫做"接收者"(receiver),messageName叫做“选择子”(selector),选择子与参数合起来称为“消息”(message)。 编译器看到此消息后,将其转换为一条标准的c语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其原型如下

void objc_msgSend(id self,SEL cmd, ...)

编译器会把上面例子中的消息转换为如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法再跳转,如果最终还是找不到相符的方法,那就执行消息转发(message forwarding)操作。
objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行就会很快了。

Objective-C运行环境中的另一些函数
  1. objc_msgSend_stret 如果待发送的消息要返回结构体,就交由该函数处理
  2. objc_msgSend_fpret 如果消息返回的是浮点数,则交由此函数处理
  3. objc_msgSendSuper 如果要给超类发消息,例如[super message:parameter],那么就交由次函数处理

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用技术”,编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的栈幁。

要点:
  1. 消息由接收者、选择子以及参数构成。给某对象“发送消息",也就相当于在该对象上"调用方法"
  2. 发给某对象的全部消息都要由"动态消息派发系统"来处理,该系统会查出对应的方法,并执行其代码

十二、理解消息转发机制

当对象接收无法解读的消息后,就会启动"消息转发"(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。

消息转发分为两大阶段:
  1. 第一阶段先征询接收者,所属的类,看其是否能 动态添加方法,以处理当前这个"未知的选择子",这叫做"动态方法解析"(dynamic method resolution)
  2. 第二阶段涉及"完整的消息转发机制"(full forwarding mechanism),细分为两步:首先,请接收者看看有没有其他对象能处理这条消息,若有,则运行期系统会把消息转个那个对象,消息转发过程结束。其次,若没有备援的接收者(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前未处理的这条消息
动态方法解析

对象在收到无法解读的消息后,首先调用其所属类的下列方法:

+(BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,令其返回值为Boolean类型,<u>表示这个类是否能新增一个实例方法用于处理此选择子</u>。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,叫做"resolveClassMethod:" 。
<u>使用动态方法解析的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了</u>。 此方案常用来实现@dynamic属性,比方说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。

id autoDictionaryGetter(id self,SEL _cmd);
void autoDictionarySetter(id self,SEL _cmd,id value);


+(BOOL)resolveInstanceMethod:(SEL)selector{
    NSString *selectorStirng = NSStringFormSelector(selector);
    if(/* selector is from a @dynamic property */){
        if([selectorString hasPrefix:@"set"]){
            class_addMethod(self,selector,(IMP)autoDictionarySetter,@"v@:@");
        }else{
            class_addMethod(self,selector,(IMP)autoDictionaryGetter,@"@@:"  
        }
        return YES;
    }
    return [super resolveInstanceMethod:selector];
}
备援接收者

当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。

-(id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,否则就返回nil。
值得注意的是,我们无法操作经由这一步所转发的消息,若是想在发送给备援接收者之前修改消息内容,那就得通过完整的消息转发机制来做了。

完整的消息转发

如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。
首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,"消息派发系统"(message-dispatch system)将亲自出马,把消息指派给目标对象。

-(void)forwardInvocation:(NSInvocation *)invocation
消息转发全流程
要点
  1. 若对象无法响应某个选择子,则进入消息转发流程
  2. 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
  3. 对象可以把其无法解读的某些选择子转交给其他对象来处理
  4. 经过上述两步之后,如果还是没有办法处理选择子,那就启动完整的消息转发机制

十三、用"方法调配技术"

类的方法列表会把选择子的名称映射到相关的方法实现之上,使得"动态消息派发系统"能够据此找到应该调用的方法.这些方法均以函数指针的形式来表示,这种指针叫做IMP。其原型如下:

id (*IMP)(id,SEL,...)

可以通过以下c函数互换两个方法实现

void method_exchangeImplementations(Method m1,Method m2)
//此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得
Method class_getInstanceMethod(Class class,SEL aSelector)
//此函数根据给定的选择子从类中取出与之相关的方法

可以用自定义的类中方法交换某个目标类中方法,如下:

@implement NSString (EOCMyAdditions)
-(NSString *)eoc_myLowercaseString{
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@",self,lowercase);
    return lowercase;
}
@end

Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString)];
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod,swappedMethod);  

通过此方案,开发者可以为那些"完全不知道其具体实现的"黑盒方法增加日志记录功能,这非常有助于程序调试,然后此做法只在调试的时候有用。若是滥用,会令代码变得不易读懂且难于维护。

要点
  1. 在运行期,可以向类中新增或替换选择子所对应的方法实现
  2. 使用另一份实现来代替原有的方法实现,这道工序叫做"方法调配",开发者常用此技术向原有实现中添加新功能
  3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种方法不宜滥用

十四、理解"类对象"的用意

类型信息查询: "在运行期检视对象类型"这一操作叫做类型信息查询(introspection,内省),这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。

Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class{
    Class isa;
    Class superClass;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
}

其中super_class 指针确立了继承关系,而isa指针描述了实例所属的类。

"isMemberOfClass:"能够判断出对象是否为某个特定类的实例;而"isKindOfClass:"则能够判断出是否为某类或其派生类的实例

要点:
  1. 每个实例都有一个指向Class对象的指针isa,用以表明其类型,而这些Class对象则构成了类的继承体系
  2. 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知
  3. 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能
上一篇下一篇

猜你喜欢

热点阅读