面試驅動技術之 - isa && 元類 && 函式呼叫

小蠢驢打程式碼發表於2019-01-20

偽裝成首頁

面試驅動技術之 - 帶著面試題來找答案

  • 一個NSObject 物件,佔用多少記憶體
  • 物件方法 與 類方法的存放在哪
  • 什麼是isa指標
  • 什麼是meta-class
  • megsend 是如何找到方法的
@implementation MNSubclass

- (void)compareSelfWithSuperclass{
    NSLog(@"self class = %@",[self class]);
    NSLog(@"super class = %@",[super class]);
}
@end
複製程式碼
  • 輸出的結果是什麼
  • 。。。

友情tips:如果上訴問題你都知道答案,或者沒有興趣知道,就可以不用繼續往下看了,興趣是最好的老師,如果沒有興趣知道這些,往下很難讀得進去~


OC物件的本質

我們平時編寫的Objetcive-C,底層實現都是C/C++實現的

面試驅動技術之 - isa && 元類 && 函式呼叫

  • 問 : Objetcive-C 基於 C/C++ 實現的話,Objetcive-C 物件相當於C/C++ 中的什麼資料結構呢?
@interface MNPerson : NSObject
{
    int _age;
    double _height;
    NSString *name;
}
複製程式碼

MNPerson為例,裡面的成員變數有不同型別是,比如intdoubleNSString 型別,假如在C/C++ 中用陣列儲存,顯然是不太合理的

  • 答: C/C++中用 結構體 的資料格式,表示oc物件。
// 轉成c/c++ 程式碼後,MNPerson 的結構如下

struct NSObject_IMPL {
	Class isa;
};

struct MNPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	double _height;
	NSString *name;
};
複製程式碼

使用指令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc oc原始檔 -o 輸出的c++檔案oc 程式碼轉成 c++ 程式碼之後,發現內部確實是結構體



面試題來襲!前方請做好準備!!

  • 一個NSObject 物件,佔用多少記憶體

  • 思路:

      1. 由上面可知,NSObject的本質是結構體,通過NSObject.m 可以發現,NSObject 只有一個 isa 成員,isa 的本質是 class, struct objc_object * 型別,所以應該佔據 8 位元組
    • 2.NSLog(@"%zu",class_getInstanceSize([NSObject class])); 輸出 - size = 8
  • 注意!實際上,

{
    //獲得 - NSObject 一個例項物件的成員變數所佔用的大小 >> 8
    NSLog(@"%zu",class_getInstanceSize([NSObject class]));
    
    NSObject *obj = [[NSObject alloc]init];
    // 獲取 obj 指標,指向的記憶體大小 >> 16
    NSLog(@"%zu",malloc_size((__bridge const void *)obj));
}
複製程式碼
//基於 `objc-class.m` 檔案 750 版本

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// Class‘s ivar size rounded up to a pointer-size boundary.
// 點選一下 - 智慧翻譯 ==> (返回類的成員變數所佔據的大小)

uint32_t alignedInstanceSize() 
{
    return word_align(unalignedInstanceSize());
}
複製程式碼

opensource 原始碼

物件建立 - alloc init, 查詢alloc底層實現

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
複製程式碼
  • CoreFoundation 硬性規定,一個物件,至少有 16 位元組
  • NSObject為例,這裡傳入的 sizealignedInstanceSize, 而alignedInstanceSize 已經知道 = 8, 8 < 16,retun 16, 最終 NSObject 建立的物件,佔據的記憶體大小是 16

lldb 除錯下,使用 memory read 檢視物件記憶體

(lldb) p obj
(NSObject *) $0 = 0x000060000000eb90
(lldb) memory read 0x000060000000eb90
0x60000000eb90: a8 6e 3a 0b 01 00 00 00 00 00 00 00 00 00 00 00
複製程式碼

也能發現,前8 位儲存 isa 指標,後 8 位都是0,但是整個物件還是佔據了 16 個位元組


一個NSObject記憶體分配示意圖

一個NSObject記憶體分配示意圖

總結:

  • 問 :一個NSObject 物件,佔用多少記憶體?
  • 答 :
    • 系統在alloc的時候,分配了16個位元組給 NSObject 物件(malloc_size函式獲得)
    • 但是實際上 NSObject 只使用了 8個位元組的儲存空間(64bit系統下)
    • 可以通過class_getInstanceSize()

