Objective-C 對象模型及應用整理

本文參考自 http://blog.devtang.com/2013/10/15/objective-c-object-model/ 以及 http://ios.jobbole.com/81657/ 。 純粹是對文章內容的整理和整合,供自己以后查閱,版權歸原作者所有。

isa 指針

什么數據結構才能稱之為對象?

每個對象都有類。這是面向對象的基本概念,但是在Objective-C中,它對數據結構也一樣。含有一個指針且該指針可以正確指向類的數據結構,都可以被視作為對象。

在Objective-C中,對象的類是isa指針決定的。isa指針指向對象所屬的類。

類結構圖

實際上,Objective-C中對象最基本的定義是這樣的:


objc_object
Class

這說的是:任何帶有以指針開始并指向類結構的結構都可以被視作objc_object。
我們還可以看到,Class 也是一個包含 isa 指針的結構體。(圖中除了 isa 外還有其它成員變量,但那是為了兼容非 2.0 版的 Objective-C 的遺留邏輯,大家可以忽略它。)
Objective-C中對象最重要的特點是你可以發送消息給它們:

[@"stringValue"  writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];

這能工作是因為Objective-C對象(這兒是NSCFString)在發送消息時,運行時庫會追尋著對象的isa指針得到了對象所屬的類(這兒是NSCFString類)。這個類包含了能應用于這個類的所有實例方法和指向超類的指針以便可以找到父類的實例方法。運行時庫檢查這個類和其超類的方法列表,找到一個匹配這條消息的方法(在上面的代碼里,是NSString類的writeToFile:atomically:encoding:error方法)。運行時庫基于那個方法調用函數(IMP)。重點就是類要定義這個你發送給對象的消息。

什么是元類(meta class)?

你可以發送消息給一個類:

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];

在這個示例里,defaultStringEncoding被發送給了 NSString類。

因此Objective-C中每個類本身(Class)也是一個對象。如上面圖Class所展示的,這意味著類結構必須以一個isa指針開始,從而可以和objc_object在二進制層面兼容。為了調用類里的方法,類的isa指針必須指向包含這些類方法的類結構體。這個類結構體就是元類 (metaclass)。
簡單說就是:

  • 當你給對象發送消息時,消息是在尋找這個對象的類的方法列表。
  • 當你給類發消息時,消息是在尋找這個類的元類的方法列表。

元類是必不可少的,因為它存儲了類的類方法。每個類都必須有獨一無二的元類,因為每個類都有獨一無二的類方法。每個對象的isa所指的是一個元類的實例。那么這個實例所屬的類是如何定義的呢?這就引出了:

元類的類是什么?

元類,就像之前的類一樣,它也是一個對象。你也可以調用它的方法。自然的,這就意味著他必須也有一個類。

如類結構圖所示,所有的元類都使用根元類(繼承體系中處于頂端的類的元類)作為他們的類。這就意味著所有NSObject的子類(大多數類)的元類都會以NSObject的元類作為他們的類。

根據這個規則,所有的元類使用根元類作為他們的類,根元類的元類則就是它自己。也就是說基類的元類的isa指針指向他自己。

驗證

下面的代碼在運行時創建了一個NSError的子類,并且添加了一個方法:

Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

ReportFunction函數就是添加的實例方法,具體實現如下

void ReportFunction(id self, SEL _cmd)
{
    NSLog(@"This object is %p.", self);
    NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
 
    Class currentClass = [self class];
    for (int i = 1; i < 5; i++)
    {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = object_getClass(currentClass);
    }
 
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

表面上看來,這相當簡單。在運行時創建一個類只需要3個步驟:

  1. 為”class pair”分配內存 (使用objc_allocateClassPair).
  2. 添加方法或成員變量到有需要的類里 (我已經使用class_addMethod添加了一個方法).
  3. 注冊類以便它能使用 (使用objc_registerClassPair).

這里解釋一下 SEL和IMP

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

其中,Apple源碼里并沒有給出objc_selector的定義,這里用例子來說明:

@interface NSObject (Sark)
+ (void)foo;
@end

@implementation NSObject (Sark)

- (void)foo
{
    NSLog(@"IMP: -[NSObject(Sark) foo]");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SEL sel = @selector(foo);
        NSLog(@"%s", (char *)sel);
        NSLog(@"%p", sel);

        const char *selName = [@"foo" UTF8String];
        SEL sel2 = sel_registerName(selName);
        NSLog(@"%s", (char *)sel2);
        NSLog(@"%p", sel2);
    }
    return 0;
}

