编写高质量的OC代码--------对象、消息、运行时

第6条:理解“属性”这一概念
    “属性”是Objective-C的一项特性,用于封装对象中的数据。属性通过存取方法访问。编译器在编译期会自动合成一套存取方法,用以访问给定类型中具有给定名称的实例变量。
    使用@synthesize语法可以指定属性对应实例变量的名字。但是不建议这么做,因为如果所有人都使用默认的命名方案,那么写出来的代码大家都能看懂。
    使用@dynamic关键字可以阻止编译器自动合成存取方法。它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。
    属性特质:
        通过“属性特质”可以指定存储数据所需的正确语义。在自定义存取方法时一定要遵从该属性所声明的语义,因为属性特质就相当于“类”和“待设置的属性值”之间所 达成的契约。
        原子性
            默认情况下,编译器所合成的存取方法会通过锁定机制(同步锁)确保其原子性。在iOS程序中,所有的属性都声明为nonatomic。这样做的历史原因是:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是原子的,因为这并不能保证“线程安全”,若要实现线程安全还需要采用更为深层次的锁定机制才行。例如,一个线程在连续多次读取某一个属性值的过程中,有别的线程同时改写了该值,那么即便声明为atomic,页还是会读到不同的值。
        读/写权限
            readwrite 读写  编译器会自动合成存取方法
            readonly 只读 编译器只生成读取方法。可以使用此特质吧某个属性对外公开为只读,然后在“class-continuation分类”中将其重新定义为读写属性。
        内存管理语义:
            assign、strong、weak、unsafe_unretained、copy
        方法名:
            getter = <name> 指定获取方法名称
            setter = <name> 指定设置方法名称

    如果要在其他方法中设置属性值,那么同样要遵守属性定义中所宣称的语义,例如初始化方法。
        
第7条:在对象内部尽量直接访问实例变量
    直接访问实例变量与通过属性访问的区别:
  •     由于不经过Objective-C的“方法派发”步骤,所以直接访问实例变量的速度比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块儿内存。
  •     直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,之后保留新值并释放旧值。
  •    如果直接访问实例变量,那么不会触发KVO通知。
  •    通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和“设置方法”中新增断点。
     合理的折中方案是:在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,直接访问之。此办法即提高读取速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关属性的“内存管理语义”得意贯彻。但是,选用这种做法时,需要注意几个问题。
    1、初始化方法中应该总是直接访问实例变量,因为子类可能覆写设置方法。但是,在某些情况下却又必须在初始化方法中调用设置方法:如果待初始化的实例变量声明在父类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用“设置方法”了。
    2、在使用懒加载的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会被初始化。

第8条:对象等同性
    根据“等同性”来比较对象是一个非常有用的功能。不过,按照==操作符比较出来的结果未必是我们想要的,因为该操作符比较的是两个指针本身,而不是其所指的对象。
    NSObject协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
+ ( NSUInteger )hash;
NSObject类对着两个方法的默认实现是:当且仅当”指针值“(内存地址)完全相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其约定。如果”isEqual:“方法判断两个对象相等,那么hash方法也必须返回同一个值。但是如果hash方法返回同一个值,”isEqual:“方法未必会认为两者相等。
    hash方法的实现:
    下面这种写法完全可行:
-( NSUInteger )hash{
   
return 1234 ;
}

    不过这种写法在集合中使用这种对象将产生性能问题,因为集合在检索哈希表时,会用对象的哈希码做索引。将各个对象按照其哈希吗分装到不同的”箱子数组“中。上述hash实现方法会将所有对象装到同一个”箱子数组“中。会对集合的遍历、插入、删除等操作带了性能影响。
    第二种实现方法:
-( NSUInteger )hash{
   
NSString * stringToHash = [ NSString stringWithFormat : @"%@:%@:%ld" , _firstName , _lastName , _age ];
   
return [stringToHash hash ];
}

    这么做符合约定,但这样做还需负担创建字符串的开销,所有比返回单一值要慢。把这种对象添加到集合中时,也会产生性能问题,因为想要添加,必须先计算哈希吗。
    最后一种方法:
-( NSUInteger )hash{
   
NSUInteger firstNameHash = [ _firstName hash ];
   
NSUInteger lastNameHash = [ _lastName hash ];
   
NSUInteger ageHash = _age ;
   
return firstNameHash^lastNameHash^ageHash;
}

    这种做法既能保持高效,又能使生成的哈希码至少位于一定范围之内,而不过于频繁地重复。编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

    特定类所具有的等同性判定方法。
    对于经常需要判定等同性,那么可能会自己来创建等同性判定方法,因为无需检测参数类型,所以能大大地提升检测速度。自己来编写判定方法的另一个原因是:使代码看上更美观、易读,而且不用再检测两个受测对象的类型。此动机与NSString类”isEqualToString:“类似。
    
    对象实例的等同性判断有时候无序将所有数据逐个比较,只根据其中部分数据即可判明二者是否等同。比方说,对象数据是从数据库中取出来的,那么其中就有可能含有一个”唯一标识符“属性。在这种情况下只需判断”唯一标识符“是否相等即可。

    还有一种情况一定要注意,就是在集合中放入可变类对象的时候。把某个对象放入集合之后,就不应再改变其哈希码了。前面解释过,集合会把各个对象按照其哈希码分装到不同的”箱子数组’。如果某对象放入“箱子”之后哈希码变了,那么其现在所处的箱子对它来说就是“错误”的。

