如果iOS中談到取屬性,相信大家都會誇誇其談,不就是get方法嗎?或者大談kvc取屬性的機制。不得不說這些也是對的。這時大家可能就疑惑了,那你還要說啥的!!大家不妨想想,這些都是程式碼層的實現,其實我們的程式碼最終都會被編譯,然後載入到記憶體中,那你在記憶體中是怎麼取到屬性的呢??對的我們討論就是它!
指標
如果說到記憶體,不知道大家會不會想到指標呢?這裡簡單介紹一下,讓大家有個簡單的理解。如果理解不了的話,建議大家找一個C語言的教程,學一下指標。
指標(Pointer)是程式語言中的一個物件,利用地址,它的值直接指向(points to)存在電腦儲存器中另一個地方的值。由於通過地址能找到所需的變數單元,可以說,地址指向該變數單元。因此,將地址形象化的稱為“指標”。意思是通過它能找到以它為地址的記憶體單元。
- 那到底什麼是指標呢??
型別 * 變數名
這就是宣告瞭一個指標變數
- 指標型別有什麼作用呢?
比如:
int* num;
複製程式碼
指標變數的型別決定了通過這個指標找到變數的首地址以後,連續操作多少個位元組空間 為什麼會說連續操作多少個位元組空間??主要是指標有算術運算加減,說白了就是指標的移動。
-
指標是int* 連續操作4個位元組
-
指標是double* 連續操作8個位元組
比如:
int* p = #
p++;
複製程式碼
當指標+1的時候,這時候指標要移動1個單元,而不是1個位元組!!
那到底這1個單元是多大呢?其實1個單元的大小就是指標型別的大小。這裡是int
型,所以移動了4個位元組
以上就是簡單給大家做了指標介紹,其實理解了指標,對於我們出現的一些野指標的bug、runtime原始碼中的一些機制等等是有所幫助的。言歸正傳。接下來讓我看一道題,真正的去了解記憶體和指標的關係。
int num1 = 10;
int num = 20;
int* p = #
p++;
printf("%d\n",*p);//列印為10,因為p++,指標已經移動了4個位元組,下一個記憶體儲存10正好是4個位元組
複製程式碼
這裡其實是前邊宣告瞭一個num1,正好是4個位元組,所以就將10取出來了。(說白了就是記憶體中下一個連續的4個位元組存的是什麼取出來就是什麼)
說了這麼多都是指標和記憶體,建議大家搞明白以上內容再讀以下的內容,如果上邊都搞不明白的話,下邊有關iOS中runtime取屬性的內容有可能就會雲裡霧裡。
iOS中成員變數與屬性
以下題目是sunnyxx習題中的一題,網上也有詳細的答案。這裡作者就簡述一下自己的理解,如果想看非常詳細的答案的話可以點選上邊的連結。
下面程式碼會? Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak
{
NSLog(@"my name is %@", self.name);
}
@end
@interface Test : NSObject
@end
@implementation Test
- (instancetype)init
{
self = [super init];
if (self) {
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[[Test alloc] init];
}
return 0;
}
複製程式碼
答案:程式碼正常輸出,輸出結果為:
2014-11-07 14:08:25.698 Test[1097:57255] my name is
- 為什麼能夠正常執行,並呼叫到speak方法?
計算機將我們的Sark
類資訊通過
id cls = [Sark class];
這一行載入到記憶體中,並且取得了cls
變數。這個時候其實我們只要知道cls
這個變數的地址就行了,其實相當於類的物件的地址。void *obj = &cls;
這句話就讓我們獲得了物件的地址。(平時我們new
物件的時候就幹了兩件事:1、申請記憶體;2、獲取記憶體的地址(物件變數的地址就是記憶體的地址),這裡的物件與我們new
出來的物件有所不同。但是雖然不是new物件,iOS中Class
物件已經儲存了我們需要的東西。比如有關變數的記憶體偏移、方法等等所有的資訊)接下來可以幹我們想幹的任何事情了。
iOS中
Class
中儲存了我們想要的東西,這一塊的知識要上升到了runtime的原始碼,上邊給到的連結中有詳細介紹。其實大家想想編譯完之後肯定得有一個類或者其他東西儲存著有關記憶體等等相關的資訊的。
- 為什麼self.name會輸出?
我們程式在編譯之後其實就是一堆的彙編指令,彙編操作的就是記憶體地址。所以當我們程式執行的時候都是暫存器一條條的執行彙編指令。其實執行彙編指令最重要的就是變數、方法、物件等等的一大堆地址,因為暫存器有限,所以會把有限的資料從記憶體中載入到暫存器。所以總得來說是操作暫存器的地址和記憶體地址。如果沒有地址那怎麼知道執行什麼呢?所以只要有地址了就好辦了。
指令如下圖:
變數對應於runtime的objc_ivar程式碼如下:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
複製程式碼
其中 ivar_offset
就是變數的地址偏移位元組。
變數地址=物件地址 + 基類大小 + ivar偏移位元組
到這裡再結合我上邊指標的鋪墊相信大家應該明白了為什麼為什self.name會輸出吧。
其實通過這裡我們也知道了其實iOS中取物件就是指標的偏移。
Student *student = [[Student alloc] init];
Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");
int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));
NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));
*age_pointer = 10;
NSLog(@"%@", student);
複製程式碼