Understanding Objective-C

Understanding Objective-C

本文翻译原作者地址这里

动态语言VS静态语言

Objective-C是一个基于运行时的语言,也就是说通过运行时,Objective-C会延迟到编译的时候再决定具体执行的是什么。通过运行时,当你要重定向一个消息到一个合适的对象,或者有目的的交换两个方法的实现提供了很大的灵活性。如果我们将C语言与其进行对比,C语言从一个main()方法开始这行,然后便是根据你的逻辑自上而下设计和执行。一个C语言的结构体不能转发请求到另一个目标上执行函数。
通常一个下面这样的C程序

#include < stdio.h >
 
int main(int argc, const char **argv[])
{
        printf("Hello World!");
        return 0;
}

编译器解析后是这样的:

.text
 .align 4,0x90
 .globl _main
_main:
Leh_func_begin1:
 pushq %rbp
Llabel1:
 movq %rsp, %rbp
Llabel2:
 subq $16, %rsp
Llabel3:
 movq %rsi, %rax
 movl %edi, %ecx
 movl %ecx, -8(%rbp)
 movq %rax, -16(%rbp)
 xorb %al, %al
 leaq LC(%rip), %rcx
 movq %rcx, %rdi
 call _printf
 movl $0, -4(%rbp)
 movl -4(%rbp), %eax
 addq $16, %rsp
 popq %rbp
 ret
Leh_func_end1:
 .cstring
LC:
 .asciz "Hello World!"

最后通过一个库连接到一起然后生成一个可执行的程序。与Objective-C不同的是尽管处理的是类似的代码,但是编译器编译的时候是基于Objective-C 运行时的库来处理的。我们在Objective-C入门的时候都知道,
[self doSomethingWithVar:var1]
是转化为
objc_msgSend(self, @selector(doSomethingWithVar:), var1);
但是除此之外,我们不知道以后进一步发生了些什么。

什么是Objective-C的Runtime?

Objective-C Runtime是一个Runtime库,大部分是由C语言编写的,给C语言添加了面向对象的特性创造了Objective-C。这些特性包括加载Class信息,方法分发,方法转发等。Objective-C的runtime创建了所有的能够使Objective-C能够面向对象编程的结构。

Objective-C 术语

为了方便我们更好的理解,先了解一些属于。Runtime到目前是有两种:最新的Runtime 和历史Rumtime。所有的64位Mac OS APPs以及所有的iOS APPs都是新的Runtime.其余的都是历史Runtime。另外介绍两种基本的方法类型。实例方法(以-开头,如- (void)doFoo;)通过类的实例操作。另外一个是类方法(+开头,如+ (id)alloc;)
Objective-C方法与C语言函数相似

- (NSString *) movieTitle{
    return @"Hello World!";
}

选择器在Objective-C中本质上是一个C的数据结构,来确定一个你所指定对象执行的方法。在Runtime中定义是下面这个样子:
typedef struct objc_selector *SEL
用法是
SEL aSel = @selector(movieTitle)
消息
[target getMovieTitleForObject:obj]
一个Objective-C的消息是在'[]’内的,其中包含你消息要发送的对象,你要执行的方法,以及执行方法所需要的参数。Objective-C消息发送与C语言函数调用不一样的,区别在于在Objective-C中你给一个对象发送消息并不代表这个对象就会执行这个消息。对象会检查谁是消息的发送者并且在这个基础之上执行一个不同的方法或转发消息给一个另外一个target。
Runtime中的class定义如下:

typedef struct objc_class *Class;
typedef struct objec_object {
    Class isa;
} *id;

在Runtime中,我们定义了Objective-C Class的结构体和对象的结构体。在object_object中定义了一个Class类型的指针isa,这就是我们通常所说的isa 指针。在Objective-C Runtime中需要这个isa指针来检查一个对象判断它的class是什么,并且当你给对象发送消息时候观察它是否能够对选择器做出反应。最后我们看到id指针。id指针默认除了告诉我们这是一个Objective-C对象外没有告诉任何别的消息。通过对象的id指针找到这个对象的class,查看它是否对某个方法做出响应等,当你指导指针指向的对象是什么的时候你可以有更多的行为。
IMP(Method Implementations)
typedef id (*IMP)(id self, SEL _cmd, ...);
IMP是编译器生成的指向方法实现的函数指针。如果你是Objective-C刚入门,你不需要对这些进行深入了解,但是这对于你理解Objective-C Runtime是怎样调用的方法是有好处的。
Objective-C的基本实现是下面这个样子:

