上一節我們了解了isa
的內部結構
,了解了結構
和類
的關系。
- 現在,我們用代碼來探究下
isa
的指針指向與類
的關系
1. 類與isa指針的關系
在objc4
源碼中,加入測試代碼。在NSLog
打印出加上斷點
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%@", person);
}
return 0;
}
- 準備好
isa
位運算的MASK(遮罩)。
image.png
p person
打印,x/4gx
打印,p/x
&與上ISA_MASK
、打印獲取到的isa中的shiftcls
地址。成功找到HTPerson
類
如果看不到,需要回看上一節。了解
isa內部構造
和獲取類地址的方法。
- 我們猜想,既然拿到了
HTPerson
類的isa地址
,那HTPerson
類的isa
指針指向哪里呢?
image.png
我們發現,0x0000000100002630
和0x0000000100002608
都打印出來是HTPerosn
的。2個地址不一樣為什么打印出來結果一樣?
類地址不應該是唯一的嗎?
這個問題我們保留。下面再一起回答。
現在,我想繼續順著這根藤(isa指針方向),看可以摸到哪個類去。
我們發現,一直順著摸,摸到NSObject
后,內存地址不再發生變化。
為了解答上面2個不同地址
都是打印了HTPerson
的問題。我們需要先了解一個新東西: ??
2. 元類(Meta)
元類的定義和創建都由系統控制,由編譯器自動完成,不受我們管理
對象的isa
來自于類
,類
也是對象
。那類
的isa
指向哪里呢?
- 答案: 元類 。類的歸屬來自于元類。
類既然是對象,就需要管理方法
、屬性
的存儲和歸屬。而這個管理者,就是元類(Meta)
上面2個
不同地址
都打印HTPerson
的問題,實際上打印路徑是: HTPerosn -> HTPerson元類 -> NSObject
問題: 既然你說打印到了元類
, 那HTPerson元類
到NSObject
之后,為什么就結束了? 不應該再打印一次NSObject
元類嗎?
- 我們順著上面代碼。打印一次
p/x [NSObject class]
:
發現[NSObject class]
打印的地址與之前打印的地址不一致
!
- 因為
[NSObject class]
打印的是NSObject
本類,而HTPerson元類
的父類是NSObject元類
。所以地址不一樣。 - 我們驗證一下。
果然,NSObject
的isa
指針指向了NSObject元類
。 地址和HTPerson元類
的isa
指針地址一致。
那么,類在內存中會存在多份(多地址)嗎?
#import <objc/runtime.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class class1 = [HTPerson class];
Class class2 = [HTPerson alloc].class;
Class class3 = object_getClass([HTPerson alloc]);
Class class4 = [HTPerson alloc].class;
NSLog(@"%p", class1);
NSLog(@"%p", class2);
NSLog(@"%p", class3);
NSLog(@"%p", class4);
}
return 0;
}
我們多種方式
讀取類,通過打印可以發現,所有地址都一樣。
- 類的信息在
內存
中永遠只存在一份
問題: 不直接繼承NSObject類,Isa是如何指向的呢?
@interface HTMan : HTPerson
@end
@implementation HTMan
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTMan * man = [[HTMan alloc]init];
NSLog(@"%p", man);
}
return 0;
}
代碼中HTMan
繼承自HTPerson
,而HTPerson
繼承自NSObject
但是isa
的指向,卻是HTMan
->HTMan元類
->NSObject元類
總結
- 類的信息在
內存
中永遠只存在一份
- 類的
isa
指針首先指向自己的元類
,再直接指向NSObject元類
(自己類->自己元類->NSObject元類) -
NSObject元類
的isa也指向NSObject元類
-
NSObject元類
是所有元類的始祖。所以也叫根元類
誤區:
- 上述是isa的
指針指向
,并非類的繼承關系。類
有繼承
關系,實例對象無繼承
關系。NSObject
沒有父類
(父類為Null)。
所以我們類的繼承,溯源只需要找到NSObject。
OC
語言中:NSObject是對象的始祖。萬物皆對象。- NSObject
根元類
的isa指針
是直接指向NSObject根元類
3. OC對象的本質
首先了解2個結構體: objc_object
(根對象)和 objc_class
(根類)
我們打開objc4
源碼,搜索struct objc_object
搜索struct objc_class
:
源碼搜索時,注意看結構體
尾部
的聲明。UNAVAILABLE
已廢棄的不要耗費精力了。
image.png
我們發現,objc_class
繼承自objc_object
。
object_object
擁有isa
屬性,所以objc_class
也擁有isa
。萬物皆對象(object)。
使用方法:
-
struct objc_object * Object
以objc_object為模板,定義一個對象 -
struct objc_class * Class
以objc_class為模板,定義一個類
3. 類的結構分析
老規矩,先從案例下手,看懂了再總結
#import <Foundation/Foundation.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%p", person);
}
return 0;
}
-
p/x
打印HTPerson
類地址。 ->x/4gx 打印內存信息
請問:
1. 為什么第一個地址是
HTPeroson
?第二個是NSObject
?
objc_class
第一個地址存放的是isa
地址,第二個存放的是superclass
類地址
(參考上面objc_object部分代碼
和objc_class部分代碼
圖。)2. 第二個地址打印的是
NSObject
類還是元類?
image.png
NSObject.class
打印的地址與第二個地址
打印的一致
。 與接著打印的NSObject
元類地址不一致
。 說明第二個地址
打印的是NSObjetc
自身類。
3. 內存偏移
老規矩,先從案例開始,在main.m
文件中加入測試代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int f[4] = {10,20,30,40};
NSLog(@"%p", &f);
NSLog(@"%p", &f[0]);
NSLog(@"%p", &f[1]);
NSLog(@"%p", &f[2]);
NSLog(@"%p", &f[3]);
}
return 0;
}
在打印的尾部加入斷點
我們發現:
-
&f
和&f[0]
內存地址一樣。 證明對象
的內存地址
就是使用內部首元素
的地址
-
-
f
是int
類型的數組,內部元素每位占用4字節
。 所以每個元素內存偏移值
為4
。
-
是不是瞬間有個小想法:
我們是否可以根據數據類型
,確定內存占用空間的大小
,通過內存值偏移
,就可定位
到下一元素
的指針地址。然后直接取出
這個指針地址指向的值
。
小拓展: 如何通過
地址
取出對應的值
:
知識點: 指針(地址)的指針
地址
強轉
為指定類型(int) -> 加*
讀取對象(指針)的指針
-> 打印目標值
image.png快速讀取:
p *((int *)0x7ffeefbff5b0)
image.png
我們通過首地址偏移
,果然:
完美??
但前提是我們必須知道偏移值
是多少,也就是存儲
的是什么類型
。
- 上面我們使用的是
地址偏移
,所以必須加入偏移值
。 - 我們也可以使用
指針偏移
。直接在屬性層在進行操作。
至此。我們已經掌握了地址偏移
和指針偏移
。
- 只有知道
屬性類型
,占用空間大小
,才能使用地址偏移
和指針偏移
,讀取類的所有信息。
現在,我們來分析類的結構
4. 分析類結構
在objc4
源碼中,搜索objc_class
。
-
剔除
不會占用類空間的const
、void
、static
和函數
。
struct objc_class : objc_object {
// Class ISA; 。 // 指針 - 占用8字節
Class superclass; // 指針 - 占用8字節
cache_t cache; // ?
class_data_bits_t bits;
};
如果我們要獲取bits
的首地址位置。只有cache_t
類型不知道空間大小。 點進去看看:
-
剔除
不會占用類空間的const
、void
、static
和函數
。
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
- 我們進入
explicit_atomic
查看:
- 發現返回的類型就是傳入的泛型
T
。 所以explicit_atomic
的類型只跟他傳入
的類型相關。
- 我們進入
bucket_t
結構體:
這里的
_sel
和_imp
是不是很眼熟。 ?? 后續我們會詳細介紹
進入uintptr_t
:
- 發現
uintptr_t
實際就是unsigned long
,64位操作操作系統下占用8
個字節
所以我們_buckets
實際大小就是8字節
接下來我們看mask_t
。
-
點進去
image.png
發現它就是uint32_t
類型。占用4
字節
匯總一下:
現在系統都是64位系統。
所以
cache_t
內存大小為: 12 + 2 + 2 =16 字節
回頭繼續分析object_class
的內存大小
struct objc_class : objc_object {
// Class ISA; 。 // 指針 - 占用8字節
Class superclass; // 指針 - 占用8字節
cache_t cache; // 占用16字節
class_data_bits_t bits;
};
所以我們得出結論。
-
object_class內部的找到bits,需要偏移8 + 8 + 16 =
32
位
現在,我們來讀取bits
5. 讀取bits
-
x/4gx HTPerson.class
打印內存地址 -
p/x 0x0000000100002390 + 32
獲取bits地址 -
p (class_data_bits_t *)$12
強轉為class_data_bits_t
類型
在objc_class
結構中,我們發現bits.data()
返回的是class_rw_t
類型
我們打印bits->data()
檢驗一下
成功拿到,打印HTPerson
的class_rw_t信息:
- 我們想要尋找這個類的
方法
和屬性
,但是發現只有一個ro_or_rw_ext
。
我們進入class_rw_t
內部, 折疊所有函數。
Xcode折疊所有函數:
command + shift + ←
6. 讀取methods、properties和protocols
main.m
加入測試代碼:
@interface HTPerson : NSObject {
NSString * hobby;
}
@property(nonatomic, copy) NSString * name;
@property(nonatomic, copy) NSString * nickname;
+(void) ClassFunction;
-(void) objectFunction;
@end
@implementation HTPerson
+(void) ClassFunction {
NSLog(@"類方法");
}
-(void) objectFunction{
NSLog(@"對象方法");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%p", person);
}
return 0;
}
NSLog
處加入斷點
,運行
代碼。快速找到data()
對象的位置:
p/x HTPerson.class
- ->
p/x (class_data_bits_t *)(0x0000000100002258 + 32)
- ->
p $1->data()
methods
利用$2
對象打印p $2->methods()
:
我從圖中看到了list
。繼續打印p *$3.list
:
- 看到
count
有6個值,我們使用get
打印一下:
properties
利用$2
對象打印p $2->properties()
:
接下來打印p * properties()
屬性方法。
protocols
利用$2
對象打印p $2->protocols()
:
成員變量hobby
去哪了?
打印ro
打印p *$23.ivars
打印每一個成員變量p $25.get(0)
- 我們發現,除了
hobby
變量。所有property
屬性,都自動生成了帶下劃線
的成員變量。
為什么案例中的ClassFunction
類方法沒出現在Methods中?
對象的isa
指向自己的類,所以對象方法存放到了HTPerosn
中,難道類方法存放在它的isa
上一級?HTPerson元類中
?
實踐出真知:
-
p HTPerson.class
->x/4gx $0
->p/x (class_data_bits_t *)(0x0000000100002230 + 32)
->p $1->data()
->p $2->methods()
->p $3.list
->p *$4
->p $5.get(0)
image.png
果然。 對象
的方法存在類
中,類
的方法存在元類
中
下一章