第9条:使用类族模式隐藏实现细节
    类族模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无需自己创建子类实例,只需调用基类方法来创建即可,可惜Objective-C中没有办法指明某个基类是“抽象的”,于是开发者通常会在文档中写明类的用法。
    如果对象所属的类位于某个类族中,那么在查询其类型信息时就要当心了。你可能觉得自己创建了某个类的实例,而实际上创建的却是其子类的实例。
    
    Cocoa里的类族
       系统的框架中有许多类族。大部分集合类都是某个类族的抽象基类。例如NSArray与NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具有公共接口的类有两个,但仍然可以合起来算一个类族。不可变的类定义了所有数组都通用的方法,而可变的类则只定义了那些只适用于可变数组的方法。两个类共属于同一类族,这意味着二者在实现各自类型的数组时可以共用实现代码,此外,还能够把可变数组复制为不可变数组,反之亦然。
    在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某类的实例,此实例充当“占位数组”。该数组稍后会转为另一个类的实例,而这个类就是NSArray的实体子类。
    如下代码判断永远为错:
    NSArray * maybeAnArray = [[ NSArray alloc ] init ];
   
if ([maybeAnArray class ] == [ NSArray class ]) {
       
//will never be hit
       
NSLog ( @"yes" );
    }

    如果你明白NSArray是一个类族,那就会明白上述代码错在哪里:NSArray的初始化方法返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类。不过可以使用isKindOfClass来判断出某个实例所属的类是否位于类族之中。
    对于Cocoa中的类族新增子类需要遵守几条规则:
    1、子类应该继承自类族的抽象基类
    2、子类应该定义自己的数据存储方式
        例如:NSArray的子类必须用一个实例变量来存放数组中的对象。
    3、子类应当覆写超类文档中指明需要覆写的方法

第10条:在既有类中使用关联对象存放自定义数据
    关联对象本事是使用Objective-C运行时动态绑定属性。下列方法可以管理关联对象:
    
void objc_setAssociatedObject( id object, const void *key, id value, 
objc_AssociationPolicy policy)
此方法以给定的键和策略为某对象设置关联对象值

id objc_getAssociatedObject( id object, const void *key)

此方法根据给定的键从某对象中获取相应的关联对象值

void objc_removeAssociatedObjects( id object)
此方法可以移除指定对象的全部关联对象
    
    我们可以把某对象相像成NSDictionary,把关联到该对象的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary对象上调用setObject:forKey:与objectForKey:方法。然而,两者之间有一个重要的差别:设置关联对象时用的键(key)是个“不透明指针(opaque pointer 其所指向的数据结构不局限于某种特定类型的指针)”。如果在两个键上调用“isEqual:”方法的返回值是YES,那么NSDictionary就认为二者相等。然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常用静态全局变量做键。

    第11条:理解objc_msgSend的作用

    Objective-C是C的超集,在讨论Objective-C的方法调用之前,最好先理解C语言的调用方式。
    C语言的“静态绑定”与动态绑定:
    静态绑定:
void printHello(){
   
printf ( "Hello!\n" );
}
void printGoodbye(){
   
printf ( "Goodbye!\n" );
}
void doTheThing( int type){
   
if (type == 0 ) {
       
printHello ();
    }
else {
       
printGoodbye ();
    }
}

    如果不考虑“内联”,那么编译器在编译代码的时候就已经知道程序中有printHello与printGoodbye这两个函数了,于是会直接生产调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。   
 动态绑定:
