關於IOS物件的小事的探究

chouheiwa發表於2019-03-02

前言

在上一篇文章 一道有意思的iOS面試題 中寫到,Objective-C物件也是一種特殊的結構體。那一部分寫的可能不是很清楚,也不是很易於理解。但是在原文中改動,並增加相關內容又覺得篇幅過於長。所以新開一篇文章來寫,專門寫Object-C物件相關的事。

正文

我們知道,Objective-C是一門動態語言。Objective-C物件的所有方法操作都是通過objc_msgSend這個函式傳遞的。

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
複製程式碼

這個函式是Objective-C的靈魂(我個人認為的)。

接下來我們需要清楚,究竟什麼是iOS物件,在上一篇文章裡,我是這樣講述物件的

所有NSObject物件的首地址都是指向這個物件的所屬類。這個條件是充要條件。反過來說,如果一個地址指向某個類,我們就可以把這個地址當成物件去用。所以編譯是會通過的,也不會報unrecognized selector的錯誤。

其實這個總結的並不嚴謹,但是也不算是錯誤。這篇文章會對這個解釋進行更為嚴謹的解釋並且會有更深入的程式碼示範。

接下來我們就需要從頭開始解釋了,首先objc_object在iOS中的定義:

//物件
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
複製程式碼

從這個定義中可以看出來,事實上所有的物件都是***結構體***。接下來需要知道Class的定義,這個與objc_object的定義位於同一標頭檔案 objc.h

typedef struct objc_class *Class;
複製程式碼

也就是說,Class 事實上也是一個指標,指標指向的位置是objc_class這個結構體,到這裡我們就不繼續向下看過去了,因為這步已經到了看到我們這次將要講的終點了。這個結構體是某個Objective-C的物件的類資訊,它就相當於是我們定義在.h.m中間的 @interface 類的包含資訊物件(以後篇幅會詳細講解這個結構體,這裡就是大致說一說,因為這個結構體不是本篇文章的重點)

我們接下來可以簡短地講c語言中的結構體了。

這段還是直接放百度百科的定義吧(他的解釋會比我的解釋準確的多)

結構體作用

結構體和其他型別基礎資料型別一樣,例如int型別,char型別 只不過結構體可以做成你想要的資料型別。以方便日後的使用。

在實際專案中,結構體是大量存在的。研發人員常使用結構體來封裝一些屬性來組成新的型別。由於C語言內部程式比較簡單,研發人員通常使用結構體創造新的“屬性”,其目的是簡化運算。

結構體在函式中的作用不是簡便,其最主要的作用就是封裝。封裝的好處就是可以再次利用。讓使用者不必關心這個是什麼,只要根據定義使用就可以了。

結構體的大小與記憶體對齊 結構體的大小不是結構體元素單純相加就行的,因為我們主流的計算機使用的都是32bit字長的CPU,對這型別的CPU取4個位元組的數要比取一個位元組要高效,也更方便。所以在結構體中每個成員的首地址都是4的整數倍的話,取資料元素時就會相對更高效,這就是記憶體對齊的由來。每個特定平臺上的編譯器都有自己的預設“對齊係數”(也叫對齊模數)。程式設計師可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊係數”。

規則:

1、資料成員對齊規則:結構(struct)(或聯合(union))的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員的對齊按照#pragma pack指定的數值和這個資料成員自身長度中,比較小的那個進行。

2、結構(或聯合)的整體對齊規則:在資料成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大資料成員長度中,比較小的那個進行。

我們可以把objc_object的結構體簡化下,畢竟Class這個我們看著不是很順眼,順便也把用不到的OBJC_ISA_AVAILABILITY去掉

struct objc_object {
    struct objc_class *isa;
};
複製程式碼

這個簡化結果就好了很多,同時結合結構體的定義,我們就可以說:

一個Objective-c物件,實際上就是一個連續的記憶體片段,這個記憶體片段的偏移量為0長度為某一固定值(在64位系統上,一個指標佔用8個位元組)的地址內容是指向這個物件所屬類的一個結構體的指標

同時我們將結論反推回來也是成立的,說法是:

如果一個連續的記憶體片段,偏移量為0長度為某一固定值的地址內容是指向某個物件所屬類,那麼這段記憶體地址就會系統認為是這個類的一個例項物件。

有了結論,我們接下來就可以做有意思的事情了,當然就是去驗證這個結論了

我會一步一步的把這個結論演示出來:

首先先定義一個Objective-cTest

@interface Test : NSObject

@end

@implementation Test

@end
複製程式碼