循序漸進之面試題又來了!!

@interface MNStudent : NSObject
{
    int _age;
    int _no;
}
@end
複製程式碼
  • 問:一個MNStudent 物件,佔用多少記憶體
  • 答:
    • 由上面 NSObject佔據16個位元組可知,base = 16
    • 一個int佔4位元組,age = 4, no = 4
    • 最終結果, 16 + 4 + 4 = 24!

面試驅動技術之 - isa && 元類 && 函式呼叫

哈哈!中計了!

面試驅動技術之 - isa && 元類 && 函式呼叫

原理解釋:

面試驅動技術之 - isa && 元類 && 函式呼叫

  1. 之前 NSObject 建立一個物件,確實是分配了 16 個位元組的空間
  2. 但是,他還有未使用的空間8個位元組,還是可以儲存的
  3. 這裡的age && no 存進去,正好 16,之前分配的空間正好夠用!所以答案是 16!

循循序漸進之面試題雙來了!!

(大哥別打了,這次保證不挖坑了,別打,疼,別打臉別打臉。。。)

@interface MNPerson : NSObject
{
    int _age;
    int _height;
    NSString *name;
}
複製程式碼
  • 問: 一個MNPerson物件,佔用多少記憶體
  • 答: 這題我真的會了!上面的坑老夫已經知道了!
    • 預設建立的時候,分配的內容是16
    • isa = 8, int age = 4, int height = 4, NSString = char * = 8
    • 最終分配: 8 + 4 + 4 + 8 = 24

面試驅動技術之 - isa && 元類 && 函式呼叫

哈哈哈哈! 又中計了!

面試驅動技術之 - isa && 元類 && 函式呼叫

這時候你肯定好奇了

    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

    
    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }
    
複製程式碼
  • extraBytes 一般都是 0,這裡可以理解為 size = alignedInstanceSize()
  • alignedInstanceSize = class_getInstanceSize, class_getInstanceSize 由上圖的log資訊也可以知道 = 24
  • 內心os: who tm fucking 32?

面試驅動技術之 - isa && 元類 && 函式呼叫

ios記憶體分配原始碼

下載`libmalloc`,找到`calloc`的實現

void *
calloc(size_t num_items, size_t size)
{
	void *retval;
	retval = malloc_zone_calloc(default_zone, num_items, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}

發現這個函式,就是我們呼叫的`calloc`函式的底層

void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

	void *ptr;
	if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
		internal_check();
	}

	ptr = zone->calloc(zone, num_items, size);
	
	if (malloc_logger) {
		malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
				(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
	}

	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
	return ptr;
}

複製程式碼

內心os: exo me? 這傳入的 size_t size = 24,怎麼返回32的??

涉及到 - 記憶體對齊

檢索Buckets - (libmalloc 原始碼)

#define NANO_MAX_SIZE			256 
/* Buckets sized {16, 32, 48, ..., 256} */
複製程式碼
  1. 發現,iOS 系統分配的時候,有自己的分配規則, 不是說你需要的size多大,就建立多大
  2. 作業系統內部有自己的一套規則,這裡的都是 16 的倍數,而作業系統在此基礎之上,作業系統的操作訪問最快
  3. 所以,在MNPerson 物件需要 24 的size的時候,作業系統根據他的規則,直接建立了 32 的size的空間,所以這裡的答案是 32!

面試驅動技術之 - isa && 元類 && 函式呼叫


補充說明: sizeof 運算子

(lldb) po [obj class]
MNPerson

(lldb) po sizeof(obj)
8

(lldb) po sizeof(int)
4
複製程式碼
  • sizeof是運算子,不是函式,編譯時即知道,不是函式
  • 這裡的 obj 是物件, *obj - 指標指向,編譯器知道他是指標型別,所以 sizeof = 8(指標佔據8個位元組) - 特別注意,這裡傳入的不是物件!是指標
  • sizeof是告訴你傳入的型別,佔多少儲存空間


OC物件的分類

  • 例項物件(instance物件)
  • 類物件(class物件)
  • 元類物件(meta-class物件)

