学习Objective-C (二)重拾

objective-c

说来惭愧,上次初窥OC已经过去5个月了,一直没有下文。直到今天才开始接上来。首先,要恭喜我自己,我成功地从一名苦逼的C++码农转型成了一名高大上的iOS攻城狮。今天是我入职第一天,办了入职手续之后,就继续看看OC的基础知识,在此做做笔记。
本文绝大部分知识将是参考Learn Objective-C的。

1. 调用成员方法
在OC中,调用成员方法其实就是给对象发送一个消息。基本的语法是:

[object method];
[object methodWithParam:param];

方法可以有返回值:

output = [object method];
output = [object methodWithParam:param];

我们也可以用类本身而不是实例来调用方法,即类方法,声明时在最前面用+号表示。下面的例子中,string是类NSString的类方法,返回一个NSString对象。

id myObject = [NSString string];

其中,id表示myObject可以指向任意类型的对象,类似于C语言中的void*。这是OC中动态绑定的基础,编译器是不知道myObject的类型的,只有在运行时才能判断。关于动态绑定和多态,将在本文最后讲解。

当然,这里可以使用静态类型,即制定myObject的类型。

NSString* myString = [NSString string];

注意,所有的OC实例变量都是指针类型的,由于id是预编译为指针类型,所以不用显示表示。

在OC中,支持嵌套消息。

[NSString stringWithFormat: [prefs format]];

考虑可读性,尽量避免一行中超过两层的消息嵌套。

OC中多参数的方法用冒号分成几段,比如一个方法的声明是:

-(BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;

那么调用方式:

BOOL res = [myData writeToFile:@"/tmp/log.txt" atomically:NO];

在运行时系统中,这个方法的名字是:

writeToFile:atomically:

这个方法名和一般的C函数有很大区别了,注意两个冒号。

2. 访问成员
OC中的成员变量默认都是私有的,所以需要用访问方法来获取/设置它们的值。一般有两种方式,最原始的是:

[photo setCaption:@"Day at th Beach"];
cap = [photo caption];

第二行的caption不是直接取成员变量,而是调用一个叫caption的方法。
另外一种简单的方式是用点操作符:

 photo.caption = @"Day at the Beach";
 cap = photo.caption;

在一个工程里面,最好统一一种方式。第二种方式只能用于setter和getter

3. 创建对象
前面讲到,创建一个对象可以用下面的方法:

NSString* myString = [NSString string];

实际上许多时候,我们创建一个对象是这样做的:

NSString* myString = [[NSString alloc] init];

两者的区别是前者创建的是autoreleased对象,可以自动释放,
而后者需要手动释放。详见下面的内存管理。

4. 基本的内存管理
如果手动用alloc方式创建对象,你需要释放它,但是你不能手动释放一个autoreleased对象,那将会是程序崩溃。

// string1 will be released automatically
NSString* string1 = [NSString string];
// must release this when done
NSString* string2 = [[NSString  alloc] init];
[string2 *release*];

5. 类的设计与实现
例如一个类的头文件photo.h,声明了类名、基类、成员变量或方法。

#import <Cocoa/Cocoa.h>
@interface Photo : NSObject
{
  NSString* caption;
  NSString* photographer;
}
-(NSString*) caption; //getter
-(NSString*) photographer;//getter
-(void) setCaption: (NSString*)input;//setter
-(void) setPhotographer: (NSString*)input;//setter
@end

对应的实现文件是photo.m:

#import "Photo.h"
@implementation Photo
-(NSString*) caption {
  return caption;
}
-(NSString*) photographer {
  return photographer;
}
-(void) setCaption : (NSString*)input {
  [caption autorelease];
  caption = [input retain];
}
-(void) setPhotographer : (NSString*)input {
  [photographer autorelease];
  photographer = [input retain];
}
@end

如果在一个垃圾可回收的环境中,我们可以直接赋值。

- (void) setCaption: (NSString*)input { 
  caption = input;
} 

但是如果不能垃圾回收,需要release旧的对象,并retain新对象。
通常有两种方式来释放一个对象:release和autorelease。标准的release会立即删除引用,而autorelease会在将来某个时候才删除,一般会保持到当前函数结束,除非你显示改变它。
在setter中,autorelease方法更安全,因为有时候你不想在retain的时候立马release。关于内存管理的详细知识将在下文说明。

我们可以给我们的实例变量创建一个初始化方法。
- (id) init
{
if ( self = [super init] ) {
[self setCaption:@"Default Caption"];
[self setPhotographer:@"Default Photographer"];
}
return self;
}
对应地,有dealloc方法:

- (void) dealloc 
{ 
  [caption release]; 
  [photographer release];
  [super dealloc];
}

类似于C++中的析构函数,先release所有子对象,最后要release超类对象,否则会有内存泄漏。同样,如果有垃圾回收功能,就不用调研dealloc方法了。

6. 详解内存管理
OC的内存管理机制叫做引用计数。你要做的就是追踪你的引用。alloc和retain都会增加一次计数,release会减少一次计数。

reference conunting

实际上,创建一个对象通常有两种原因,一是维护一个成员变量,二是在函数中临时使用。
大部分情况,一个成员变量的setter方法会autorelease老对象,并且retain新对象,你只需要保证在dealloc方法中release这个新对象。
那么,对于函数中的临时变量,只有一个规则:

如果你用alloc或者copy创建一个对象,那就在函数结束时给对象发送release或者autorelease;如果用其他方法创建,Do Nothing!

下面是管理一个成员变量例子:

 -(void) setTotalAmount : (NSNumber*) input
{
  [totalAmount autorelease];
  totalAmount = [input retain];
}
-(void) dealloc
{
  [totalAmount release];
  [super dealloc];
}

下面是局部变量的例子:

NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75];
NSNumber* value2 = [NSNumber numberWithFloat:14.78];