接下來我們新建一個mac的命令列工具來試驗(就不新建iOS專案了,因為太費時間):

首先我們先構建一個結構體:

struct TestCase {
    void *isa;
};
複製程式碼

這個結構體是為了模擬物件的,結構體型別,只有一個泛型指標。

main函式裡我們按照如下過程寫:

//由棧區初始化結構體記憶體
struct TestCase testCase;
//將結構體中的isa指標指向Test的類  需要用__bridge 是因為 Objective-c 指標無法 直接強轉成 c的指標
testCase.isa = (__bridge void *)[Test class];
//我們把這個結構體取地址,後直接使用 __bridge 強轉成id物件,最後用Test型別的指標去接收
Test *obj = (__bridge id)&testCase;
//列印物件
NSLog(@"我是由棧區分配的物件,我的地址很大:%@",(__bridge id)&testCase);
複製程式碼

然後接下來我們執行這段程式碼,終端會返回:

2018-12-04 12:37:09.621478+0800 TestCase[41835:1359221] 我是由棧區分配的物件,我的地址很大:<Test: 0x7ffeefbff5a8>

通過列印發現,我們這個列印的就是一個沒有重寫description方法的物件的標準返回,返回中包含兩個內容:這個物件的***類*** 和 記憶體地址

此時已經說明了這個結構體已經被識別成物件了,理論上這個結構體應該已經能執行這個類的所有方法了,我們可以在Test這個類裡面增加一個物件方法

- (void)test {
    NSLog(@"執行了Test Object的-test方法");
}
複製程式碼

然後我們在這個上面的main方法中增加一個呼叫:

//呼叫物件方法
[obj test];
複製程式碼

執行程式碼,控制檯會多返回一條

2018-12-04 12:57:32.848874+0800 TestCase[42088:1396362] 執行了Test Object的-test方法

在這裡就已經可以知道了,我們的這個結構體就是徹底的一個物件了。

到這裡,本文的正文部分就相當於結束了,我們相對細緻的講解了一下Objective-c物件。

彩蛋

接下來我們可以做一個很騷的操作,這個操作我個人把它叫做偷天換日,解釋一下就是把一個例項類的物件的所屬類更換,通過這個方法,例如我們可以把原本是NSObject物件的例項替換成我們自己定義的類的例項。

接下來我們把原本main函式的方法複製出來,建立一個函式testCase1,然後清空main函式

首先,我們再新建一個Test1的類,裡面有一個物件方法-test

@interface Test1 : NSObject

@end

@implementation Test1

- (void)test {
    NSLog(@"執行了Test1 Object的-test方法");
}
@end
複製程式碼

接下來就是騷操作的表演開始,這裡我們直接就把這段程式碼生成在一個測試函式中

void testCase2() {
    //建立一個Test類的例項物件
    Test *objc = [[Test alloc] init];
    //呼叫test類的物件方法-[ test]
    [objc test];
    //用我們上文建立的TestCase結構體
    //宣告一個結構體指標,指標指向剛才建立的物件
    struct TestCase *testCase = (__bridge void *)objc;
    //騷操作開始,我們把結構體的isa替換成Test1物件所屬類
    //然後接下來就是可以放棄這個結構體指標了,我們的目標繼續迴歸原objc物件
    testCase->isa = (__bridge void *)[Test1 class];
    //呼叫test檢視返回值吧
    [objc test];
}
複製程式碼

直接執行程式,可以發現如下列印:

2018-12-04 16:44:33.922381+0800 TestCase[44225:1606663] 執行了Test Object的-test方法

2018-12-04 16:44:33.922641+0800 TestCase[44225:1606663] 執行了Test1 Object的-test方法

物件的所屬類已經替換了

總結

我們都知道物件導向有三大特徵:封裝、繼承、多型

我們可以從這個示例中看出來Objective-c是如何實現的多型,因為所有的類都是一樣的資料結構,所以多型由此形成。我們還可以從更底層的去看為什麼物件間的強轉可以生效,因為所有資料都不是預先定好的,都和執行時候的記憶體內容相關。

由此看出,Objective-c真的是一門神奇的語言

擴充

接下來,我們可以通過這個想到一些其他的面試題。

接下來就是我自己的隨意思考了。

1. Objective-C 物件可以在執行時更換所屬類麼
......
複製程式碼

好像就只額外想到一個。。。

本文首發於,本人部落格與公眾號(見下圖),如果希望轉載到公眾號,請聯絡本人開通許可權。

公眾號

公眾號剛剛起步,以後也會經常更新一些相關的技術性文章,大家也可留言一些遇到的問題,看到了一定秒回

相關文章