目录
参考书:ios程序设计 Matt Neuburg
存取器和内存管理
存取器
存取器(accessor):用于获取(getter)或设置(setter)实际变量的方法。存取器是与实例变量交互的出入口,便于内存管理及实际变量改变时进行需要的额外操作。
插座变量会使用键值编码对(nib中的)插座变量名及(代码中的)实际变量名进行配对。意味着在代码中应该使用存取器,并缺它们的名字用遵循命名惯例,否则,键值编码就找不到它需要的存取器。
存取器的命名规则:
- setter的名字以set开头,后面紧接着首字母变为大写后的实例变量名。
- getter的名字与实例变量名相同。
键值编码
Cocoa通过实例变量名生成存取器名字的方法是键值编码(Key-Value Coding, KVC)机制。键(key)是一个字符串(NSString),代表需要访问的变量名字。键值编码的基础为NSKeyValueCoding非正式协议(实际为一个类别),NSObject遵循该协议。
键值编码的基础方法为valueForKey: 以及setValue:forKey: 。当调用它们时,对象会进行自我检查,即键值编码会寻找恰当的存取器,如果不存在,则直接访问实例变量。键值编码能够彻底绕过实例变量的私密性,重写accessInstanceVariablesDirectly并返回NO(默认返回YES)可避免。如果一个类实现了通过键进行访问的方法或拥有通过该键访问的实际变量,就说这个类在该键上具有键值编码兼容性。
基于NSDictionary使用dictionaryWithValueForKey: 与setValueForKeysWithDictionary: 可一次性获取或设置多个键值对。
键值编码对于Cocoa内置类的一些特殊用法:
- 向NSArray发送valueForKey: ,它会向其中的每一个元素发送valueForKey:并返回一个新的包含调用结果的数组。NSSet的行为于此相似。
- NSSortDescriptor通过向每一个元素发valueForKey: 排列NSArray。
- NSDictionary将valueForKay: 实现与objectForKey: 相同(在操作字典的数组时非常有用)。NSUserDefaults也是如此。
- CALayer以及CAAnimation支持用键值编码来定义和获取一个键的值,就像它们本身就是字典。
健路径(key path)支持将多个键连接到一个表达式中。如果对象在某个键上具有键值编码兼容性,而该键所对应的值本身又在另一个键上具有键值编码兼容性,那就能在调用valueForKeyPath: 或setValue: forKeyPath: 时将两个键连接起来。键路径看上去就是一串用点(“.”)连接起来的键名。eg: valueForKeyPath: @"key1.key2"会先在消息接受者上用@"key1"键调用valueForKey: 方法,获取了方法返回的对象后,在这个对象上用@"key2"键调用valueForKey: 方法。
键值编码的另一个功能是允许在对象里实现对应值为数组(或集合)的键,即使这个值本身并不是这样。
//
// main.m
#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject * myobj1 = [[MyObject alloc]initWithArray:@[@"one", @"two", @"three", @{@"one":[NSNumber numberWithInteger:1], @"two":[NSNumber numberWithInteger:2]}]];
NSArray * arr1 = [myobj1 valueForKey:@"array"];
/*log
//countOfArray
//countOfArray
//objectInArrayAtIndex:
//objectInArrayAtIndex:
//objectInArrayAtIndex:
//objectInArrayAtIndex:
*/
if(arr1){
NSLog(@"%@", arr1);
/*log
//(one, two, three, { one = 1; two = 2;})
*/
}
NSLog(@"[arr1 count]: %lu", [arr1 count]);
/*log
countOfArray
[arr1 count]: 4
*/
NSLog(@"[arr1 objectAtIndex:0]: %@", [arr1 objectAtIndex:0]);
/*log
objectInArrayAtIndex:
[arr1 objectAtIndex:0]: one
*/
MyObject * myobj2 = [[MyObject alloc]init];
NSArray * arr2 = [myobj2 valueForKey:@"array"];
NSLog(@"[arr2 count]: %lu", [arr2 count]);
/*log
countOfArray
[arr2 count]: 18446744073709551615
*/
NSLog(@"[arr2 objectAtIndex:0]: %@", [arr2 objectAtIndex:0]);
/*log
objectInArrayAtIndex:
[arr2 objectAtIndex:0]: (null)
*/
}
return 0;
}
//
// MyObject.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyObject : NSObject
@property (retain, nonatomic)NSArray * titles;
@property (assign, nonatomic)int num;
- (id)initWithArray:(NSArray*)titles;
//- (NSInteger)countOfArray;//可缺省
//- (id) objectInArrayAtIndex:(NSUInteger)index;//可缺省
@end
NS_ASSUME_NONNULL_END
//
// MyObject.m
#import "MyObject.h"
@implementation MyObject
- (id)initWithArray:(NSArray*)titles{
self = [super init];
if(self){
_titles = [NSArray arrayWithArray:titles];
}
return self;
}
- (NSInteger)countOfArray{
NSLog(@"countOfArray");
if(_titles){
return _titles.count;
}
return -1;
}
- (id) objectInArrayAtIndex:(NSUInteger)index{
NSLog(@"objectInArrayAtIndex:");
if(_titles){
if(index >= _titles.count){
return @"error";
}
return [_titles objectAtIndex:index];
}
return nil;
}
@end
// NSArray
- (NSUInteger) countOfMyArray{
return [self.titles count];
}
- (id) objectInMyArrayAtIndex: (NSUInteger)ix{
return [self.titles objectAtIndex: ix];
}
// NSMutableArray
- (void) insertObject: (id)val inMyArrayAtIndex: (NSUInteger) ix{
[self.titles insertObject: val atIndex: ix];
}
- (void) removeObjectFromMyArrayAtIndex: (NSUInteger) is{
[self.titles removeObjectAtIndex: ix];
}
内存管理
(补充)
内存泄漏memory leak:占用了内存,但没有其他对象有它的一个引用。
悬挂指针(野指针)dangling pointer:指针所指向的对象被悄悄销毁。
当向一个实例对象发送copy消息时,产生的新对象(副本)的保留计数也是递增了1的。
基于ARC的工程中使非ARC的代码:让非ARC只存在于一部分文件中,编辑Target(目标),切换到Build Plases(生成阶段)标签下,在Compole Sources(编译源代码)部分中,双击清单中的非ARC文件,并键入 -fno-objc-arc (以此输入到Compiler Flags(编译器标记)列中)。
实例变量的内存管理(非ARC)
存取器的标准模版
//保留对象的简单setter
- (void) setTheData: (NSMutableArray*) value {
if(self->theData != value) {
[self->theData release];
self->theData = [value retain];
}
}
实例变量的内存管理(ARC)
在ARC下,可能因为一些其他原因实现dealloc--例如,用于注销通知,但不需要对任何实例变量调用release,也不需要调用super。
保留循环和弱引用
保留循环(retain cycle)是指对象A和对象B相互保留对方的情形。使用弱引用(weak reference)来避免向实例变量赋值时保留该对象。前面提到NSTimer,如果将timer强引用保存在对象中,对象可能无法进行dealloc。在声明实例对象时使用__weak限符。
//
// main.m
#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject * myObj = [[MyObject alloc] init];
[myObj start];
NSLog(@"%@", [NSDate date]);
for(int i = 0; i < 60000; i++){
for(int j = 0; j < 9000; j++)
;
}
NSLog(@"%@", [NSDate date]);
}
return 0;
}
//
// MyObject.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyObject : NSObject
- (void)myMethod;
- (void)start;
- (id)timer;
@end
NS_ASSUME_NONNULL_END
//
// MyObject.m
#import "MyObject.h"
@implementation MyObject{
__weak NSTimer * timer;
}
- (void)start{
self->timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"This is block~");
}];
NSLog(@"%hhd", [self->timer isValid]);
}
- (void)myMethod{
NSLog(@"This is myMethod");
}
- (void)dealloc{
NSLog(@"%hhd", [self->timer isValid]);//1
[self->timer invalidate];
NSLog(@"%hhd", [self->timer isValid]);//0
NSLog(@"dealloc...");
}
- (id)timer{
if(!self->timer){
NSLog(@"%f", timer.timeInterval);
}
return self->timer;
}
@end
如果在块对象中遇到了self,即使是简洁的(意思是即使只使用到了实例变量),块对象的副本就睡保留self。能够把块对象self引用变为弱引用来避免这种情况。一般称“waek-strong dance”。
// Weak-strong Dance:避免块对象的副本保留self
- (void) start {
__weak MyClass * wself = self;//建立一个对self的本地弱引用。它在块对象之外,但块对象能看见它。
self->observer = [[NSNotificationCenter defaultCenter]
addObserverForName: @"heyho"
object: nil queue: nil usingBlock: ^(NSNotification *n){
MyClass * sself = wself;//在块对象内,使用这个弱引用建立一般引用。
if(sslef){
NSLog(@"%@", sslef;);//在块对象内任何需要用到self的地方使用刚刚建立的一般引用。
}
}];
}
指向void的上下文信息指针的内存管理
很多Cocoa方法接受可选的类型为void* 的参数,并经常将其橙味context: 。id是一个万能指针对象类型,而void* 仅仅是一个C语言指针,Cocoa不会将其视为对象,则void*类型时Cocoa不会对其进行任何内存管理的提示。
在ARC中,当对象实例化后,自始至终都会有ARC进行内存管理。但是一旦对象转换为void* 类型,就超出ARC内存管理的领域。如果不提供更多的信息,ARC不明白此后它内粗管理的职责,因此ARC不会轻易放手(不易转换)。类似的,把一个非对象(一个void* )转换为对象类型时,它将建立好的东西传入ARC内存管理的领域,ARC也不会在没有更多信息的情况下轻易接受它。
一般来说,当把兑现作为context: 的参数传入时,最简单的方法就是自己保留该对象的引用,并且通过这个引用进行内存管理。这样,当context:参数离开ARC的管理权限(并在之后回来)后,ARC无需进行内存管理的操作。具体方法是使用__bridge限定符对这个值进行显示转换,
CFTypeRef的内存管理
ARC不管理CFTypeRef,需要手动管理CFTypeRef的内存。规则:如果通过名称中含有Create或Copy的函数获取一个对象,就需要负责释放它。对于一个Core Foundation对象(其类型名以CF开头)来说,需要使用CFRelease释放它;其他用于创建对象的函数也有用于释放对象的类时函数。PS:即使某个Objective-C对象是nil也能向它发送消息,但CFTypeRef却不能接受NULL参数。在释放CFTypeRef前要确保它不是NULL。
//例子
- (void) addPattern: (CGContextRef) context color: (CGColorRef) incolor{
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(baseSpace);
CGContextSetFillColorSpace(Context, patternSpace);
CGColorSpaceRelease(patternSpace);
CGColorSpaceRelease(baseSpace);
}
在ARC之前可以用Create或Copy函数获取一个CFStringRef,将其转换为NSString,再在以后向产生NSString发送release。在ARC下不能使用release,但能让ARC来做玩去啊相同的事情:在进行桥接时,需要使用CFBridgingRelease函数来传递这个CFString。函数返回的id可以赋给NSString变量,之后ARC会释放它来平衡Create或Copy函数递增的保留计数。三种通过桥接进行类型转换:
- __bridge转换:将对象桥接到对应的类型。意味着桥接对象双方的内存管理职责是相互独立的。这个转换告知ARC:你会手动在CFTypeRef这一段进行正确而完整的内存管理。
- CFBridgingRelease函数(已Retain,ARC需进行Release):正在将CFTypeRed桥接到对象。这个函数告诉ARC该对象的内存管理是不完整的:它在CFTypeRef一端已经递增了保留计数(可以通过Create或Copy函数创建它,或者对其调用CFRetain),并且最终需要ARC在对象一端进行相应的释放操作。(也可以使用 __bridge_transfer 转换达到同样的效果)
- CFBridgingRetain函数(ARC已Retain,需手动Release):正在将对象桥接到CFTypeRef。这个函数告诉ARC就这样让对象的内存管理不完整:你已经知道对象递增了保留计数,你会负责在CFTypeRef一端调用CFRelease。(或者使用__bridge_retained转换达到相同的效果。)
属性
(补充)
属性(property)是允许使用点表示法调用存取器的简便语法。
在ARC中:
- strong与retain等效,保留新值,释放旧值(之前的值)。
- copy:类似strong和retain,但保留副本(副本保留计数递增1)。
- weak:直接赋值,指向值消失时本身失效(nil)。
- assign(默认):用在与weak相同的地方。setter完全不管理内存,传入的值会直接赋给实例变量。然而,实例变量不是ARC的弱引用,当其指向的实例消失时,它本身不会自动失效;这是一个非ARC的弱引用(__unsafe_unretained),可能会变成悬挂指针。
@synthesize指令:请求合成存取器。以下注释部分为指令的另一个例子。
//
// main.m
#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject * myObj = [[MyObject alloc] initWithName:@"Dude"];
[myObj printName];
}
return 0;
}
//
// MyObject.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyObject : NSObject
@property (strong, nonatomic)NSString * name;
- (id)initWithName:(NSString *)n;
- (void)printName;
@end
NS_ASSUME_NONNULL_END
//
// MyObject.m
#import "MyObject.h"
@implementation MyObject
//- (id)initWithName:(NSString *)n{
// self = [self init];
// if(self){
// _name = n;
// }
// return self;
//}
//
//- (void)printName{
// NSLog(@"_name: %@", _name);
NSLog(@"name%@", name);//error
NSLog(@"self._name:%@", self._name);//error
// NSLog(@"self.name: %@", self.name);
// NSLog(@"[self name]: %@", [self name]);
//}
//
//@synthesize name = _name;
- (id)initWithName:(NSString *)n{
self = [self init];
if(self){
myname = n;
}
return self;
}
- (void)printName{
NSLog(@"myname: %@", myname);
// NSLog(@"_name%@", _name);//error
NSLog(@"self.name: %@", self.name);
NSLog(@"[self name]: %@", [self name]);
// NSLog(@"[self myname]: %@", [self myname]);
}
@synthesize name = myname;
//@synthesize propertyName = ivarName;
//using ivarName in method insteads of propertyName
@end
当希望其他类只知道一个属性时readonly的,而它对于类内部的代码来说是readwrite的。可在头文件中类的接口部分将属性声明为readonly,这是外部代码看到的内容。之后在实现文件的匿名类别中重声明它,并不指定为readonly(默认为readwrite),这是只有类内部才看得到的内容。两个属性声明中的其他特性必须完全一致。