Objective-C Runtime笔记(官方Doc翻译+原创)

先上官方地址:Objective-C Runtime Programming Guide


Objective-C是一门动态语言,它将静态语言在编译和链接时期做的事放在运行时处理Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译代码,这个运行时系统即Objc Runtime,运行时系统作为OC语言的操作系统。尤其是它在运行时动态的加载类,并且向其他对象转发消息。它同时提供在你的程序运行时如何找到对象的信息。

Objc Runtime是一个用C语言和汇编语言编写的库

Objc Runtime有两个版本:LegacyModern 两个版本

在Legacy版本中,如果你在类中改变了实例变量的布局,你必须重新编译继承它的类。

在Modern版本中,如果你在类中改变了实例变量的布局,你不需要重新编译继承它的类。

iPhone应用和在OS X v10.5上运行的64位程序以及以后的版本使用Modern版本。

其他程序(OS X 32位程序)使用Legacy版本。



与运行时系统的交互

OC程序在3个层次上与运行时系统进行交互:
    通过OC源代码
    通过Foundation框架下NSObject类中定义的方法
    通过直接调用运行时函数

OC源码

大部分时候,运行时系统在后台自动工作,你需要做的仅仅是编写并运行OC源码。
当你编译包含OC类和方法的代码时,编译器为了实现语言的动态特性创建数据结构和函数调用。数据结构捕获类、类别定义和协议声明中的信息;它们包括OC语言中定义类时涉及到的类和协议对象,协议,以及SEL,实例变量模板,和其他从源码中提取的信息。主要的运行时函数是发送消息,它由源代码的消息表达式调用。

NSObject方法

大多数Cocoa中的对象都是NSObject类的子类,所以大多数对象都继承它定义的方法。(一个值得注意的例外是NSProxy类。)它的方法因此建立了每个实例和每个类对象固有的行为。然而,在少数情况下,NSObject类仅仅为了表明一些事情应该这么做而定义一个模板,它本身不提供任何必需的源码。

例如,NSObject类定义了一个实例方法description,这个方法返回一个描述该类内容的字符串。这主要用于调试——GDB打印对象 命令打印从这个方法返回的字符串,NSObject类的这个方法的实现不知道包含有什么类,所以它返回包含对象名字和地址的字符串。NSObject的子类可以实现这个方法来返回更多详细信息。例如,Foundation框架下的NSArray类返回它包含的所有对象的描述。

一些NSObject的方法简单地查询运行时系统的信息。这些方法允许对象执行自省(自我认知),这些举例列出的方法都是类方法,其要求对象识别他们的类;isKindOfClass:isMemberOfClass:它测试对象在继承层次结构中的位置;repondsToSelector:它表示一个对象能接受一个特定的消息,conformsToProtocol它表示一个对象是否实现了定义在特定协议中的方法;methodForSelector:提供了方法的实现地址。像这些方法给一个对象认识自身的能力。

运行时函数

运行时系统是一个在位于/usr/include/objc的头文件中包含了一些列函数和数据结构的动态共享库,许多函数与允许你使用C语言硬编码去复写在你写OC代码时编译器做的事情。其他形式的基础功能导入时通过NSObject类的方法。这些函数使得开发其他面向运行时系统和增强开发环境的工具成为可能;它们不需要OC语言环境。然而,一部分运行时函数可能会不定期的被用来写OC程序。所有的这些函数都被记录在Objective-C Runtime Reference。(在API文档中查看)


消息机制


本节描述消息表达式是如何转为objc_msgSend的函数调用和如何通过方法名来引用一个方法。然后讲解如何利用objc_msgSend和如何绕过动态绑定。

objc_msgSend函数

在OC中,直到运行时之前,消息不会被绑定到方法的实现。编译器会转换一个消息表达式:
[receiver message]
转变为一个消息函数的调用——objc_msgSend。函数带有接受者和方法提及的名字,作为方法选择器的两个主要参数:
objc_msgSend(receiver, selector)
任何传入消息的参数都被交给objc_msgSend:
objc_msgSend(receiver, selector, arg1, arg2, ...);

