Oc语言学习 —— 重点内容总结与拓展(上)
隐藏和封装
有四种访问控制符:==@private(当前类访问权限),@package(与映像访问权限相同),@protect(子类访问权限),@public(公共访问权限)。
访问控制符
1.private(当前类访问权限)
成员变量只能在当前类的内部被访问,用于彻底隐藏成员变量。在类的实现部分定义的成员变量默认使用这种访问权限。
2.package(与映像访问权限相同)
成员变量可以在当前类以及当前类实现的同一个映像的任意地方进行访问。用于部分隐藏成员变量
3.protected(子类访问权限)
成员变量可以在当前类,当前类的子类的任意地方进行访问。类的接口部分定义的成员变量默认这个访问权限
4.public(公共访问权限)
这个成员变量可以在任意地方进行访问。
理解@package访问控制符
@package使受他控制的成员变量不仅可以在当前类访问,还可以在相同映像的其他程序中访问。关键何为相同映像?
同一映像的概念:
简单的说,就是编译后生成的同一个框架或同一个执行文件,当我们想开发一个基础框架时,如果用private就限制的太死了,其他函数可能需要直接访问这个成员变量,但是该框架又不希望外部程序访问我们的成员变量,就可以考虑用package了
当编译器最后把@package限制的成员变量所在的类,其他类,函数编译成一个框架库后,这些类、函数就都在一个映像中(注意这里的函数包括我们的主函数)。也就是说当我们使用@package之后我们的主函数也可以调用我们的成员变量,但是当其他程序引用这个框架库时,由于其他程序与这个框架库不在一个映像中,其他程序就无法访问我们的被@package限制的成员变量。
对象初始化
一、为对象分配空间
无论创建哪个对象,我们都先需要使用alloc方法来分配我们的内存,alloc方法来自NSObject类,而所有的类都是他的子类。
我们调用alloc方法时,系统帮我们完成以下的事情。
1、系统为我们的所有实例变量分配内存空间
2、将每个实例变量的内存空间都置为0,同时对应的类型置为对应的空值。
仅仅分配内存空间的对象还不能使用,还需要执行初始化,也就是init才可以使用它。
二、初始化方法与对象初始化
重写一个init来代替NSObject的init
#import "Person.h"@implementation Person-(id) init {if (self = [super init]) {self.name = @"四川芬达";self.saying = @"他们朝我扔粑粑";self.age = 123;}return self;
}@end
NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject@property (nonatomic,copy) NSString* name;
@property (nonatomic,copy) NSString* saying;
@property (nonatomic,assign) NSInteger age;@endNS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
#import "Person.h"int main(int argc, const char * argv[]) {@autoreleasepool {Person* singer = [[Person alloc] init];NSLog(@"%@",singer.name);NSLog(@"%@",singer.saying);NSLog(@"%ld",singer.age);}return 0;
}
OC属性及属性关键字
@property
属性用于封装对象中数据,属性的本质是 ivar + setter + getter。
可以用 @property 语法来声明属性。@property 会帮我们自动生成属性的 setter 和 getter 方法的声明。
@synthesize
帮我们自动生成 setter 和 getter 方法的实现以及 _ivar。
你还可以通过 @synthesize 来指定实例变量名字,如果你不喜欢默认的以下划线开头来命名实例变量的话。但最好还是用默认的,否则影响可读性。
如果不想令编译器合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法 setter or getter,那么另一个还是会由编译器来合成。但是需要注意的是,如果你实现了属性所需的全部方法(如果属性是 readwrite 则需实现 setter and getter,如果是 readonly 则只需实现 getter 方法),那么编译器就不会自动进行 @synthesize,这时候就不会生成该属性的实例变量,需要根据实际情况自己手动 @synthesize 一下。
@synthesize ivar = _ivar
@dynamic
告诉编译器不用自动进行 @synthesize,你会在运行时再提供这些方法的实现,无需产生警告,但是它不会影响 @property 生成的 setter 和 getter 方法的声明。@dynamic 是 OC 为动态运行时语言的体现。动态运行时语言与编译时语言的区别:动态运行时语言将函数决议推迟到运行时,编译时语言在编译器进行函数决议。
类、元类、父类的关系
我们先给出类与对象源码的定义:
我们的类实际上就是一个结构体指针。
typedef struct objc_class *Class;
struct objc_class { Class isa; Class super_class; const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols;
};
我们再来看看对象的定义:
OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个isa指针,指向其所属的类。那么类的类是什么?就是我们所说的元类,所以,元类就是类的所属类。
这样我们便可以总结出我们类、元类、父类的基本关系:
isa指向
实例对象的所属类实际上就是其所属类,我们的实例对象就是图中的Instance of Subclass。
接下来我们的isa指向了我们的类,那么继续研究我们类的所属类。
从图中可以看出,我们类的所属类实际上就指向了我们的元类。
接下来我们再看我们的元类,我们所有元类的所属类实际上都是根元类,在笔者参考的资料中是这样理解的,因为我们OC中几乎所有的类都是NSObject的子类,所以我们的根元类也可以认为是我们的NSObject的元类。
接着我们再看图中根元类的虚线,根元类所属的类指向了它本身。
superClass指向
我们这里的superClass指向就是我们类的父类,到了这里其实就比较好理解了,元类与我们的类一样,其父类都是一层层往上的,而不像元类所属的类一样,全部指向我们的根元类
在这里唯一需要注意的是我们的根元类的父类是我们的根类,也可以理解为NSObject类。而NSObject类的父类就是nil了。
isKindOfClass && isMemberOfClass
re1:1 [NSObject类对象 isKindOfClass:[NSObject class]]
NSObject 的元类的继承链最终指向 NSObject 类本身(因为根元类的父类是 NSObject),因此返回 YES(1)re2:0 [NSObject类对象 isMemberOfClass:[NSObject class]]
NSObject 的类对象的元类是 NSObject 的元类(metaclass),而非 NSObject 类本身,因此返回 NO(0)。re3:0 [GGObject类对象 isKindOfClass:[GGObject class]]
GGObject 的元类的继承链未指向 GGObject 类本身(除非显式修改元类继承关系),因此 isKindOfClass: 返回 NO(0)。re4:0 [GGObject类对象 isMemberOfClass:[GGObject class]]
类对象的元类与目标类 GGObject 不匹配,因此 isMemberOfClass: 返回 NO(0)。re5:1 [[NSObject分配的对象] isKindOfClass:[NSObject class]]
实例对象是 NSObject 的直接实例,且 isKindOfClass: 会检查类的继承链。
NSObject 是所有类的根类,因此返回 YES(1)。re6:1 [[NSObject分配的对象] isMemberOfClass:[NSObject class]]
实例对象直接属于 NSObject 类,因此 isMemberOfClass: 返回 YES(1)。re7:1 [[GGObject分配的对象] isKindOfClass:[GGObject class]]
GGObject 实例的类继承自 NSObject,但 isKindOfClass: 会检查到其自身类 GGObject,因此返回 YES(1)。re8:1 [[GGObject分配的对象] isMemberOfClass:[GGObject class]]
实例对象直接属于 GGObject 类,因此 isMemberOfClass: 返回 YES(1)。
isKindOfClasss:判断类或元类继承链是否包含目标类
isMemberOfClass:严格匹配当前类或元类是否等于目标类
特殊:NSObject
的元类的父类是 NSObject
类本身,因此 [NSObject class] isKindOfClass:[NSObject class]
返回 YES
,而其他类(如 GGObject
)的元类无此特性
+isKindOfClass 类方法是从当前类的isa指向 (也就是当前类的元类) 开始,沿着 superclass 继承链查找判断和对比类是否相等。
-isKindOfClass 对象方法是从 [self class] (当前类) 开始,沿着 superclass 继承链查找判断和对比类是否相等。
+isMemberOfClass 类方法是直接判断当前类的isa指向 (也就是当前类的元类) 和对比类是否相等。
-isMemberOfClass 对象方法是直接判断 [self class] (当前类) 和对比类是否相等。
编译结果:
关键字
assign
assign:对属性只是简单赋值,不更改对所赋的值的引用计数,这个指示符主要适用于NSInteger等基本类型,以及short、float、double、结构体等各种C数据类型。它是一个弱引用声明类型,我们一般不使用assign来修饰对象,因为被assign修饰的对象,在被释放掉以后,指针的地址还是存在的,也就是说指针不会被置为nil,造成野指针,可能导致程序崩掉。那为什么assign就能用来修饰基本数据类型呢,是因为基本数据类型一般分布在栈上,栈的内存会由系统自动处理,因此不会造成野指针
weak
weak:该属性也是一个弱引用声明类型,使用weak修饰的对象是不会造成引用计数器+1的,并且引用的对象如果被释放了以后会自动变成nil,不会出现野指针,很好的解决了内存引起的崩溃情况。通常我们会在block和协议的时候使用weak修饰,通过这样的修饰,我们可以规避掉循环引用的问题。
strong
strong:该属性是一个强引用声明类型,只要该强引用指向被赋值的对象,那么该对象就不会自动回收。
retain
retain:释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1;在ARC模式下很少使用,通常用于指针变量,就是说你定义了一个变量,然后这个变量在程序的运行过程当中会改变,并且影响到其他方法。一般用于字符串、数组等
copy
copy:若使用copy指示符,则调用setter方法给变量赋值的时候,会将被赋值的对象复制一个副本,再将该副本赋值给成员变量。copy指示符会将原成员变量所引用对象的计数减1。
相当于就是说,不用copy的话,会创建一个新的空间,它的内容和原对象内容一模一样,然后指针是指向新空间的。当再有什么操作在对那个对象操作的话,只是在原空间上操作,对新空间没有影响。当成员变量的类型是可变类型,或其子类是可变类型的时候,被赋值的对象有可能在赋值后被修改,如果程序不需要这种修改影响setter方法设置的成员变量的值,此时就可考虑用copy指示符。
strong、copy
如果属性声明中指定了copy特性,合成方法会使用类的copy方法,这里注意:属性并没有MutableCopy特性。即使是可变的实例变量,也是使用copy特性,正如方法copyWithZone:的执行结果。所以,按照约定会生成一个对象的不可变副本。
相同之处:用于修饰标识拥有关系的对象
不同之处:strong赋值是多个指针指向同一个地址,而copy的复制是每次会在内存中复制一份对象,指针指向不同的地址。所有对于不可变对象我们都应该用copy修饰,为确保对象中的字符串值不会无意变动,应该在设置新属性时拷贝一份。
回顾一下深浅拷贝
深拷贝就是内容拷贝,浅拷贝就是指针拷贝。
深拷贝就是拷贝出来和原来仅仅是值一样,但是内存地址完全不一样的新的对象,创建后和原对象没有任何关系。浅拷贝就是拷贝指向原来对象的指针,使原来的对象的引用计数➕1,可以理解为创建了一个指向原对象的指针的新指针而已,并没有创建一个全新的对象。
#import <Foundation/Foundation.h>
#import "Person.h"int main(int argc, const char * argv[]) {@autoreleasepool {NSMutableString *otherName = [[NSMutableString alloc] initWithString:@"Jack "];Person *person = [[Person alloc] init];person.name = otherName;person.age = 23;[otherName appendString:@"and rose"];NSLog(@"person.name = %@",person.name);}return 0;
}
copy的结果:
strong的结果:
因为otherName是可变的,person.name属性是copy,所以创建了新的字符串,属于深拷贝,内容拷贝,我们拷贝出来了一个对象,后面的赋值操作都是针对新建的对象进行操作,而我们实际的调用还是原本的对象。所以值并不会改变。
如果设置为strong,strong会持有原来的对象,使原来的对象引用计数+1,其实就是浅拷贝、指针拷贝。这时我们进行操作,更改其值就使本对象发生了改变。
我们既然创建的是不可变类型,那我们就不希望其发生改变,所以这个时候我们就应该使用copy关键字,strong会使其在外部被修改时发生改变
那么对于可变类型字符串 ,我们使用copy时,按约定会生成一个对象的不可变副本,此时我们进行增删改操作就会因为找不到对象而崩溃
readonly、readwrite
这是访问权限的控制,决定该属性是否可读和可写,默认是readwrite,所以我们定义属性的时候,一般不需要这个修饰。只有只读属性才加上readonly
readonly来控制读写权限的方式就是只生成setter,不生成getter
单例模式
单例模式是因为在某些时候,程序多次创建这一类的对象没有实际的意义,那么我们就只在程序运行的时候只初始化一次,自然就产生了一个单例模式。
定义:
如果一个类始终只能创建一个实例,则这个类被称为单例类。在程序中,单例类只在程序中被初始化一次,所以单例类是存储在全局区域,在编译时分配内存,只要程序还在运行就一只占用内存,只要在程序结束的时候释放这一部分内存
有三个注意点
- 单例类只有一个实例
- 单例类只能自己创建自己的实例
- 单例类必须给其他对象提供这一实例
#import "Singleton.h"@implementation Singletonstatic Singleton* instance = nil;//静态实例对象
+(instancetype)sharedInstance {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[super allocWithZone:NULL] init];});return instance;
}//重写allocWithZone,防止再次创建新实例
+(id) allocWithZone:(NSZone *)zone {return self;
}//实现copy协议,返回同一个实例
-(id)copyWithZone:(NSZone *)zone {return self;
}//实现mutableCopy协议,返回同一个实例
-(id) mutableCopyWithZone:(NSZone *)zone {return self;
}@end
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Singleton : NSObject <NSCopying, NSMutableCopying>@property (nonatomic, strong) NSString* data;+(instancetype)sharedInstance;@endNS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
#import "Singleton.h"int main(int argc, const char * argv[]) {@autoreleasepool {Singleton *s1 = [Singleton sharedInstance];s1.data = @"Hello, BeijingRDFZ!";Singleton *s2 = [Singleton sharedInstance];NSLog(@"s2.data = %@", s2.data); NSLog(@"s1 == s2 ? %@", (s1 == s2) ? @"YES" : @"NO"); }return 0;
}
我们来打印结果:
重写description方法
我们将我们的值包装成对象后我们肯定就需要打印他了。当我们用NSLog单独打印我们的对象时,实际上是调用我们的description方法来返回我们的类的十六位进制,但是我们并不想要得到这个东西,我们想要得到的是他的值,所以我们就要重写我们的description方法。
@implementation Person- (NSString *)description {return [NSString stringWithFormat:@"Person: name = %@, age = %ld", self.name, (long)self.age];
}@end
==与isEqual与hash
比较方式 | 方法/符号 | 作用 | 是否可重写 | 比较内容 |
---|---|---|---|---|
指针比较 | == | 判断两个对象地址是否相同 | 不可重写 | 内存地址 |
内容比较 | isEqual: | 判断两个对象内容是否相等 | 通常需重写 | 对象内容 |
哈希值比较 | hash | 通常与 isEqual: 搭配使用 | 通常需重写 | 内容生成的整数 |
isEqual 默认是实现是==(也就是地址比较)因此,如果你定义自己的类,要比较内容,必须重写该方法
@implementation Person- (BOOL)isEqual:(id)object {if (![object isKindOfClass:[Person class]]) return NO;Person *other = (Person *)object;return [self.name isEqualToString:other.name] && self.age == other.age;
}
hash:对象的哈希值,用于快速查找和集合操作。默认NSObject会返回一个基于地址的hash。当你重写了isEqual,必须配套重写hash
下面举几个例子:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end@implementation Person- (BOOL)isEqual:(id)object {if (![object isKindOfClass:[Person class]]) return NO;Person *other = (Person *)object;return [self.name isEqualToString:other.name] && self.age == other.age;
}- (NSUInteger)hash {return self.name.hash ^ self.age;
}@end
@interface Book : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *isbn;
@end@implementation Book- (BOOL)isEqual:(id)object {if (![object isKindOfClass:[Book class]]) return NO;return [self.isbn isEqualToString:((Book *)object).isbn];
}- (NSUInteger)hash {return self.isbn.hash; // 因为只看 isbn 是否相同
}@end
@interface UserAccount : NSObject
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@end@implementation UserAccount- (BOOL)isEqual:(id)object {if (![object isKindOfClass:[UserAccount class]]) return NO;UserAccount *other = object;return [self.username isEqualToString:other.username] &&[self.email isEqualToString:other.email];
}- (NSUInteger)hash {return self.username.hash ^ self.email.hash;
}@end
特别鸣谢:感谢iOS各级学长学姐的博客分享