@interface MyClass :NSObject {
    //vars
    NSInteger counter;
}
//methods
- (void)doFoo;
end

但是在Runtime中则是需要有更多的信息来表示

#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 

从上面可以看到一个class用有其super class信息,name, 实例变量,缓存,协议等。Runtime 通过这信息来响应你给类或者类的实例发送消息的处理。

Class来定义对象,那么Class自身也是对象吗?
是的,Objective-C classes其自身也是对象。Runtime对于这类对象的处理是通过创建Meta Class(元类)。当你发送[NSObject alloc]你实际是给类对象发送一个消息,并且这个类对象必须是元类的实例。所有元类的父类都是根元类。所有的元类里面都包含其能够响应的类方法的方法列表。所以当你给类对象发送消息如[NSObject alloc],那么objc_msgSend()在元类中的方法进行搜索来查找能够响应这个操作方法。
为什么继承Apple Classes
在开始Cocoa项目的时候,教程都说通过继承NSObject类来编码,并且你也从中体会到一些益处。当我们实例化一个类的对象的时候
MyObject *object = [[MyObject alloc] init];
首先执行的消息是+alloc如果你如果你查看文档,文档会这么说:新实例的isa实例变量初始化为一个新的描述类的数据结构。所以通过继承苹果的类,我们能够很简单的分配和创建runtime所期望的结构。
与Class cache相关(objc_cache *cache)
当Objective-C Runtime通过对象isa指针查询对象的时候,能够找到查看其实现的很多方法。然而我们 只是用到这些方法中的一小部分。所以类实现了一个cache,当你通过类的分发查找到你要用的方法的时候将其加入cache。所以当objc_msgSend()从一个类中查询一个selector的时候,首先就是查询cache中是否存在。使用cache的理论依据是当你在class中发送消息,你很可能在之后再次发送消息,所以把这个方法加入缓存是有意义的。

MyObject *obj = [[MyObject alloc] init];
 
@implementation MyObject
-(id)init {
    if(self = [super init]){
        [self setVarA:@”blah”];
    }
    return self;
}
@end

上面这段程序执行后,产生了下面几个步骤:1.[MyObject alloc]最先执行,MyObject类没有实现alloc方法,所以通过superclass指针指向的NSObject中查找该方法。2.查看NSObject是否能够响应+alloc方法,发现其能够响应。+alloc方法价差receiver MyObject然后根据class大小来分配一块内存并初始化一个isa指针指向MyObject的类。这样我们就you8leyige实例,并最后将+alloc方法加到NSObject类的cache中。3.在这之前我们是发送了一个类消息,现在我们讲发送一个实例消息-init。这里我们的class 有- (id)init,因此能够响应这一方法。所以将- (id)init方法加入cache中。4.接下来self = [super init]执行,通过调用super,于是我们就进入NSObject中调用init方法。super调用保证父类中正确初始化其变量。接下来在subclass中初始化变量。在上面这个例子中子类的init 并没有发生什么重要变化,但是在有些情况下变化还是挺大的。

#import < Foundation/Foundation.h>
 
@interface MyObject : NSObject
{
 NSString *aString;
}
 
@property(retain) NSString *aString;
 
@end
 
@implementation MyObject
 
-(id)init
{
 if (self = [super init]) {
  [self setAString:nil];
 }
 return self;
}
 
@synthesize aString;
 
@end
 
 
 
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
 id obj1 = [NSMutableArray alloc];
 id obj2 = [[NSMutableArray alloc] init];
  
 id obj3 = [NSArray alloc];
 id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
  
 NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
 NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
  
 NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
 NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
  
 id obj5 = [MyObject alloc];
 id obj6 = [[MyObject alloc] init];
  
 NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));
 NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));
  
 [pool drain];
    return 0;
}

通常都会认为输出的结果为:

NSMutableArray
NSMutableArray 
NSArray
NSArray
MyObject
MyObject

但是实际上输出的结果为:

obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject

出现上面这种情况原因就是+alloc方法返回一个class对象,而-init方法返回另一个class的对象。
那么究竟objc_msgSend发生了什么呢?
例如有代码如下
[self printMessageWithString:@"Hello World!"];
编译器编译如下:
objc_msgSend(self, @selector(printMessageWithString:), @"Hello World!");
接下来我们通过目标对象的isa指针来查找对象(或者它的父类)是否能够响应selector@selector(printMessageWithString:).假设我们在类分发表或者cache中找到了这个选择器。接下来objc_msgSend()没有返回值,它开始执行并跟随一个指向你的方法的指针。接下来你的方法有了返回值,这看起来好像是objc_msgSend()有了返回值。有关objc_msgSend()方法,Bill Bumgarner在其博客的part1,part2中讲的很清楚。总结起来说就是下面几点:
1.检查被忽略的选择器-很显然,如果我们在垃圾回收中运行程序,我们忽略-ratian, -release等等。2.检查空对象。与其他语言不同,nil在Objective-C中作为参数传递是合法的。3.查找类中IMP的实现(方法实现)。我们首先查找的是与其类,如果找到就根据指针跳转到相应的函数。4.如果函数实现在cache中没有查找到,就跳转到class的分发表中查找,若仍然都没有查找到,跳转步骤5。5.在这一步中首先我们跳转至转发机制,在转发机制中就意味着你的代码被编译器编译成C函数。所以如果你的函数这么写:
- (int)doComputeWithNum:(int)aNum
转化为C函数则是:
int aClass_doComputerWithNum(aClass *self, SEL _cmd, int aNum)
Objective-C Runtime通过唤醒函数指针来调用你的方法。现在可以说你不能直接调用这个转换后的方法。然而Cocoa Framework 提供了下面的方法。

