美文网首页
对象、消息、运行期

对象、消息、运行期

作者: 飞行员suke | 来源:发表于2017-03-20 13:16 被阅读0次

    对象 --- 在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. 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能

    相关文章

      网友评论

          本文标题:对象、消息、运行期

          本文链接:https://www.haomeiwen.com/subject/nnaenttx.html