消息函数做了所有动态绑定所必需的事情:
  • 它首先找到选择器所引用的方法实现。由于相同的方法可以被单独的类不同地实现,它找到的确切的方法实现取决于接受者所属的类。
  • 然后它通过接收对象以及该方法所指定的所有参数来调用方法实现。
  • 最后它通过调用方法实现的返回值来作为自己的返回值。
意:编译器生成的调用消息函数,你绝对不能在你的代码中直接调用。

消息的关键在于编译器为每个类和对象构建的结构,每个类结构包含两个必要的元素:
  • 一个指向父类的指针
  • 一个类的调度表。这个表包含关联了方法选择器与它们标记的类特定方法的地址。setOrigin::方法的选择器与setOrigin::的地址关联起来,等等。
当一个新的对象被创建的时候,内存会为它开辟一部分空间,它的实例变量同时被初始化。对象的所有变量中的第一个是一个指向它的类结构的指针,这个指针叫做isa指针,提供给对象访问它的类,通过类,到达所有该类继承的类。

注意:严格说虽然不是语言的一部分,isa指针对于工作在OC运行时系统中的对象来说是必须的。一个对象需要与结构体struct objc_object(在objc/objc.h中定义)中定义的任何字段“等同”,但是,你很少,如果有需要创建你自己的根对象,从NSObject或者NSProxy集成而来的类对象都自动拥有isa变量。

类和对象结构中的元素展示:消息机制框架


当一条消息发送给一个对象的时候,消息函数跟随对象的isa指针到它的类结构,在调度表中查找方法的选择器,如果他不能在那找到选择器,objc_msgSend跟随指针来到父类并且试着在调度表中找到选择器,连续的失败会使objc_msgSend沿着继承机构向上寻找直到它到达NSObject类,一旦它发现选择器 ,该函数调用表中输入的方法并将其传递给接收对象的数据结构。

这里着重说明一个知识点:
       在一个类中调用[super class] 很多初学者会认为输出父类的名字,但是结果却还是与[self class]相同的输出,而又不理解,其实self是类的隐藏参数,指向当前调用方法的类,另一个隐藏参数是_cmd前面已经介绍过,带表当前方法的selector,这里只关注这个self,而super并不是一个隐藏参数,它是一个“编译器指示符”,和self指向相同的消息接受者,[self class]和[super class] ,接收class消息的都是指向当前类的指针,而不是想当然的super指向的父类,不同之处在于super只是告诉编译器,调用方法是要去调用父类的class方法而不是本类的,其实self和super最后调用的都是NSObject定义的方法,输出本类的类名,所以才会出现上面那种结果。
    

这就是方法实现在运行时才会确定的实现方式,或者用面向对象编程的行话,那些方法与消息动态绑定。

为了加快消息的传递过程,运行时系统缓存它们用到的选择器和方法的地址,每一个类都有一个单独的缓存,它能包含继承方法,就像它们自己定义的一样。在搜索调度表之前,发送消息例行在它的接收对象的类的缓存中查找(理论上,一个方法被使用一次可能会被再次使用),如果方法选择器在缓存中,消息传递仅仅比函数调用略微慢一点点。一旦一个程序已经运行的足够长的时间来“预热”它的缓存,几乎所有的消息都会找到缓存方法,在程序运行过程中,缓存动态增加来容纳新消息。

使用隐藏参数

当objc_msgSend找到了方法实现的程序段,它调用这段程序并传递消息中的所有参数,它也会传递给这段程序两个隐藏的参数:
  • 接收对象
  • 方法的选择器
这些参数给它的方法实现提供显示的关于两半调用它的消息表达式的信息。他们被称为是隐藏的是因为它们并不会被声明在它们定义的源码中,当源码被编译的时候,这两个参数会被插入到实现中。

