观“编写高质量iOS与OC 代码的52个有效方法”有感(二)· 对象、消息、运行时

MacBook Pro.jpg

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

  • Objective-C面向对象语言编程。
    对象就是“基本构造单元”,开发者用对象存储并传递数据。
    对象之间传递数据并且执行任务的过程就叫做“消息传递”
    程序运行起来。相关支持的代码就叫做“Objective-C runtime”。

  • “属性”是Objective-C的一项特性,用于封装对象中的数据。
    Objective-C对象会把数据保存为各种实例变量。
    实例变量通过“存取方法”(accessmethod)访问。
    getter 用于读取变量值
    setter用于写入变量值

  • @property
    对象接口的定义中,可以使用属性。 -> 标准的写法
    编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。

@property (nonatomic ,copy) NSString *nameString;

等同于下面的这种写法

- (void)setNameString:(NSString *)nameString;

- (NSString *)nameString;
  • 属性优势:

    • 编译器自动编写与访问这些属性所需的方法 =“自动合成”(autosynthesis)
      这个过程是由编译器在编译时期执行的,所以编辑器看不到这些“合成方法”的源代码。
    • 除了生成方法代码之外,编译器还会自动向类中添加适当类型的实例变量,并且添加下划线作为实例变量的名字。
      比如上面的名称就为_nameString。
  • 自己实现存取方法

    • 使用@dynamic关键字 。告诉编译器不让自动创建实例变量、存取方法!
  • 属性特质

    • 原子性
      默认情况下。编译器合成的方法通过通过锁定机制确实其原子性(atomicity)。
      如果属性具备nonatomic特质,则不使用同步锁。
    • 读/写权限
      readwrite(读写):具有setter和getter方法!
      readonly(只读):仅有getter方法。
    • 内存管理
      assign:针对CGFloat或者NSInteger等“纯量类型”(基础数据类型 和C数据类型)简单赋值,不更改索引计算。
      strong:为属性设置新值时,保留新值,释放旧值,再将新值设置上去。
      weak:既不保留新值,也不释放旧值,同assign。
      copy:与strong类似,但是并不保留新值。而是将其“拷贝”。
      经典案列:NSString 确保对象中的字符串值不会无意间改动。

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

  • 使_XXX直接访问实例变量。
    • 速度快。不经过Objective-C的“方法派发”(method dispatch),编译器直接访问保存对象实例变量的那块内存
    • 直接访问实例变量,不调取“设置方法”(setter)这就绕过了内存管理语义。
    • 直接访问实例变量,也不会触发“键值观测”(Key-Value Observing, KVO)
      这样做是否有问题还是取决于具体的对象行为。
  • 使用self.XXX来访问
    • 有助于排查与之相关的错误,因为可以给“setter”和“getter”设置断点
    • 懒加载也是需要通过“获取方法”来访问属性。否则。实例变量永远不会初始化。
  • 建议:
    除特殊情况。
    在读取实例变量的时候采用直接访问的形式,
    在设置实例变量的时候通过属性来做。

8、理解“对象等同性”这一概念

  • 等同性(equality)
    一般情况下相比较我们都是用的 “==” 但是比较出来的结果未必是我们想要的。
    因为“==”比较是指针本身,而不是其对象。

  • NSObject中的“isEqual”

    • 判断两个对象的等同性。
    • NSObject协议中2个判断等同性的关键方法
    - (BOOL)isEqual:(id)object;
    @property (readonly) NSUInteger hash;
    

定义:如果“isEqual:”方法判断两个对象相等,那么其hash方法也必须返回同一个值。
如果两个对象的hash方法返回同一个值,“isEqual”未必会认为两者相等。

  • NSString实现了一个独有的等同性判断方法:isEqualToString
    该方法比“isEqual”快,因为该方法快递对象规定为NSString。而“isEqual”还要执行额外步骤,因为“isEqual”不知道受测对象类型。

  • NSArray与NSDictionary也有类似的特殊的等同性方法。
    “isEqualToArray”与“isEqualToDictionary”
    如果检测到受测对象不是数组或者字典就会抛出异常。

  • 自己实现等同性方法原理:

    • 首先,判断两个指针是否相等。相等则说明指向同一对象!
    • 其次,比较两个对象所属的类(考虑到父类与子类的判断)
    • 然后,检测每个属性是否相等(不要盲目逐步检查每条属性,而是根据需求来定制)
    • 最后,实现hash方法:
      等同性约定:若两个对象相等,则哈希码相等,但是两个哈希码相同的对象却未必相等。(应使用计算速度快而且哈希码碰撞几率低的算法,否则会影响性能)
  • 小技巧:

    • 如果要重写“isEqual”方法。
      如果受测参数与接收消息对象属于同一个类,就调用自己写的判定方法。否则就交给超类来判断。
    • 等同性的判断深度。
      如果判断两个对象的所有属性是否相等,这样的叫“深度等同性判定”。
      不过更多时候是根据其中部分数据即可判断二者是否等同。

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

  • 类族:把实现细节隐藏在一套简单的公共接口后面。
  • 例子:
  • 系统框架中有很多类族。
    比如UIButton创建的时候的类方法
    UIButton *button = [UIButton buttonWithType:<#(UIButtonType)#>]
    这样该方法返回的对象,决定传入的按钮类型。
  • NSArray与可变类型NSMutableArray。
    有两个抽象基类,一个不可变数组,一个可变数组。
  • 动手创建一个类族:
    • 需求:一个公司分为2种人:1、管理者。2、工人。分别干不同的事!
    • 创建一个基于NSObject的类
#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, SCUserType) {
    SCUserTypeManager,
    SCUserTypeWorker,
};

