Objective-C(一)-对象、属性、方法

1.对象模型

Objective-C是一门面向对象的语言,对象是我们编程的基本单元,所有的操作都是通过对象。对象其实是对 数据行为 的封装。在OC中,数据的载体就是实例变量,我们可以通过属性便捷的访问到实例变量行为其实就是对象的方法,也可以称为发消息,方法内部可以传递数据和操作数据。

平时我们声明一个对象,例如:NSString *string = @"zzy"; 这条语句的意思是,创建了一个NSString类型的对象实例,实例的内容是zzy,并返回这个实例的内存地址给string这个变量保存,之后我们就可以通过变量string来操作这个实例。学过C语言的都知道,其实*string就是指这是一个NSString类型的指针,所以OC对象的本质其实就是指向某块内存地址的指针。

对象的结构

在OC中每个对象都是一个类的实例,对象的结构如下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

可以看到对象是一个结构体,结构体当中有一个Class类型的成员变量isa。Class的定义为typedef struct objc_class *Class;,这是一个指向objc_class结构体的一个结构体指针。而objc_class其实就是对象所属类的原型:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

可以看到Class中也存放了一个isa指针,所以Class也是一个对象,被称为类对象。同时Class结构体当中还存着:指向父类的super_class指针、类名、版本、实例对象的大小、实例变量列表、方法列表、缓存、协议等信息。通过类创建出来的实例对象所拥有的属性,方法,协议等都是存储在类结构体当中。

实例对象的isa指针指向其所属的类,类结构体当中存放着对象的属性和方法列表。类对象的isa指针指向其所属的元类,元类中存放着类对象的方法列表(类方法),而元类内部也有一个isa指针,指向的是基类的元类,而基类的元类的isa指针指向自身。用一个经典的图类表示整个关系链路:

image

不能向编译后得到的类中增加实例变量:因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。

运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,否则类一旦注册到runtime中后就不能改变实例变量了。

2. 属性

对象中的数据是通过实例变量来保存的,OC提供了一种便捷的访问实例变量的方式:属性。属性的本质就是包括实例变量 + setter方法 + getter方法。实例变量的值保存在对象内部,通过“偏移量(offset)”来保存,即:该变量距离对象内存区域的起始地址的距离,这个是通过硬编码来标识的。当我们声明一个属性之后,系统会自动帮我们生成一个成员变量和属性,这个成员变量和属性的描述(类型,名称等)分别存放在类的ivar_listproperty_list当中,并且生成setter方法和getter方法追加到method_list当中,然后计算属性在对象内部的偏移量,并实现setter方法和getter方法。

声明属性的方式:@property (nonatomic, readwrite, copy) NSString *name; 这个时候编译器会自动帮我们合成实例变量_name。默认的实例变量名为属性名前面加下划线。如果不想用系统默认的实例变量名,也可以通过@synthesize关键字手动指定实例变量名,例如@synthesize name = _myName; 那么我们的实例变量就变成了_myName,不过一般为了规范没有人这么做。

如果不想让系统自动帮我们合成实例变量和settergetter方法,也可以通过关键字@dynamic声明(例如:@dynamic name;),然后自己手动去合成实例变量和相关存取方法。如果没有实现这些合成方法的话,那么是无法正常使用属性的。

目前使用@synthesize的一般场景:

  • 当我们手动实现了settergetter方法的时候,系统默认我们自己管理属性,所以就不再帮我们合成实例变量了,这个时候需要用@synthesize手动合成实例变量,不然编译器就会报错。

  • 当重写父类属性的时候,在子类中需要用@synthesize手动合成实例变量,否则无法使用。

  • 使用了@dynamic的时候。

属性包含的三种语义:

atomic && nonatomic :原子性 && 非原子性

在声明属性的时候,编译器默认的属性语义是atomic原子性的。atomicnonatomic的区别是前者在setter方法赋值的时候会进行加锁保证赋值过程的完整性(安全性),而后者不会使用同步锁。在iOS中,由于频繁加锁会导致性能问题,而且即使采用了atomic,在多线程操作的情况下,也并不能保证线程的安全性。如果要保证线程安全,需要专门进行其他加锁机制处理。所以一般情况下,我们平时声明属性用的都是nonatomic

readwrite && readonly :可读可写 && 只读

readwrite 表示属性可读可写。readonly表示属性只能读取不能修改,一般用于在.h中对外暴露的属性不想被别人修改时这么声明,然后在.m的extension中再重新定义为可读可写。编译器默认的属性语义为readwrite

内存管理语义:strong、weak、copy、assign、unsafe_unretained

  • strong 表示对属性所指对象的一种强引用的关系。当声明为strong时,在setter方法中会先持有新值,再释放旧值,然后再把新值赋值给实例变量。
    eg:
@property (nonatomic, strong) NSMutableArray *array;
- (void)setArray:(NSMutableArray *)array {
   // [array retain];
   // [_array release];
    _array = array;
}
  • weak 表示对属性所指对象的一种弱引用的关系。当声明为weak时,在setter方法中即不会持有新值,也不会释放旧值,只是进行一次简单的赋值操作。但是当属性所指的对象被销毁时,属性值会自动置为nil 比较安全。
    eg:
@property (nonatomic, weak) NSMutableArray *array;
- (void)setArray:(NSMutableArray *)array {
    _array = array;
}
  • copy 类似于strong,也表示对属性所指对象的一种强引用的关系,不同的是,copy语义在setter方法中并不是直接持有新值,而是会拷贝出一份不可变的副本持有,然后再赋值给实例变量。
    eg:
@property (nonatomic, copy) NSString *name;
- (void)setName:(NSString *)name {
    NSString *copyName = [name copy];
    //[_name release];
    _name = copyName;
    //[copyName release]
}

copy语义一般是用于那些具有可变子类的类型如:NSArrayNSDictionaryNSString等。这些类族都有其对应的可变子类,如果声明一个不可变的NSString类型的属性,由于父类指针可以指向子类对象,在给属性赋值的时候,传递给setter方法的值有可能是一个可变子类NSMutableString的实例对象,那么在该属性值赋值完成后,由于属性所指的对象其实是个可变的字符串,就有可能被外界所篡改。所以为了保证属性的安全,在赋值的时候需要先copy出一份不可变的对象,然后再赋值。

  • assign 表示在 setter 方法赋值时只会进行简单的赋值操作,只用于修饰基本类型的数据(例如NSInteger、CGFloat等)。

  • unsafe_unretained 语义类似于assign,不同的是它用于修饰对象类型。如同它的字面意义一样,它不会持有属性所指的对象(类似于weak),但是当属性所指的对象被销毁时,属性值不会自动置为nil,所以它并不安全,可能会导致野指针。

3. 方法

在OC中,方法又被称为发消息,对象需要调用方法来传递数据,而方法是什么呢?例如一个方法:[self doSomething:@"something"];,编译器会将这个方法转为如下的C语言函数:
objc_msgSend(self, @selector(doSomething:), @"something");

objc_msgSend(id self, SEL cmd, ...) 这个函数就是OC消息传递的核心函数。我们平时调用的方法最终都会转为这个函数调用。这个函数接受两个及以上的参数,分别是:消息的接收者消息的签名selector消息的参数。消息的接收者就是该方法的调用者,消息的签名是一个SEL类型的数据,可以理解为方法名的包装,参数就是调用方法所传递的参数,按顺序传入。

当调用这个函数后,objc_msgSend会根据当前消息接收者的isa指针找到其所属的类,然后在类的方法列表中根据selector的名称找到对应的方法并执行,同时会将查找的结果缓存起来以供下次查找时快速的执行。如果找不到,就会根据super_class 指针沿着继承体系一直往上查找,直到找到合适的方法之后跳转到方法的实现并执行。如果直到找到基类还是找不到对应方法的话,那就开始执行消息转发机制。如果消息转发的过程中也没有找到的话,那就抛出异常程序终止。异常信息是常见的unrecognized selector send to instace xxxx

消息转发

上面在介绍方法调用的实现流程时说到了消息转发,消息转发是发生在当给一个对象发送没有实现的方法时会启动消息转发。例如我们给一个对象发送没有实现的消息:

[obj performSelector:@selector(doWork:array:) withObject:@"somthing" withObject:@[@"work"]];

此时消息转发一共分为三个阶段:

第一个阶段:动态方法解析

runtime会先询问对象所属的类,看其能否动态的添加方法以处理当前未处理的消息。处理的方法有两个:

实例方法:+ (BOOL)resolveInstanceMethod:(SEL)sel

类方法:+ (BOOL)resolveClassMethod:(SEL)sel

方法的参数就是当前消息的selector,返回值是来标识当前的类是否能动态的添加方法来处理这个selector
eg:

@interface MessageInvokeObj : NSObject

@end

@implementation MessageInvokeObj

void doWork(id self, SEL cmd, NSString *work, NSArray *array) {
    NSLog(@"work = %@, array = %@", work, array);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"doWork:array:"]) {
        class_addMethod(self, sel, (IMP)doWork, "v@:@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

通过class_addMethod函数动态的添加一个方法来处理当前的消息。此时动态添加方法成功,消息转发结束。

第二个阶段:备援接收者

如果第一个阶段无法处理该消息的话,runtime会询问当前的类能否把这个消息转发给其他对象去处理,如果可以的话,就返回这个对象,否则就返回nil。

处理方法:- (id)forwardingTargetForSelector:(SEL)aSelector

eg:

@interface InvokeMethodObj : NSObject

@end

@implementation InvokeMethodObj

- (void)doWork:(NSString *)work array:(NSArray *)array {
    NSLog(@"work = %@, array = %@", work, array);
}

@end

@interface MessageInvokeObj : NSObject

@end

@implementation MessageInvokeObj

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selString = NSStringFromSelector(aSelector);
    if ([selString isEqualToString:@"doWork:array:"]) {
        InvokeMethodObj *obj = [[InvokeMethodObj alloc] init];
        return obj;
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

第三个阶段: forwardInvocation

最后一个阶段,runtime会将这个消息的所有信息封装到NSInvocation对象当中,包括消息的接收者target、消息的selector以及消息的所有参数。最后一次询问接收者能否处理,如果不能将抛出异常。如果可以,直接把消息指派给目标对象。

先返回正确的方法签名:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

然后执行消息转发:- (void)forwardInvocation:(NSInvocation *)anInvocation

eg:

@interface MessageInvokeObj : NSObject

@end

@implementation MessageInvokeObj

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSString *selString = NSStringFromSelector(aSelector);
    if ([selString isEqualToString:@"doWork:array:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    InvokeMethodObj *obj = [[InvokeMethodObj alloc] init];
    if ([obj respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:obj];
    }
}

@end

OC对象模型大概就是这些,要想更深入的了解,需要读下runtime的源码。下一篇写下OC的内存管理...

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