尽管这些参数并不是显示声明的,源码仍然能够引用到它们(就像它能够引用到接受对象的实例变量一样)。一个引用接收对象为——self,引用他自己的方法选择器为——_cmd。在下面的例子中,_cmd引用strange的方法选择器,引用self作为接收strange消息的对象。
- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}
self在两个参数中更有用一些,事实上,这是接收的对象的实例变量对于方法定义变得可用的方式。

得到一个方法地址

绕过动态绑定的唯一方式就是得到方法的地址并像调用函数一样调用它。这在极少数场合是合适的,当一个特定的方法被连续调用多次的时候而且你想避免方法每次被执行发送消息的开销。

一个定义在NSObject类中的方法,methodForSelector:你可以要求一个指向实现一个方法的过程的指针,然后用指针调用这个过程,methodForSelector:返回的指针必须仔细转换到恰当的函数类型。返回和参数类型都应该包含在转换中。

下面的例子展示了setFilled:方法的实现过程如何被调用:
void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);
第一次被传过去的两个参数是接收对象self和方法选择器_cmd。
这些参数在方法语法中被隐藏,但是当方法被当做函数调用的时候必须显示的传递。

使用methodForSelector:来规避动态绑定节约了大量发送消息所需的时间,要使节省变得有意义,必须当一个特定的消息重复很多次的时候,就像上面for循环展示的那样。

注意方法methodForSelector:是Cocoa 运行时系统提供的,它并不是OC语言自己的特性。


动态方法解析

这节描述了如何动态地提供一个方法。

动态方法解析

有时你会想要动态地提供一个方法的实现,例如,OC声明属性特性包含@dynamic关键字

@dynamic propertyName;

告诉编译器与属性关联的方法会被动态地提供。


你可以实现方法resolveInstanceMethod:和方法resolveClassMethod:来分别地动态提供一个给定selector,实例和类名的方法实现。

一个OC方法的根本就是带有self和_cmd两个参数的C函数,你可以用函数class_addMethod来添加一个函数到类中去作为方法。因此给出以下函数:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

你可以使用方法resolveInstanceMethod:动态添加它的实现到类中作为方法(resolveThisMethodDynamically):

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

官方的runtime编程指南到这里就结束了,你可以喷我,但我还是想说,新手根本看不懂微笑所以还要再增加篇幅从头讲

每个方法都有一个SEL(selector)和一个IML(implement),SEL可以随便写,但是不一定有对应的IML,如果消息函数沿着继承层次结构找到了顶端还是找不到对应的方法实现,就会抛出异常而crash。

上文提到“消息函数做了所有动态绑定所必需的事情:它首先找到选择器所引用的方法实现。”但是如果一直没有找到,就会开始尝试动态解析,消息转发,标准消息转发:



其实这就是通过SEL查找IML,这个过程也可以用下图表示:

SEL查找IML过程


resolveInstanceMethod函数

函数原型是:

+ (BOOL)resolveInstanceMethod:(SEL)name;
在运行时(runtime),SEL没有找到对应的IML就会先执行这个函数, 这个函数是给类利用class_addMethod添加方法的机会

如果实现了添加方法的代码则返回YES,如果没有实现则返回NO。

新建一个工程在.m文件添加如下代码:

#import "ViewController.h"

@interface ViewController()

@end

@implement ViewController

- (void)viewDidLoad{
    [super viewDidLoad];
    [self performSelector:@selector(doSomething:)];
}

- (void)didReceiveMemoryWarning{
    [super didReveiceMemoryWarning];
}
结果就是程序crash控制台报错:

terminating with uncaught exceptionof type NSException
因为程序没有找到doSomething:这个方法,下面我们实现

+ (BOOL)resolveInstanceMethod:(SEL)sel;
并且判断若果sel是doSomething:那就说出add method here

#import "ViewController.h"

@interface ViewController()

@end

@implement ViewController