//declare C function pointer
int (computer *)(id, SEL, int);
//methodForSelector is COCOA & not ObjC Runtime
//gets the same function pointer objc_msgSend gets
computeNum = (int (*)(执行一个特定的方法。这种方法同Objective-C Runtime中`objc_msgSend(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
 
//execute the C function pointer returned by the runtime
computeNum(obj,@selector(doComputeWithNum:),aNum); 

通过这种方式你可以直接访问函数并且在运行时直接唤醒它。甚至可以使用这种方法绕过runtime的动态特性直接)方法如出一辙。 **Objective-C的消息转发** 在Objective-C中给对象发送一个该对象不知该如何做出反应的消息是合法的。在苹果的官方文档中给出的这样使用的原因之一是为了模拟多重继承(而Objective-C自身是不支持多重继承的),或者你希望抽象出来一个方法但是要在类或者对象中对实现细节进行隐藏。Runtime正好能够满足这一条件。Runtime是这样处理的:1.Rumtime在类方法cache和类以及父类的方法分发表中查找方法,但是没有查找到特定的要执行的方法。2.于是Objecltive-C Runtime在你的类中提供一个+ (BOOL)resolveInstanceMehtod:(SEL)aSEL`方法,这就给了你弥补出错的机会,告诉Runtime你通过调用这个方法就可以找到要搜索的方法了。举例如下:

void fooMethdo (id obj, SEL _cmd) {
    NSLog("Doing Foo");
}

你应该在通过class_addMethod()方法来解决问题

+ (BOOL)resolveInstanceMehtod:(SEL)aSEL 
{
    if (aSEL == @selector(doFoo:)) {
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolverInstanceMethod:];
}

上面的class_addMethod()方法中"v@:"是方法的返回值,同时也作为参数,Runtime Guide中编码类型那部分可以查看具体可以传入哪些在里面。如果没有实现+ (BOOL)resolveInstanceMethod:方法的话,Runtime接下来又提供(id)forwardingTargetForSelector:(SEL)aSelector.这一方法指向Runtime 的另一个能够响应这个消息的对象。如果这个方法仍然没有做出响应的话,Objective-C Runtime就会执行更昂贵的过程来唤醒-(void)forwardInvocation:(NSInvocation *)invocation
看代码:

- (id)forwardingTargetForSelector:(SEL)aSelector 
{
    if (aSelector == @selector(mysteriousMehtod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

从上面的方法返回我们应该做到显示不应该返回self,否则就会陷入无限循环了。4.如果上面的方法没有满足跳转,那么就剩下最后一次弥补的机会,就是调用- (void)forwardInvocation:(NSSInvocation *)anInvocation,如果从没见过NSInvocation的话,就记住它本质上是对象形式的Objective-C消息。一旦你拿到了NSInvocation,你就可以改变与这个消息相关的任何事物,比如:对象,选择器,参数等等(target, selector, arguments…)。你可以做的就类似下面这样的:

- (void)forwardInvocation:(NSinvocation *)invocation 
{
    SEL invSEL = invocation.selector;
    if([altObject resopnseToSelector:invSEL]){
        [invocation invokeWithTarget:alterObject];
    } else {
        [self doesNotRecognizeSelector:invSEL];
    }
}

Non Fragile ivars(Modern Rumtime)
Modern Runtime其中的一个就是非脆弱的实例变量(Non Fragile ivars)。当编译器编译类的时候,编译器生成了一个实例变量布局,在这个实例变量不居中描述了访问你的实例变量的位置,这就是获取指向你对象指针的底层细节。在这里实例变量是通过所占字节的相对偏移位置来描述的。但是在Mac OS 10.x以后出现了新的问题,比如 在父类NSObject中定义了实例变量 NSArray secretAry, NSImage secretImg,这时一个子类MyObject继承自父类,又添加了自身的新的实例变量NSArray students ,NSArray teachers.在实例变量布局中发现students和teachers是被覆盖了,因为指针偏转位置与父类重叠了。

NSObjectNSMyObject:NSObject
0Class isa0Class isa
4NSArray secretAry4NSArray students
8NSImage secretImg8NSArray teachters

于是出现了非脆弱实例变量,可以通过下面的表格看出:

NSMyObject:NSObject
0Class isa
4NSArray secretAry
8NSImage secretImg
12NSArray students
16NSArray teachers

在非脆弱实例变量中编译器将之前父类中的实例变量在新建一份,这样runtime在查询一个重载自父类的类的时候会动态调整偏移位置。这样就不会导致子类中新增的变量被删除。
Objective-C相关的对象(Objective-C Associated Object)
从Mac OS10.6开始,Objective-C的Runtime支持动态的给对象添加变量。如果我们想要给已经存在的类添加变量。以NSView举例,可以这样做:

#import < Cocoa/Cocoa.h> //Cocoa
#include < objc/runtime.h> //objc runtime api’s
 
@interface NSView (CustomAdditions)
@property(retain) NSImage *customImage;
@end
 
@implementation NSView (CustomAdditions)
 
static char img_key; //has a unique address (identifier)
 
-(NSImage *)customImage
{
    return objc_getAssociatedObject(self,&img_key);
}
 
-(void)setCustomImage:(NSImage *)image
{
    objc_setAssociatedObject(self,&img_key,image,
                             OBJC_ASSOCIATION_RETAIN);
}
 
@end

在runtime.h中可以看到给方法objc_setAssociatedObject()方法传值是怎么表示的。

/* Associated Object support. */
 
/* objc_setAssociatedObject() options */
enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403
};

混合虚表分发
你仔细看最新的runtime代码,你会看到如下介绍:

/***********************************************************************
* vtable dispatch
* 
* Every class gets a vtable pointer. The vtable is an array of IMPs.
* 每个类都有一个虚指针表。这个虚表保存了一个方法实现的数组。
* The selectors represented in the vtable are the same for all classes
*   (i.e. no class has a bigger or smaller vtable).
*   虚方法表中的选择器对所有类都是一样的。
* Each vtable index has an associated trampoline which dispatches to 
*   the IMP at that index for the receiver class's vtable (after 
*   checking for NULL). Dispatch fixup uses these trampolines instead 
*   of objc_msgSend.
*   每个虚方法表的索引都与其要分发到的类的虚方法表所指向的方法之间建立索引。方法的分发通过索引来实现,从而替代了objc_msgSend
* Fragility: The vtable size and list of selectors is chosen at launch 
*   time. No compiler-generated code depends on any particular vtable 
*   configuration, or even the use of vtable dispatch at all.
*   脆弱性:虚方法表的大小以及选择器表单都是在启动的时候确定的。由编译器产生的代码不会依赖虚方法表配置,甚至不会用到虚方法表的分发。
* Memory size: If a class's vtable is identical to its superclass's 
*   (i.e. the class overrides none of the vtable selectors), then 
*   the class points directly to its superclass's vtable. This means 
*   selectors to be included in the vtable should be chosen so they are 
*   (1) frequently called, but (2) not too frequently overridden. In 
*   particular, -dealloc is a bad choice.
*   内存大小:如果一个类的虚方法表与其父类的虚方法表是一样的(这个类没有覆盖父类的选择器)那么这个类直接指向其父类的虚方法表。这就意味着包含在虚方法表内的这些选择器能够被选择,这些选择器通常是1.经常被调用的2.并不经常被覆盖的。特别要指出的是-dealloc是个不明智的选择。
* Forwarding: If a class doesn't implement some vtable selector, that 
*   selector's IMP is set to objc_msgSend in that class's vtable.
* +initialize: Each class keeps the default vtable (which always 
*   redirects to objc_msgSend) until its +initialize is completed.
*   Otherwise, the first message to a class could be a vtable dispatch, 
*   and the vtable trampoline doesn't include +initialize checking.
*   转发:如果一个类没有实现虚方法表中的选择器那么选择器的方法实现就是类虚方法表中的objc_msgSend方法。
*   初始化:每个类都持有一个默认的虚方法表(总是指向ojbc_msgSend),直至它初始化完成。反之,发送给类的第一个消息会是一个虚方法表分发,并且这个分发并没有包含初始化检查。
* Changes: Categories, addMethod, and setImplementation all force vtable 
*   reconstruction for the class and all of its subclasses, if the 
*   vtable selectors are affected.
*   变化:categories,增加方法,设置函数实现都可以强制虚方法表重新创建(这将会影响到类自身和他的父类)。
**********************************************************************/
The idea behind this is that the runtime is trying to store in this vtable the most called selectors so this in turn speeds up your app because it uses fewer instructions than objc_msgSend. This vtable is the 16 most called selectors which make up an overwheling majority of all the selectors called globally, in fact further down in the code you can see the default selectors for Garbage Collected & non Garbage Collected apps... 
这一思想的背后是为了将那些在虚方法表中调用次数最多的选择器进行保存,这样加快了你的app响应速度,因为相比objc_msgSend方法来说这样用到更少的指令。下面这个虚方法表列举了16个调用次数最多的选择器组成了一个全局选择器。再往下从代码中你可以看到垃圾收集的默认选择器。
static const char * const defaultVtable[] = {
    "allocWithZone:", 
    "alloc", 
    "class", 
    "self", 
    "isKindOfClass:", 
    "respondsToSelector:", 
    "isFlipped", 
    "length", 
    "objectForKey:", 
    "count", 
    "objectAtIndex:", 
    "isEqualToString:", 
    "isEqual:", 
    "retain", 
    "release", 
    "autorelease", 
};
static const char * const defaultVtableGC[] = {
    "allocWithZone:", 
    "alloc", 
    "class", 
    "self", 
    "isKindOfClass:", 
    "respondsToSelector:", 
    "isFlipped", 
    "length", 
    "objectForKey:", 
    "count", 
    "objectAtIndex:", 
    "isEqualToString:", 
    "isEqual:", 
    "hash", 
    "addObject:", 
    "countByEnumeratingWithState:objects:count:", 
};

现在你知道了这些方法你想做什么呢?在你进行栈调试的时候,你会看到上面的一些方法调用。所有的这些方法作为debug来说,你就将他们理解成objc_msgSend()就好了。
对于objc_msgSend_fixup方法来说,当你调用一个应该出现在虚方法表中的方法但是不是常用的16个方法(objc_msgSend[0-15])中的一个的时候,会触发。比如有时候你看到objc_msgSend5这就意味着你调用了虚方法表中常用方法的某一个。Runtime会随它的意愿指定或者解除。所以你不要通过数字objc_msgSend10来就认为是-lenght.
总结
总之如果要详细的了解Objective-C Runtime,请参考官方文档Objective-C Runtime Programming GuideObjective-C Runtime Reference

    原文作者:Shawn_Wang
    原文地址: https://www.jianshu.com/p/08d194ee3719
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