-
继承的概念
继承:指一个对象直接使用另一对象的非私有化属性和方法。
继承的实现:
#import “SuperClass.h”
@interface subClass : SuperClass
@end
导入父类的.h文件,无需在导入Foundation框架,因为父类中已经存在了。
在子类的冒号前写上父类的类名。
NSObject是任何类的父类
实例:
父类:Father
#import <Foundation/Foundation.h>
@interface Father : NSObject
{
NSString *_name;
}
- (void)showName;
@end
@implementation Father
- (void)showName
{
_name = @"父类属性";
NSLog(@"父类方法");
}
@end
子类
#import "Father.h"
@interface Son : Father
@end
#import "Son.h"
@implementation Son
@end
main函数中测试:
从上面的实例当中我们可以看到,父类Father具有属性_name和showName方法
而子类Son 中什么都没有定义。但是在main函数中,通过实例化,我们却能够使用父类的方法,说明了它们之间存在着继承关系。子类可以使用父类的非私有属性和方法。
实验:1.我们可以将父类的属性访问修饰符设置为@private,你会发现,在子类当中无法直接调用被@private所修饰的属性。
2.在父类中不声明的方法,皆为私有方法。可以方法,在子类中无法直接访问父类的私有方法。
解决办法:没有绝对的私有化,我们可以通过公共和私有相结合,让父类的公共调用私有,子类是可以继承父类的公共属性或者方法的,这样就解决了无法使用父类私有的属性和方法的问题。
-
重写
重写:对父类已有方法进行重新编写。
为什么要重写?
我们通过继承,可以很方便的获取父类已经拥有的方法,但是很多时候,父类的功能不能完全符合我们对于项目的要求,这个时候,我们就需要用到重写的功能,调用父类的方法,对其重新编写。当然,如果想保留父类的功能 可以使用[super xxx];使用super关键字来获取父类的原有方法。
实例:继续沿用上面的实例,只需要改动如下
@implementation Son
- (void)showName
{
NSLog(@"子类方法");
}
@end
根据结果可以看到,由于我们重写了showName方法,在main函数被调用的方法就变成了子类当中的showName方法。可以使用command+坐标左键 点击看这个方法会跳跃到哪里。
-
多态的概念
多态:编译时的类型和运行时的类型不同即在相同情况下表现出多种状态。
没有继承就没有多态
代码的体现:父类类型的指针指向子类对象
好处:如果函数方法参数中使用的是父类类型,则可以传入父类和子类对象,而不用再去定义多个函数来和相应的类进行匹配了。
局限性:父类类型的变量不能直接调用子类特有的方法,如果必须要调用,则必须强制转换为子类特有的方法。
-
多态在代码中的表现
故事背景:有一个父亲。开了一个厂,这个厂在他手里赚了100W,但是过了几年,传到他儿子手里,经过重新装修,改良了技术后,赚了2个亿。
//父类
#import <Foundation/Foundation.h>
@interface Father : NSObject
- (void)createAFactory;
- (void)getInfoWithTheFactory:(Father *)father;
@end
@implementation Father
- (void)createAFactory
{
NSLog(@"-----------------");
NSLog(@"工厂名称:加工厂");
NSLog(@"工厂年收益:100W");
NSLog(@"厂长:爸爸");
NSLog(@"-----------------");
}
- (void)getInfoWithTheFactory:(Father *)father
{
[father createAFactory];
}
@end
//子类:Sub
#import "Father.h"
@interface Sub : Father
@end
@implementation Sub
//重写
- (void)createAFactory
{
NSLog(@"-----------------");
NSLog(@"工厂名称:加工厂");
NSLog(@"工厂年收益:20000W");
NSLog(@"厂长:儿子");
NSLog(@"-----------------");
}
- (void)getInfoWithTheFactory:(Sub *)father
{
[father createAFactory];
}
@end
// main函数中测试:
#import <Foundation/Foundation.h>
#import "Father.h"
#import "Sub.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
father的变量,sub的实例
Father *father = [[Sub alloc]init];
在运行时 father 会被转换成 sub类型
[father getInfoWithTheFactory:father];
}
return 0;
}
通过上面的实例,我们可以看出 ,定义的时候明明是Father类型,却在最后运行的运行处了子类的重写方法。由此判定,在实例化的过程中,原本属于Father类的变量,编程了Sub类的实例。
我们分步骤分析:
编译时:Father *father; 定义了属于Father类的变量
运行时:father = [Sub alloc]; Sub类进行实例化操作,将结果给了father,从这里,father就应该属于Sub了.
运行时:father = [father init]; Sub类初始化
从上面的分析来看,在alloc的时候,分配的并不是属于Father的内存地址,而是Sub的内存地址,所以该实例内存当中只会储存属于Sub的方法。当然,如果getInfoWithTheFactory方法没有被重写,那么将会调用父类的getInfoWithTheFactory方法。
-
扩展:OC的消息机制 - RunTime
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。假如在OC中写了这样的一个代码:
[obj setMessage];
其中obj是一个对象setMessage是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(setMessage));
@selector (setMessage):这是一个SEL方法选择器。
下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。
1. 首先编译器将代码[obj setMessage];转化为objc_msgSend(obj, @selector (setMessage));
2. 在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。
3.在Class中先去cache中 通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度).
4.若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。
5.若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
扩展:SEL 类型和IMP 函数指针
SEL 是一种方法选择器,通过@select(方法名)搜寻从当前类开始,再到父类,一直到NSObject为止的所有符合条件的方法。
SEL其主要作用是快速的通过方法名字(setMessage)查找到对应方法的函数指针,然后调用其函 数。SEL其本身是一个int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同 的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
下边代码我们来看看如何来生成一个SEL
#import <Foundation/Foundation.h>
@interface MessageTest : NSObject
//无参方法
- (void)messageFunction;
//带参方法
- (void)sendMessage:(NSString *)msg;
//多参方法
- (void)sendMessage:(NSString *)msg moreMsg:(NSString *)moreMsg;
//通过SEL来调用上面的方法
- (void)performMethodsForSelectors;
@end
#import "MessageTest.h"
@implementation MessageTest
- (void)messageFunction
{
NSLog(@"消息方法");
}
- (void)sendMessage:(NSString *)msg
{
NSLog(@"发送消息:%@",msg);
}
- (void)sendMessage:(NSString *)msg moreMsg:(NSString *)moreMsg
{
NSLog(@"发送消息:%@,更多消息:%@",msg,moreMsg);
}
- (void)performMethodsForSelectors{
//调用无参方法
[self performSelector:@selector(messageFunction)];
//调用带一个参数方法
[self performSelector:@selector(sendMessage:) withObject:@"一个参数方法"];
//调用带两个参数方法
[self performSelector:@selector(sendMessage:moreMsg:) withObject:@"第一个参数" withObject:@"第二个参数"];
}
@end
实例中出现了三种方法:
performSelector:@selector()
performSelector:@selector() withObject:
performSelector:@selector() withObject:
withObject:
分别是调用
1.没有参数的方法
2.调用有一个参数的方法,参数的值由withObject的参数决定。
3.调用有两个参数的方法,参数的值有withObject的参数决定。
SEL 作为参数的传递
#import <Foundation/Foundation.h>
@interface MessageTest : NSObject
- (void)function;
//参数类型为SEL
- (void)getMethodForParameter:(SEL)fun;
@end
@implementation MessageTest
- (void)function
{
NSLog(@"用于查询的方法");
}
- (void)getMethodForParameter:(SEL)fun
{
//1.如果使用了ARC会产生“performSelector may cause a leak because its selector is unknown”警告
//2.这种方式当传入一个不符合约定的消息时会继续运行并不报错。例如应该传入2个参数,但只传入1个参数。或传入了3个参数,第三个参数不会被初始化。
[self performSelector:fun];
}
@end
实例中的方式三,采用的NSSelectorFromString方法:
SEL NSSelectorFromString(NSString *aSelectorName);
可以看出,该方法的返回类型是SEL 可以直接用SEL类型变量获取。
通过selector 的使用可以看出,OC中没有真正的私有方法,通过SEL类型的方法选择器,可以任意获取。
IMP 函数指针的使用:
#import <Foundation/Foundation.h>
@interface Father : NSObject
{
int age;
}
@end
#import "Father.h"
#pragma mark
@implementation Father
//私有方法
- (void)displayWithAge
{
age = 10;
NSLog(@"age:%d",age);
}
@end
从上面的例子可以看出,Father类当中有一个私有方法,在外界无法访问,但程序设计不可能设计的那么绝对,所以今天我们来探讨如何用IMP指针来底层寻找方
#import <Foundation/Foundation.h>
#import "Father.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Father *fa = [[Father alloc]init];
//寻找displayWithAge方法
SEL selector = NSSelectorFromString(@"displayWithAge");
//利用IMP指针指向displayWithAge方法
IMP imp = [fa methodForSelector:selector];
//将imp_func方法指针变量设定为一种类型
typedef void (*imp_func)(id, SEL);
// 将imp所指向的方法赋值给imp_f方法指针
imp_func imp_f = (imp_func)imp;
/*调用这个方法
fa:为在哪个对象中寻找
selector:调用的方法
*/
imp_f(fa, selector);
}
return 0;
}
-
扩展:NSObject
首先我们来一步一步解开NSObject类的面纱:
使用command+鼠标左键 点击 NSObject
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在同理点击Class
typedef struct objc_class *Class;
再点击 objc_class
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
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
#endif
}
到这里,我们基本上就已经明白了一个类的组成了。
一个完整的类 包含:
Class isa; 指向metaclass
Class super_class ; 指向其父类
const char *name ; 类名
long version ; 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
long info; 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size ; 该类的实例变量大小(包括从父类继承下来的实例变量);
struct objc_ivar_list *ivars; 用于存储每个成员变量的地址
struct objc_method_list **methodLists ; 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache *cache; 指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list *protocols; 存储该类遵守的协议
可以看到即使NSObject作为最基本的类,仍然会有isa指针,它指向的是metaNSObject,称之为元类,元类之后叫做rootMetaNSObject,根元类。
每个类的创建都会诞生元类,但是我们无法查看。
核心规则:类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass。
通俗说法:成员方法记录在class method-list中,类方法记录在meta-class中。即instance-object的信息在class-object中,而class-object的信息在meta-class中。
class 是 instance object 的类类型。当我们向实例对象发送消息(实例方法)时,我们在该实例对象的 class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的 methodlists 去查找。