- (void)viewDidLoad{
    [super viewDidLoad];
    [self performSelector:@selector(doSomething)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if(sel == @selector(doSomething)){
        NSLog(@"add method here!");
        return YES;
    }
    return NO;
}

- (void)didReceiveMemoryWarning{
    [super didReveiceMemoryWarning];
}
运行查看控制台发现程序虽然崩溃了,但是控制台输出的第一句话就是add method here! 说明确实进入了这个方法并且通过了判断。

所以我们可以在if语句里做一下操作,使得这个方法的得到实现而不至于走到方法:

- (void)doesNotRecognizeSelector:(SEL)aSelector;
走到这个方法就会Crash,接下来我们继续更改

#import "ViewController.h"

@interface ViewController()

@end

@implement ViewController

- (void)viewDidLoad{
    [super viewDidLoad];
    [self performSelector:@selector(doSomething:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if(sel == @selector(doSomething)){
        NSLog(@"add method here!");
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicMethodIMP(id self, SEL _cmd){
    NSLog(@"doSomthing SEL");
}

- (void)didReceiveMemoryWarning{
    [super didReveiceMemoryWarning];
}
定义了一个void dynamicMethodIMP(id self, SEL _cmd)这个函数,并且在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中执行了class_addMethod方法,运行工程我们查看LOG:

add method here!
doSomething SEL
程序成功输入,这说明我们已经通过runtime成功向我们这个类中添加了一个方法,这里说几点 注意事项:

首先class_addMethod是定义在<objc/runtime.h>中的方法,使用前要导入头文件,前几个查找IML的方法是定义在NSObject中的方法,所以无需导入头文件。

我们再来看一下class_addMethod的方法定义

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

  • cls 方法所要添加到的类
  • name 方法名字可以随意起
  • imp 实现方法的函数
  • types 定义该函数返回值类型和参数类型(依次按序输入)的字符串,注意这个参数不是NSString类型,而是const char*类型 所以不要用@“”,而要直接用"",我们上面的函数是  void dynamicMethod(id self, SEL _cmd) 返回值是void——(对应)v ; 第一个参数是self——(对应)@  第二个参数是SEL——(对应):,所以连起来就是“v@:”就是此处该写入的参数。

再举个例子:

int newMethod(id self, SEL _cmd, NSString *str){
    return 100;
}
那么添加这个函数的方法就是

class_addMethod([self class], SEL name, IMP imp, "i@:@");

forwardingTargetForSelector函数

如果在+ (BOOL)resolveInstanceMethod:(SEL)sel中没有找到或者添加方法,消息继续往下传递到

-(id)forwardingTargetForSelector:(SEL)aSelector
看看是不是有对象可以执行这个方法,我们再原有例子的基础上在新建一个类

#import"SecondViewController.h"

@interface SecondViewController

@end

@implementation SecondViewController

- (void)viewDidLoad{
    [super viewDidLoad];
}

- (void)secondVCMethod{
    NSLog(@"This is secondVC method");
}

- (void)didReceiveMemoryWarning{
    [super didReceiveMemoryWarning];
}
添加好后我们要在ViewController 中调用secondVCMethod,可是这个两个类并没有继承关系,正常是无法调用的

在ViewController中

#import "ViewController.h"

@interface ViewController()

@end

@implement ViewController

- (void)viewDidLoad{
    [super viewDidLoad];
    [self performSelector:@selector(secondVCMethod)];
}

- (void)didReceiveMemoryWarning{
    [super didReveiceMemoryWarning];
}
这样调用肯定会找不到方法而崩溃,下面我们是用forwardingTargetForSelector方法来转发一下消息,继续处理ViewConreoller类

#import "ViewController.h"

@interface ViewController()

@end

@implement ViewController

- (void)viewDidLoad{
    [super viewDidLoad];
    [self performSelector:@selector(secondVCMethod)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    return [super resolveInstanceMethod:sel];
}

- (id)forwadingTargetForSelector:(SEL)aSelector{
    Class class = NSClassFromString(@"SecondViewController");
    UIViewController * vc = class.new;
    if(aSelector == NSSelectorFromString(@"secondVCMethod")){
        NSLog(@"secondVC do this");
        return vc;
    }
    return nil;
}

- (void)didReceiveMemoryWarning{
    [super didReveiceMemoryWarning];
}
我们会发现secondVCMethod方法执行了,程序并没有崩溃,原因在于当没有找到secondVCMethod这个方法的时候消息一直传递到方法
- (id)forwadingTargetForSelector:(SEL)aSelector
然后在里面创建了一个SecondViewController的对象,并判断如果这个需要转发的方法是secondViewController中的方法就返回secondViewController的对象,消息成功转发给secondViewController的对象,并执行。同时也相当于完成了一个多继承。

动态加载

一个OC程序可以在运行的时候绑定并连接新的类和分类。新的代码会被合并到程序中,与一开始就加载的代码没有区别。

动态加载可以被用来做血多不同的事情,例如,在系统APP的许多模块都是动态绑定。

在Cocoa环境下,动态绑定通常被用来自定义APP。其他则是用来写一些运行时加载的组件——就像Interface Builder加载定制的调色板和OS X系统应用加载自定义模块一样,可加载模块扩展了你的应用可以做什么,它们的贡献在于你提供框架,他人提供代码。

虽然运行时函数在Mach-O文件中执行动态绑定(objc_loadModiles,在objc_load.h中定义),Cocoa的NSbundle类为动态帮顶提供了一个显着更方便的接口——一种面向对象并与相关服务集成的接口。在Foundation框架查看NSBundle类的说明参考类的信息和它所使用的。通过 《OS X ABI Mach-O 文件格式参考》查看Mach-O文件的信息。

消息转发

向一个对象发送消息,对象没有处理消息,就会报错。然而,在报错之前,运行时系统给接收消息的对象两个选择去处理消息。


转发

如果你给一个对象发送消息,并且这个对象没有处理这个消息,在抛出一个错误之前,运行时系统会向对象发送一个消息:

forwardInvocation:
NSInvocation作为它的唯一实参,通过 NSInvocation 对象封装了原始的消息和实参,



类对象与基础数据结构

Class

Command + Shift + O 选择打开文件objc.h
Objective-C的类由Class类型表示,它实际上是指向objc_class的结构体的指针
typedef struct objc_class *Class;
id是一个objc_object类型的指针,objc_object是表示一个类的实例的结构体
typedef struct objc_object *id;
struct objc_object{
	Class isa OBJC_ISA_AVAILABILITY;
};

objc/runtime.h中objc_class的结构体定义如下:(Command + Shift + O可以选择打开文件)
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
 
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    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

struct objc_object{
	Class isa OBJC_ISA_AVAILABILITY;
};
 
 
是一个Class类型的指针,每个Instance都有一个isa指针,指向对象的类,也叫普通的Class,这个Class中存储普通的成员变量和对象方法,而Class类里也有一个isa指针(所有的类自身也是一个对象),指向元类meteClass,也就是静态Class,静态Class中存储static类型的成员变量和类方法。
当我们向Objective-C对象发送消息时,运行时库会根据instance的isa指针找到这个instance所属的类,runtime库会在类的方法列表查找,没有的话顺着isa指针找到父类继续查找与消息对应的selector指向的方法,找到后即运行这个方法。

super_class 

指向该类的父类,如果该类是最顶层的根类如:NSObject或者NSProxy,则super_class为NULL

cache

用于缓存最近使用的方法
在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上,这总情况下如果每次来消息都在methodList中遍历一遍,性能会很差。
当每次调用一个方法后,将这个方法缓存在cache中,下次调用的时候runtime就会优先去cache中查找,如果没有再去methodList中查找。
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

version

这个字段提供类的版本信息,对于对象的序列化有用,可以识别出不同类定义版本中实例变量的布局。
Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值