輸出結果:

2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114

因此可以發現,Objective-C在編譯時,會根據方法的名字生成一個用來區分這個方法的唯一的一個ID。只要方法名稱相同,那么它們的ID就是相同的。
兩個類之間,不管它們是父類與子類的關系,還是之間沒有這種關系,只要方法名相同,那么它的SEL就是一樣的。每一個方法都對應著一個SEL。編譯器會根據每個方法的方法名為那個方法生成唯一的SEL。這些SEL組成了一個Set集合,當我們在這個集合中查找某個方法時,只需要去找這個方法對應的SEL即可。而SEL本質是一個字符串,所以直接比較它們的地址即可。

那么什么是IMP呢?
看其定義, IMP本質就是一個函數指針,這個被指向的函數包含一個接收消息的對象id,調用方法的SEL,以及一些方法參數,并返回一個id。因此我們可以通過SEL獲得它所對應的IMP,在取得了函數指針之后,也就意味著我們取得了需要執行方法的代碼入口,這樣我們就可以像普通的C語言函數調用一樣使用這個函數指針。

那么什么是方法列表呢?
方法列表就是在圖objc_object里,objc_class結構中的成員 struct objc_method_list **methodLists.

重點參考自:http://chun.tips/blog/2014/11/06/bao-gen-wen-di-objective[nil]c-runtime(3)[nil]-xiao-xi-he-category/

運行ReportFunction,我們需要創建一個動態實例來創建類調用report方法:

id instanceOfNewClass = [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];

這里沒有聲明report方法,但我使用performSelector:調用它,所以編譯器不會給出警告。函數使用object_getClass跟蹤isa指針,因為isa指針是類的保護成員(你不能直接接收其他對象的isa指針)。ReportFunction不使用類方法,因為在類對象里調用類方法不能返回元類,它會再次返回這個類(因此[NSString class]會返回NSString類而不是NSString元類).
ReportFunction函數會沿著isa進行檢索,來告訴我們class,meta-class以及meta-class的class是什么樣的情況:

This object is 0x10010c810.
Class is RuntimeErrorSubclass, and super is NSError.
Following the isa pointer 1 times gives 0x10010c600
Following the isa pointer 2 times gives 0x10010c630
Following the isa pointer 3 times gives 0x7fff71038480
Following the isa pointer 4 times gives 0x7fff71038480
NSObject's class is 0x7fff710384a8
NSObject's meta class is 0x7fff71038480

觀察isa到達過的地址的值:

  • 對象的地址是 0x10010c810
  • 類的地址是 0x10010c600
  • 元類的地址是 0x10010c630
  • 根元類(NSObject的元類)的地址是 0x7fff71038480
  • NSObject元類的類是它本身.

這些地址的值并不重要,重要的是它們說明了文中討論的從類到meta-class到NSObject的meta-class的整個流程。

系統相關API及應用

ias swizzling的應用

系統提供的 KVO 的實現,就利用了動態地修改 isa 指針的值的技術。

Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the [class] method to determine the class of an object instance.

Method Swizzling API 說明

Objective-C 提供了以下 API 來動態替換類方法或實例方法的實現:
class_replaceMethod 替換類方法的定義
method_exchangeImplementations 交換 2 個方法的實現
method_setImplementation 設置 1 個方法的實現

這 3 個方法有一些細微的差別,給大家介紹如下:

  • class_replaceMethod
    在蘋果的文檔(如下圖所示)中能看到,它有兩種不同的行為。當類中沒有想替換的原方法時,該方法會調用class_addMethod
    來為該類增加一個新方法,也因為如此,class_replaceMethod在調用時需要傳入types參數,而method_exchangeImplementations和method_setImplementation卻不需要。
  • method_exchangeImplementations 的內部實現相當于調用了 2 次method_setImplementation方法,從蘋果的文檔中能清晰地了解到(如下圖所示)

從以上的區別我們可以總結出這 3 個 API 的使用場景:

  • class_replaceMethod, 當需要替換的方法可能有不存在的情況時,可以考慮使用該方法。
  • method_exchangeImplementations,當需要交換 2 個方法的實現時使用。(常用于使用自定義的類的方法來hack掉iOS SDK提供的方法)
  • method_setImplementation 最簡單的用法,當僅僅需要為一個方法設置其實現方式時使用。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容