void printHello(){
   
printf ( "Hello!\n" );
}
void printGoodbye(){
   
printf ( "Goodbye!\n" );
}
void doTheThing( int type){
   
void (*fnc)();
   
if (type == 0 ) {
        fnc =
printHello ;
    }
else {
        fnc =
printGoodbye ;
    }
    fnc();
}

    这时就得使用“动态绑定”了,因为所要调用的函数直到运行期才能确定。待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。
    在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
    给对象发送消息可以这样来写:

    id returnValue = [someObject messageName:parameter];

    编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其“原型”如下:

    void objc_msgSend(id self, SEL cmd,…);

    这是一个可变参函数,第一个参数代表接收者,第二个参数代码方法名,后续参数就是消息中的那些参数。编译器会把刚才那个例子的消息转换如下:

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

    objc_msgSend函数会依据接受者与选择子类型来调用适当的方法。为完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承提醒继续向上查找,等找到合适的方法之后再跳转。如果还是找不到相符的方法,那就执行消息转发。objc_msgSend会将匹配结果缓存在“快速映射表”里面,每个类都有这样一块儿缓存,若稍后还向该类发送与方法名相同的消息,那么执行起来就很快了。
    刚才提到,objc_msgSend等函数一旦找到应该调用的方法之后,就会跳转过去。之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:

    <return_type> Class_selector(id self,SEL _cmd,…);

   每个类里都有一张表格,其中的指针都会指向这种函数,而方法名则是查表时所用的“键”。objc_msgSend等函数正式通过这张表来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和objc_msgSend函数很像。这不是巧合,而是为了利用“尾调用优化”技术,优化消息传递机制。这项优化对于objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,此外,若不优化,还会发生“栈溢出”现象。尾调用的重要性在于它可以不在调用栈上添加一个新的栈帧——而是更新它 (完全不改变调用栈是不可能的,还是需要校正调用栈上形参 与局部变量 的信息 )。

    第12条:消息转发机制
    动态方法解析:
        对象在收到无法解读的消息后,首先将调用其所属类的下列方法:
+(BOOL)resolveInstanceMethod:(SEL)selector;
        假如尚未实现的方法不是实例方法而是类方法,那么运行时系统就会调用另外一个方法,该方法叫做“resolveClassMethod”。
        使用这种办法的前提是:相关方法的实现代码已经写好,只等运行的时候动态插在类里面就可以了。此方法常用来实现@dynamic属性。

    备援接收者:
    如果在动态方法解析返回false,运行时系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:
-(id)forwardingTargetForSelector:(SEL)aSelector;
    通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。请注意,我们无法操作经由这一步所转发的消息。若想在发送给备援接收者之前先修改消息内容,那就必须得通过完整的消息转发机制来做。

    完整的消息转发:
    首先系统会调用以下方法获取方法签名:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    在类的实现上必须覆写该方法,并生成适当的方法签名。运行时系统会获取的方法签名生成 NSInvocation对象,把尚未处理的那条消息有关的细节都封装与其中。此对象包含selector、目标及参数。在创建NSInvocation对象后,“消息派发系统”将亲自出马,把消息指派给目标对象。
    此步骤会调用下列方法来转发消息:
-(void)forwardInvocation:(NSInvocation *)anInvocation;
    这个方法可以实现的很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或改换selector,等等。实现此方法时,若发现某调用操作不应本类处理,需调用其超类的同名方法。

    第13条:methodSwizzle
    methodSwizzle在之前的博客中有深入的实现原理讲解,这里只简单总结一下原理:
    类的方法列表会把selector(方法名)映射到相关的方法实现之上,使得“运行时”能够据此找到应该调用的方法实现。这些方法实现均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
    id(*IMP)(id,SEL,)
    Objective-C运行时系统提供了几个方法能够操控这张表。开发者可以向其中新增selector,也可以改变某selector所对应的方法实现,还可以交换两个selector所映射的指针。
    通过这些特性我们可以达到在运行时,向类中新增或替换selector所对应方法的实现。需要强调的是这种做法在程序中不宜滥用。

   
   第14条:理解“类对象”的用意
    OC中通用的对象类型id的定义
    
typedef struct objc_object {
    Class isa;
} *id;

    由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类。通常称为“is a”指针。Class对象定义如下:
typedef struct objc_class *Class;
struct objc_class {
    Class isa ;
    Class super_class                                        OBJC2_UNAVAILABLE;
   
const char *name                                         OBJC2_UNAVAILABLE;
   
long version                                             OBJC2_UNAVAILABLE;
   
long info                                                OBJC2_UNAVAILABLE;
   
long instance_size                                       OBJC2_UNAVAILABLE;
   
struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
   
struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
   
struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
   
struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
};


    此结构体中存放类的“元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构的首个变量也是isa指针,这说明Class本身也是Objective-C对象,isa指针所指向的类型是另外一个类,叫做“元类”,用来表述类对象本身所具备的元数据(“类方法”就定义于此处,因为这些方法可以理解为类的实例方法,每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”)。结构体里还有变量叫做super_class,它定义了本类的超类。
    假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如下图所示


        即super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行“类型信息查询”。

        可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。
        也可以使用==操作符来比较类对象是否等同,而不要使用比较OC对象时常用的“isEqual:”方法。原因在于,类对象是“单例”,在应用程序范围内,每个类的Class仅有一个实例。即便能这样做,我们也应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。比方说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这个月的对象叫做“代理”,此种对象均以NSProxy为根类。
        通常情况下,如果在此种代理对象上调用class方法,那么返回的是代理对象本身(此类是NSProxy的子类),而非接受的代理的对象所属的类。然而若是改用“isKindOfClass”这样的类型信息 查询方法,那么代理对象就会把这条消息转给“接受代理的对象”。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过class方法所返回的哪个类对象不同。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值