//only release value1, not value2
[value1 release];

只需要release用alloc创建的对象。

7. Logging
OC中的NSLog()函数类似于C语言的printf(),不同的是有个%@格式符,针对对象的。

NSLog( @"The current date and time is: %@", [NSDate date] );

8. 操作Nil对象
OC中的nil对象功能同其他语言中的空指针NULL一样,区别是OC可以调用nil对象的方法而不崩溃。所以即使不事先检查nil,直接调用方法也没问题,只是返回的对象是nil。
基于这种特性我们可以把dealloc写的更好:

-(void) dealloc 
{
  self.caption = nil;
  self.photographer = nil;
  [super dealloc];
}

这种方法也是可行的,因为其相当于setter方法retain nil,而且release老的对象。这样的好处是,不会出现野指针。

注意,这里用self.caption = nil而不是直接caption = nil;因为前者是用setter方法的,会管理好内存,而后者会造成内存泄漏。

9. 范畴(Categories)
Categories是OC最有用的特性之一。Categories可以允许你扩展一个已经存在的类,比如添加方法,而不需要你派生一个子类,也不需要知道这个类的具体实现细节。
当然,你可以对所有内置类添加方法。比如,我们要添加一个方法到NSString类中,用来判断内容是否是一个URL:

#import <Cocoa/Cocoa.h>
@interface NSString  (Utilities)
-(BOOL) isURL;
@end

声明一个categories的方式与声明一个类十分相似,区别是没有基类列表,而且在括号中声明categories名。
下面是实现的代码,这里只是为了展示categories的用法,重点不是实现判断函数。

#import "NSString-Utilities.h"
@implementation NSString (Utilities)
-(BOOL) isURL {
  if ( [self hasPrefix:@"http://"] )
    return YES;
  return NO;
}

现在我们可以对所有的NSString实例使用这个方法了:

NSString* string1 = @"http://pixar.com/";
NSString* string2 = @"Pixar";
if ( [string1 isURL] )
  NSLog (@"string1 is a URL");
if ( [string2 isURL] )
  NSLog (@"string2 is a URL"); 

注意,categories不能添加成员变量,但是可以覆盖已有的方法。
一旦你用categories改变了一个类,那么它会影响到整个应用程序中该类的实例。

10. Self & Super

self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword,它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。在一个子类中不管调用[self class]还是[super class],接受消息的对象都是子类对象。而不同的是,super是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。所以通常情况的init方法实现时,会先用[super init],此时消息的接受者还是本类,只是init方法先调用父类的而已:

-(id) initWithName : (NSString*) vName
            andAge : (int) vAge
         andGender : (NSString*) vGender
{
  //复用父类已有的init方法,值还是赋给self的。
  if(self = [super initWithName:vName andAge:vAge]) {
      self->Gender = vGender;
  }
  return self;
}

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

11. 多态与动态绑定
多态简单说就是对于不同对象响应同一个方法时做出不同的反应。在OC中动态类型id是实现多态的一种方式。动态类型使程序直到执行时才确定对象所属的类型,因而才可以确定实际调用的方法,即动态绑定。
动态类型识别方法:

-(BOOL)isKindOfClass:classObj  //是否是classObj或者它的子类的实例
-(BOOL)isMemberOfClass:classObj  //是否是classObj的实例
-(BOOL)respondsToSelector:selector  //实例是否有这个方法
+(BOOL) instancesRespondToSelector:   //类是否有这个方法
NSClassFromString(NSString*); //由字符串得到类对象
NSStringFromClass([ClassName Class]); // 由类名得到字符串
Class rectClass= [Rectangle class]; //通过类名得到类对象
Class aClass =[anObject class]; //通过实例得到类对象
if([obj1 class]== [obj2 class]); //判断是不是相同类的实例

12. 元类(Meta Class)
我们从id的类型开始分析源码,在obj.h中,id的定义如下:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

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

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

在runtime.h中

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    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;
    #endif
} OBJC2_UNAVAILABLE;

该结构体中,isa 指向所属Class, super_class指向父类别。
在Objective-C的设计哲学中,一切都是对象。Class在设计中本身也是一个对象。而这个Class对象的对应的类,我们叫它 Meta Class,即Class结构体中的 isa 指向的就是它的 Meta Class。我们可以把Meta Class理解为一个Class对象的Class。简单的说:

  • 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类的方法列表里查找
  • 当我们发送一个消息给一个类时,这条消息会在类的Meta Class的方法列表里查找

而 Meta Class本身也是一个Class,它跟其他Class一样也有自己的 isasuper_class 指针。

Class&MetaClass
  • 每个Class都有一个isa指针指向一个唯一的Meta Class
  • 每一个Meta Class的isa指针都指向最上层的Meta Class(图中的NSObject的Meta Class)
  • 最上层的Meta Class的isa指针指向自己,形成一个回路
  • 每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身
  • 最上层的NSObject Class的super class指向 nil

我们看isKindOfClass的源码:

- (BOOL)isKindOf:aClass
{
    Class cls;
    for (cls = isa; cls; cls = cls->superclass) 
        if (cls == (Class)aClass)
            return YES;
    return NO;
}

isMemberOfClass 的源码是:

- (BOOL)isMemberOf:aClass
{
    return isa == (Class)aClass;
}

结合上面讲的isa与MetaClass以及isKindOfClass、isMemberOfClass源码实现。可以知道下面习题的输出:

@interface Sark : NSObject
@end

@implementation Sark
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];

        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

        NSLog(@"%d %d %d %d", res1, res2, res3, res4);
    }
    return 0;
}

输出:

1 0 0 0

13. 选择器(Selector)
相当于C语言的回调函数功能。