@interface SCUser : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;


//创建
+ (SCUser *)scUserWithType:(SCUserType)type;

//做事情
- (void)doWork;

@end
  • .m的实现:
#import "SCUser.h"
#import "SCUserWorker.h"
#import "SCUserManager.h"

@implementation SCUser

+ (SCUser *)scUserWithType:(SCUserType)type {
    switch (type) {
        case SCUserTypeManager:
            return [SCUserManager new];
            break;
            
        case SCUserTypeWorker:
            return [SCUserWorker new];
            break;
    }
}

- (void)doWork {
    
}

@end

每个“实体子类” 都是从基类继承来的。比如:

#import "SCUser.h"

@interface SCUserManager : SCUser

@end

//.m的实现
#import "SCUserManager.h"

@implementation SCUserManager

- (void)doWork {
    NSLog(@"管理者巡逻");
}

@end

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

  • 关联对象(Associated Object)
    为了解决某些情况(无法从对象所属的类中继承一个子类,然后用子类对象存放相关信息)
    objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    此方法可以给定的键和策略为某对象设置关联对象值

    objc_getAssociatedObject(id object, const void *key)
    

    此方法根据给定的键从某个对象中获取相关对象值
    objc_removeAssociatedObjects(id object)

    此方法移除指定对象的全部关联对象。

  • 项目中遇到的问题(runtime)
    当时是为了解决NSURL中有个URLWithString的方法会默认把带中文的链接给转码。因为程序中很多地方都用到了这个方法,很明显,一个一个的修改是很费力气的。用到了runtime中的一个交换方法:
    自己实现一个函数,然后与系统的函数交换一下。完美解决问题。
    主要代码:

#import <objc/runtime.h>

@implementation NSURL (Unicode)

+ (void)load {
    /*
     self:UIImage
     谁的事情,谁开头 1.发送消息(对象:objc) 2.注册方法(方法编号:sel) 3.交互方法(方法:method) 4.获取方法(类:class)
     Method:方法名
     
     获取方法,方法保存到类
     Class:获取哪个类方法
     SEL:获取哪个方法
     imageName
     */
    // 获取imageName:方法的地址
    Method URLWithStringMethod = class_getClassMethod(self, @selector(URLWithString:));
    
    // 获取wg_imageWithName:方法的地址
    Method sc_URLWithStringMethod = class_getClassMethod(self, @selector(sc_URLWithString:));
    
    // 交换方法地址,相当于交换实现方式2
    method_exchangeImplementations(URLWithStringMethod, sc_URLWithStringMethod);
    
}


+ (NSURL *)sc_URLWithString:(NSString *)URLString {
    
    NSString *newURLString = [self IsChinese:URLString];
    return [NSURL sc_URLWithString:newURLString];
}

11、理解 objc_msgSend 的作用

  • Objective-C中给对象发消息

    [object message:parameter];
    

    原理:
    objc_msgSend(id self, SEL cmd,...)
    核心函数,这个是“参数个数可变的函数”,能接收两个或者两个以上的参数。
    第一个参数代表接收者。第二个参数代表方法的名字。
    上面的OC代码换成函数就为:
    objc_msgSend(object, @selector(message:),parameter);
    objc_msgSend函数根据接收者和方法名来调用适当的方法。
    过程:
    1.先到所属的类寻找“方法列表”,找到就跳转
    2.找不到,就会沿着继承体系继续向上查找,找到再跳转。
    3.实在找不到就执行“消息转发”
    看起来调用一个方法需要很多步骤。但是objc_msgSend会将匹配结果缓存在“快速映射表”里。这样子执行就快了。

  • 其他的方法:

    // 待发送消息返回结构体
    objc_msgSend_stret
    
    //消息返回的是浮点数
    objc_msgSend_fpret
    
    //给超类发消息  例如[super XXX];
    objc_msgSendSuper

大家码代码时期能更多的了解一些底层的工作原理。在调试的时候会帮助你很多。

12、理解消息转发机制

  • 消息转发:
    因为Objective-C中,在编译期向类发送无法解读的消息并不会报错,因为在运行期还可以继续向类添加方法。因此,编译器在编译时无法确定类中到底会不会有某个方法实现。
    当对象接受到无法解读的消息后,就会启动“消息转发”机制,程序员可以由此告诉对象如何处理未知的消息。
    大家在开发期间肯定见过这样的错误:
unrecognized selector sent to instance 0x610000026560
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SCUserWorker sss]: unrecognized selector sent to instance 0x610000026560'

错误原因:是因为接收者无法理解sss的这个方法名,因此致使程序崩溃。

  • 对象在接收到无法解读的消息后,会依次调用下列方法。
    + (BOOL)resolveInstanceMethod:(SEL)sel
    在这个方法中,参数就是未知的方法名称。在这里你可以解决问题。
    - (id)forwardingTargetForSelector:(SEL)aSelector
    这个是备援接收者,也就是给接收者第二次处理的机会,如果可以找到备援对象则将其返回,若找不到就返回nil。
  • 完整的消息转发
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    启动完整的消息转发机制,首先要创建 NSInvocation 对象,把尚未处理的消息相关的细节全部封与其中(方法名,目标以及参数)。
    使用:只需改变调用目标,使消息在新目标上得以调用就好了,和第二种方法“备援接收者”等效。
消息转发.png
  • 案列:使用class_addMethod动态添加方法
    假设我故意一个类只在.h声明了方法 没有在.m中实现该方法
    结果就会报上面的错,接收者无法解读消息。让你用runtime动态添加方法你会怎么办呢?

    • 考虑
      原因是因为没有实现该方法,所以无法解读,那么我们要为其添加方法。
      那么这个方法添加到哪呢?该如何添加?
    • 动手
      首先找到没有实现方法的那个类,在其.m添加
      + (BOOL)resolveInstanceMethod:(SEL)sel
      这个方法。上述讲到过,无法解读消息时会第一时间调用这个方法,我们可以在这来解决问题。
      接下来要用到runtime中的 class_addMethod 方法
      class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
      1.Class cls : 参数表示添加新方法的类
      2.SEL name : 表示方法名称
      3.IMP imp : 表示由编译器生成的、指向实现方法的指针。也就是说,这个指针指向的方法就是我们要添加的方法。
      4.const char **types :最后一个参数 *types 表示我们要添加的方法的返回值和参数。
      主要代码在下面:
      eat:只声明没有实现的方法。
      SCUser:创建的类。
  • C语言函数的实现
    记得导入
    #import <objc/runtime.h>

void sayHello (id self,SEL _cmd) {
    NSLog(@"Hello");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(eat)) {
        class_addMethod([SCUser class], sel, (IMP)sayHello, "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
};
  • OC形式的实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(eat)) {
        class_addMethod([SCUser class], sel, class_getMethodImplementation(self, @selector(sayHello)), "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
};



- (void)sayHello {
    NSLog(@"hahaha");
}

13、用“方法调配技术”调试“黑盒方法”

  • 在第10条中有介绍过交换方法的案列。
  • 交换方法:
  • 在运行期,向类中新增或者替换方法。
  • 使用另一个方法替换一个方法可以向其添加新功能。
  • 只有调试程序中才会用的运行期修改方法,不宜滥用。

14、理解“类对象”的用意

  • 看下面的代码
    NSString *nameStirng = @"小伙子";
    可以理解:nameString 为存放内存地址的变量。而NSString自身的数据就存在地址中。所有的Objective-C对象都是如此。
  • 还可以这样写:
    id nicknameStirng = @"牛";
    对于通用的对象类型id,因为其自身已经是指针了。所以可以这样写。
  • 比较
    两者语法意义相同,
    唯一区别:如果声明的时候指定了具体类型,那么在该类实例上调用没有的方法时,编译器会发出警告信息。
  • id的定义
typedef struct object {
    Class isa;
} *id;

说明每个对象结构体的首个成员是Class类的变量。通常称为“isa”指针。

  • metaclass
    metaclass就是isa指向的一个结构体。
  • 举例
    用一个例子说明:
    大学期间,小明的辅导员要调查小明家里有没有党员。
    首先,辅导员通过身份证号找到小明的档案,发现小明不是党员,从小明的档案中发现小明父母的身份证号,通过小明父母的身份证号找到小明父母的档案,发现小明父母都是党员。
    身份证号 = isa ,档案 = metaclass。
    (这是作者自己的粗浅理解,如果不对,欢迎指出)
  • 检测继承体系
    • isKindOfClass
      isKindOfClass来确定一个对象是否是一个类的成员,或者是派生自该类的成员。
    • isMemberOfClass
      isMemberOfClass只能确定一个对象是否是当前类的成员。

接下来也将会继续整理。如果觉得有用请点个喜欢!

您的支持将是我继续写作的动力!谢谢。

观“编写高质量iOS与OC X代码的52个有效方法”有感(一)· 熟悉Objective-C
观“编写高质量iOS与OC X代码的52个有效方法”有感(二)· 对象、消息、运行时
观“编写高质量iOS与OC X代码的52个有效方法”有感(三)· 接口与API设计
观“编写高质量iOS与OC X代码的52个有效方法”有感(四)· 协议与分类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容