instance 物件

  • 通過類 alloc 出來的物件
  • 每次 alloc 都會產生新的instance 物件(記憶體不相同)
  • instance 物件儲存的資訊
    • isa 指標
    • 其他成員變數

class 物件

  • 是建立物件的藍圖,描述了所建立的物件共同的屬性和方法(made in 維基百科)
  • 類在記憶體中只有一份,每個類在記憶體中都有且只有一個 class 物件
  • class物件在記憶體中儲存的資訊
    • isa 指標
    • superclass 指標
    • 類的物件方法 && 協議
    • 類的屬性 && 成員變數資訊
    • 。。。

meta-class

Class metaClass = object_getClass([NSObject class]);

  • metaclssNSObjectmeta-class物件
  • meta-class 在記憶體中只有一份,每個類都有且只有一個 meta-class 物件
  • meta-class 也是類,與class的物件結構一樣,但是內部的資料不一樣(用途不同)
  • meta-clas 包括:
    • isa指標
    • superclass
    • 類方法
    • 。。。

面試驅動技術之 - isa && 元類 && 函式呼叫

提問:object_getClassobjc_getClass的區別

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
複製程式碼
  • object_getClass : 傳入的是可以是任意物件(id型別),返回的是類 or 元類
    • 如果傳入 instance 物件,返回 class
    • 如果傳入 class, 返回的是 meta-class 物件
    • 如果傳入的是 meta-class,返回的是 root-meta-class 物件
Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;

    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}
複製程式碼
  • 傳入的是類名字串,返回的是該類名對應的類
  • 不能返回元類

指向圖.png

(圖片來自於 www.sealiesoftware.com/blog/archiv…)

看懂這張圖 - 就等價於看懂 isa && superclass

探究流程:

@interface MNSuperclass : NSObject

- (void)superclassInstanceMethod;
+ (void)superClassMethod;

@end

@implementation MNSuperclass

- (void)superclassInstanceMethod{
    NSLog(@"superclass-InstanceMethod - %p",self);
}

+ (void)superClassMethod{
    NSLog(@"+ superClass-classMethod- %p",self);
}

@end

@interface MNSubclass : MNSuperclass

- (void)subclassInstanceMethod;

@end

@implementation MNSubclass

- (void)subclassInstanceMethod{
    NSLog(@"subclassInstanceMethod- %p",self);
}

@end

複製程式碼

問: 子類呼叫父類的物件方法,執行的流程是如何的?

MNSubclass *subclass = [[MNSubclass alloc]init];
[subclass superclassInstanceMethod];
複製程式碼
  • 思路:
    • subclass 呼叫物件方法,物件方法存在 class
    • 第一步,先找到 subclass物件,通過 isa 指標,找到其對應的 MNSubclass
    • MNSubclass 是否有 superclassInstanceMethod 方法的實現,發現沒有,MNSubclass 沿著 superclass 指標找到他的父類 - MNSuperclass
    • 此時,MNSuperclass 中找到 superclassInstanceMethod 的實現,呼叫它,整個流程結束

面試驅動技術之 - isa && 元類 && 函式呼叫


問: 子類呼叫父類的類方法,執行的流程是如何的?

[MNSubclass superClassMethod];

  • 思路:
    • 類方法存在meta-class
    • 第一步,找到對應的MNSubclass,沿著isa指標,找到其對應的meta-class
    • MNSubclassmeta-class 中是否有 superClassMethod 方法的實現,發現沒有,沿著 superclass 指標找到 MNSuperclassmeta-class
    • 發現 MNSuperclassmeta-classsuperClassMethod 方法實現,呼叫,流程結束

面試驅動技術之 - isa && 元類 && 函式呼叫


圖中比較難理解的一根線

面試驅動技術之 - isa && 元類 && 函式呼叫

探究 : 元類物件的superclass 指標是否指向 rootclass

  • 分析:
    • meta-class 物件儲存的是類方法,class 儲存的是 物件方法
    • 從物件導向的角度來講,一個類呼叫一個類方法,不應該最後呼叫到 物件方法
    • 這裡的Root class 就是 NSObject, 要給 NSObject 新增方法就要用到 分類
    • 驗證 NSObject 的物件方法是否會被呼叫

//"NSObject+MNTest"類的宣告 && 實現

@interface NSObject (MNTest)

+ (void)checkSuperclass;

@end

@implementation NSObject (MNTest)