SEL is a type that represents a selector in Objective-C. The @selector() keyword returns a SEL that you describe. It's not a function pointer and you can't pass it any objects or references of any kind. For each variable in the selector (method), you have to represent that in the call to @selector.

-(void)methodWithNoArguments;SEL noArgumentSelector = @selector(methodWithNoArguments);
-(void)methodWithOneArgument:(id)argument;SEL oneArgumentSelector = @selector(methodWithOneArgument:); // notice the colon here
-(void)methodWIthTwoArguments:(id)argumentOne and:(id)argumentTwo;SEL twoArgumentSelector = @selector(methodWithTwoArguments:and:); // notice the argument names are omitted

Selectors通常传递给delegate方法,然后在回调时指定执行哪个函数。

@implementation MyObject
-(void)myTimerCallback:(NSTimer*)timer 
{ 
  // do some computations 
  if( timerShouldEnd ) { 
    [timer invalidate]; 
  }
}
@end
// ...
int main(int argc, const char **argv) { 
  // do setup stuff
  MyObject* obj = [[MyObject alloc] init]; 
  SEL mySelector = @selector(myTimerCallback:); 
  [NSTimer scheduledTimerWithTimeInterval:30.0 target:obj     selector:mySelector userInfo:nil repeats:YES];
  // do some tear-down 
  return 0;
}

14. KVC & KVO

KVC - key value coding
KVO - key value observing

在OC中的key是指一个字符串表示的对象的一个属性,与实例变量名以及访问方法同名。
KVC常用的四种方法是:

- (id)valueForKey:(NSString *)key; 
- (void)setValue:(id)value forKey:(NSString *)key; 
- (id)valueForKeyPath:(NSString *)keyPath; 
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

效果与setter/getter方法一样,但是没有setter/gtter方法时也是可以通过这种方法获取/更新属性值的,而且支持多级属性的简便访问方法,即
上面的后两种方法的key路径。
key路径可以用点操作符同时遍历多级属性,比如:

Department对象有manager属性,它是一个指向Employee对象的指针,而Employee对象有一个emergencyContact属性,它是一个指向Person对象的指针,Person对象有一个phoneNumber属性。

那么要了解销售部经理的紧急联系方式,可以这样用KVC:

Department *sales = ...;
Employee *sickEmployee = [sales valueForKey:@"manager"];
Person *personToCall = [sickEmployee valueForKey:@"emergencyContact"];
NSString *numberToDial = [personToCall valueForKey:@"phoneNumber"];

有了key路径,我们可以简便方法:

Department *sales = ...;
NSString *numerToDial = [sales valueForKeyPath:@"manager.emergencyContact.phoneNumber"];

也可以设置属性的值:

Department *sales=...;
[sales setValue:@"1113332223" forKeyPath:@"manager.emergencyContact.phoneNumber"];

KVO提供了一种通知对象属性更新的机制。在OC中的MVC机制中扮演着Model与Controller之间的桥梁作用。
给一个对象属性设置观察者一般有4步:

  • 1 明确是否需要设置KVO。比如一个对象的某个属性发生任意变化时,需要通知另外一个对象的时候。如下图,当BankObject的accountBalance发生任意变化时,PersonObject都需要感知到。


    kvo_objects
  • 2 PersonObeject必须注册为BankObject对象的accountBalance属性的一个观察者。
[backInstance addObserver: personInstance
                 forKeyPath: @"accountBalance"
                    options: NSKeyValueObservingOptionNew
                    context: null]
kvo_objects_connection
  • 3 要响应更新通知,观察者必须实现
    observeValueForKeyPath:ofObject:change:context:方法。


    kvo_objects_implementation
  • 4 改变一个被观察的对象属性值时,
    observeValueForKeyPath:ofObject:change:context:方法会自动被执行。


    kvo_objects_notification

KVO的最重要的优势就是你不需要自己去实现通知机制。

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

推荐阅读更多精彩内容