+ (void)checkSuperclass{
    NSLog(@"+NSObject checkSuperclass - %p",self);
}

@end

//main函式中呼叫
int main(int argc, char * argv[]) {
    @autoreleasepool
    {
        [MNSubclass checkSuperclass];
        NSLog(@"MNSubclass = %p",[MNSubclass class]);
    }
    
    return 0;
}

--------------------------------------------------------
控制檯輸出:
+NSObject checkSuperclass - 0x105817040
InterView-obj-isa-class[36303:7016608] MNSubclass = 0x105817040

複製程式碼
  1. 發現,呼叫checkSuperclass 類方法的,是MNSubclass
  2. 這時候要驗證上面那條 meta-class 指向 root-class的線, 這裡的root-class 即等於 NSObject
  3. root-class中只存物件方法,這裡,只要驗證,NSObject 中同名的類方法實現取消,變成同名的物件方法測試,即可得出結論
  4. 宣告的還是+ (void)checkSuperclass,實現的方法用 - (void)checkSuperclass物件方法替換

@interface NSObject (MNTest)

+ (void)checkSuperclass;

@end

@implementation NSObject (MNTest)

//+ (void)checkSuperclass{
//    NSLog(@"+NSObject checkSuperclass - %p",self);
//}

- (void)checkSuperclass{
    NSLog(@"-NSObject checkSuperclass - %p",self);
}

@end

//main函式中呼叫
int main(int argc, char * argv[]) {
    @autoreleasepool
    {
        [MNSubclass checkSuperclass];
        NSLog(@"MNSubclass = %p",[MNSubclass class]);
    }
    
    return 0;
}

--------------------------------------------------------
控制檯輸出:
-NSObject checkSuperclass - 0x101239040
InterView-obj-isa-class[36391:7022301] MNSubclass = 0x101239040

複製程式碼

發現 - 呼叫的還是類方法 + (void)checkSuperclass ,但是最終實現的,卻是物件方法 - (void)checkSuperclass

  • 原因:
    • [MNSubclass checkSuperclass] 其實本質上,呼叫的是傳送訊息方法,函式類似是objc_msgsend([MNSubclass class], @selector(checkSuperclass))
    • 這裡的@selector(checkSuperclass) 並未說明是 類方法 or 物件方法
    • 所以最終走流程圖的話,root-meta-class通過super class找到了root-class (NSObject),
    • NSObject 類不是元類,儲存的是物件方法,所以 最終呼叫了NSObject -checkSuperclass這個物件方法

image-20190326212536530

叮叮叮!循循循序漸進之面試題叒來了!!

@implementation MNSubclass

- (void)compareSelfWithSuperclass{
    NSLog(@"self class = %@",[self class]);
    NSLog(@"super class = %@",[super class]);
}
@end

----------------------------------------
呼叫:
    MNSubclass *subclass = [[MNSubclass alloc]init];
    [subclass compareSelfWithSuperclass];

複製程式碼
  • 問: [self class] && [super class] 分別輸出什麼

  • 思路:

    • class 方法 是NSObject 的一個物件方法,對方方法存在 class
    • 第一步,先找到 subclass物件,通過 isa 指標,找到其對應的 MNSubclass
    • MNSubclass 是否有 class 方法的實現,發現沒有,MNSubclass 沿著 superclass 指標找到他的父類 - MNSuperclass
    • 查詢MNSuperclass 中是否有 class 方法的實現,發現沒有,MNSuperclass 沿著 superclass 指標找到他的父類 - NSObject
    • 最終在 NSObject 中找到 class 的實現
    • 而呼叫方都是都是當前物件,所以最後輸出都是 - MNSubclass

驗證:

NSLog(@"self class = %@",[self class]);
NSLog(@"super class = %@",[super class]);

----------------------------------------------------------------------
控制檯輸出:
InterView-obj-isa-class[36796:7048007] self class = MNSubclass
InterView-obj-isa-class[36796:7048007] super class = MNSubclass
複製程式碼


感謝小碼哥 的精彩演出,文章中如果有說錯的地方,歡迎提出,一起學習~




demo

歡迎點贊fork~



友情客串:

小碼哥

神經病院runtime入學考試

gun

libmalloc

objc4

sealiesoftware.com

相關文章