Objective-C是一門動態(tài)性比較強的編程語言,跟C、C++等語言有著很大的不同。
什么叫動態(tài)性?
一般應(yīng)用程序運行需要經(jīng)過三步:編寫代碼 -> 編譯 -> 運行,如果是C語言,編譯之后方法就確定了,運行的時候就會調(diào)用那個方法。但是對于OC,由于其動態(tài)性,就算編譯完了,也可以在程序運行時決定調(diào)用哪個方法,而且就算沒有實現(xiàn)某個方法,也可在運行的時候動態(tài)生成某個方法。
Objective-C的動態(tài)性是由Runtime API來支撐的,Runtime API基本是開源的,源碼在https://opensource.apple.com/tarballs/搜索objc,點擊objc4文件夾進去,下載一個最新的(數(shù)字最大的)。
Runtime API提供的接口基本都是C語言的,源碼由C\C++\匯編語言編寫。
一. isa類型
在isa指針和superclass指針簡單講了,實例對象的isa&ISA_MASK得到類對象的地址值,類對象的isa&ISA_MASK得到元類對象的地址值。
要想學(xué)習(xí)Runtime,首先要了解它底層的一些常用數(shù)據(jù)結(jié)構(gòu),比如isa指針。在arm64架構(gòu)之前,isa就是一個普通的指針,的確存儲著類對象、元類對象的內(nèi)存地址,從arm64架構(gòu)開始,對isa進行了優(yōu)化,變成了一個共用體(union)結(jié)構(gòu),還使用位域來存儲更多的信息。
在objc4搜索“objc_object {”
struct objc_object {
private:
isa_t isa; //64位之前是Class isa
......
}
發(fā)現(xiàn),64位之后isa是isa_t類型的,isa_t是共用體(在isa指針和superclass指針中,我們知道,64位之前isa是Class類型的)進入isa_t:
union isa_t
{
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
二. 為什么要設(shè)計成共用體?
創(chuàng)建一個MJPerson對象,里面有三個屬性,創(chuàng)建對象,并且打印對象占用內(nèi)存大小。
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.rich = YES; // 1字節(jié)
person.tall = NO; // 1字節(jié)
person.handsome = NO; // 1字節(jié)
NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
NSLog(@"%zd", class_getInstanceSize([MJPerson class])); //16字節(jié)
}
return 0;
}
打印為16字節(jié)(isa指針占8字節(jié),BOOL值一共占用3字節(jié),一共11字節(jié),再加上內(nèi)存對齊,所以是16字節(jié))
有沒有占用內(nèi)存更小的方式實現(xiàn)給MJPerson添加三個BOOL屬性呢?
方式一:char類型的位運算
8位二進制是一個字節(jié)(0b0000 0000),BOOL值只有0和1兩個值,所以其實我們可以使用一個字節(jié)(最后3位)來表示這三個值。
原理很簡單,先使用一個char類型的成員變量保存這三個屬性:
- 取值:
使用掩碼的按位與運算,想取出哪一位的值,就把哪一位置為1,其他為0。 - 設(shè)值:
使用掩碼按位或運算,可以將某一位置為1,而不改變其他位,實現(xiàn)設(shè)置YES。
使用掩碼取反的按位與運算,將某一位置為0,而不改變其他位,實現(xiàn)設(shè)置NO。
詳細(xì)見代碼和注釋:
MJPerson.m
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
//@property (assign, nonatomic, getter=isTall) BOOL tall;
//@property (assign, nonatomic, getter=isRich) BOOL rich;
//@property (assign, nonatomic, getter=isHansome) BOOL handsome;
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
MJPerson.h
#import "MJPerson.h"
//什么是位運算?與或非運算 (& | ~) 感嘆號!是給BOOL值取反的
//按位“與”,都是1結(jié)果才是1,其他結(jié)果都是0
//開發(fā)中使用掩碼的按位與運算來取出特定位的值,想取出哪一位的值,就把哪一位置為1,其他為0
// 0000 0111
//&0000 0100
//------
// 0000 0100
//掩碼按位或運算,可以將某一位置為1,而不改變其他位
//掩碼取反的按位與運算,將某一位置為0,而不改變其他位
// 掩碼,一般用來做按位與(&)運算的
//#define MJTallMask 0b00000001
//#define MJRichMask 0b00000010
//#define MJHandsomeMask 0b00000100
#define MJTallMask (1<<0) //1往左位移0位
#define MJRichMask (1<<1) //1往左位移1位
#define MJHandsomeMask (1<<2) //1往左位移2位
@interface MJPerson()
{
char _tallRichHansome; //只占一個字節(jié) 0b 0000 0000
}
@end
@implementation MJPerson
- (instancetype)init
{
if (self = [super init]) {
_tallRichHansome = 0b00000100;
}
return self;
}
- (void)setTall:(BOOL)tall
{
if (tall) {
//掩碼的按位或運算,將某一位置為1
_tallRichHansome |= MJTallMask;
} else {
//掩碼取反的按位與運算,將某一位置為0
_tallRichHansome &= ~MJTallMask;
}
}
- (BOOL)isTall
{
//與運算之后,結(jié)果要么是0要么有值,但是我們需要的是BOOL值,怎么轉(zhuǎn)換呢?
//取反,再取反
return !!(_tallRichHansome & MJTallMask);
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHansome |= MJRichMask;
} else {
_tallRichHansome &= ~MJRichMask;
}
}
- (BOOL)isRich
{
return !!(_tallRichHansome & MJRichMask);
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHansome |= MJHandsomeMask;
} else {
_tallRichHansome &= ~MJHandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHansome & MJHandsomeMask);
}
@end
上面即可實現(xiàn)使用一個字節(jié)給MJPerson對象添加三個BOOL屬性。
方式二:結(jié)構(gòu)體的位域
上面使用了一個char類型的位運算來實現(xiàn),其實還可以使用結(jié)構(gòu)體的位域來實現(xiàn),也是占一個字節(jié)。
#import "MJPerson.h"
@interface MJPerson()
{
//char占一個字節(jié),8位
//結(jié)構(gòu)體支持位域,使用:1代表只占一位,這時候就不看左邊的char了
//這時候整個結(jié)構(gòu)體就占3位,系統(tǒng)不可能分配3位給它,所以至少占用一個字節(jié)
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome; // 0b0000 0000
//最后三位放上面的值,而且前面的成員放在最右邊(tall最后一位,rich倒數(shù)第二位,handsome倒數(shù)第三位)
}
@end
@implementation MJPerson
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (BOOL)isTall
{
//將0b1 -> 0b0000 0000
//BOOL值是8位二進制,但是我們存下來的是一位(0b1),如何將一位轉(zhuǎn)成8位二進制呢?取反再取反
//如果不做兩次取反操作,系統(tǒng)會自動把所有的0都b賦值為1,就是0b1 -> 0b1111 1111,就不對了
return !!_tallRichHandsome.tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (BOOL)isRich
{
return !!_tallRichHandsome.rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)isHandsome
{
return !!_tallRichHandsome.handsome;
}
@end
代碼比較簡單,可自行看注釋。下面驗證,執(zhí)行代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.rich = YES;
person.tall = YES;
person.handsome = YES;
NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
打斷點,打印內(nèi)存:
(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $0 = 0x00000001005236a8
(lldb) x 0x00000001005236a8
0x1005236a8: 07 00 00 00 00 00 00 00 b1 41 ec ae ff ff 1d 00 .........A......
0x1005236b8: 8c 07 00 00 01 00 00 00 33 73 75 69 74 65 2f 49 ........3suite/I
打印_tallRichHandsome地址,查看內(nèi)存,可以看出這個地址值為7,轉(zhuǎn)成二進制就是0b0000 0111,說明代碼沒問題。
方式三:共同體
isa就是使用了共同體,共用體里面的成員共用一塊內(nèi)存。
如下,結(jié)構(gòu)體:
struct Date {
int year; //4字節(jié)
int month; //4字節(jié)
int day; //4字節(jié)
};
它在內(nèi)存中結(jié)構(gòu)如下:

如果是共用體:
union Date {
int year; //4字節(jié)
int month; //4字節(jié)
int day; //4字節(jié)
};
在內(nèi)存中結(jié)構(gòu)如下:

先看如下代碼,再看代碼后的解釋:
#import "MJPerson.h"
#define MJTallMask (1<<0)
#define MJRichMask (1<<1)
#define MJHandsomeMask (1<<2)
#define MJThinMask (1<<3)
@interface MJPerson()
{
union { //共用體
char bits; //1字節(jié)
struct {
char tall : 1; //1位
char rich : 1; //1位
char handsome : 1; //1位
char thin : 1; //1位
};//這個結(jié)構(gòu)體僅僅是為了可讀性,而且自始至終一直都在操作bits,刪除這個結(jié)構(gòu)體也不影響
} _tallRichHandsome;
}
@end
@implementation MJPerson
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= MJTallMask;
} else {
_tallRichHandsome.bits &= ~MJTallMask;
}
}
- (BOOL)isTall
{
return !!(_tallRichHandsome.bits & MJTallMask);
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= MJRichMask;
} else {
_tallRichHandsome.bits &= ~MJRichMask;
}
}
- (BOOL)isRich
{
return !!(_tallRichHandsome.bits & MJRichMask);
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= MJHandsomeMask;
} else {
_tallRichHandsome.bits &= ~MJHandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHandsome.bits & MJHandsomeMask);
}
- (void)setThin:(BOOL)thin
{
if (thin) {
_tallRichHandsome.bits |= MJThinMask;
} else {
_tallRichHandsome.bits &= ~MJThinMask;
}
}
- (BOOL)isThin
{
return !!(_tallRichHandsome.bits & MJThinMask);
}
@end
上面的代碼使用了方式一的位運算來設(shè)值和取值,使用了方式二的結(jié)構(gòu)體位域來實現(xiàn)代碼的可讀性。通過共用體將bits和結(jié)構(gòu)體結(jié)合起來,而且自始至終一直都在操作bits,沒有動結(jié)構(gòu)體,結(jié)構(gòu)體僅僅是為了可讀性,所以不會影響bits里面的值,刪除這個結(jié)構(gòu)體也不影響。
這種方式就是巧妙的利用共用體,達到了代碼可讀性的目的。
三. isa_t的共用體
現(xiàn)在看isa_t的共用體你應(yīng)該就很容易理解了。
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
//0,代表普通的指針,存儲著Class、Meta-Class對象的內(nèi)存地址
//1,代表優(yōu)化過,使用位域存儲更多的信息
uintptr_t nonpointer : 1;
//是否有設(shè)置過關(guān)聯(lián)對象,如果沒有,釋放時會更快
uintptr_t has_assoc : 1;
//是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時會更快
uintptr_t has_cxx_dtor : 1;
//存儲著Class、Meta-Class對象的內(nèi)存地址信息
uintptr_t shiftcls : 33;
//用于在調(diào)試時分辨對象是否未完成初始化
uintptr_t magic : 6;
//是否有被弱引用指向過,如果沒有,釋放時會更快
uintptr_t weakly_referenced : 1;
//對象是否正在釋放
uintptr_t deallocating : 1;
//引用計數(shù)是否過大無法存儲在isa中
//如果為1,那么引用計數(shù)會存儲在一個叫SideTable的結(jié)構(gòu)體的refcnts成員中,refcnts是個散列表
uintptr_t has_sidetable_rc : 1;
//里面存儲的值是引用計數(shù)減1
uintptr_t extra_rc : 19;
};
};
- 可以看出64位之后,isa不僅僅用來存放地址值了,還用來存放更多的東西,所有的值都存放在bits里面。
- 冒號后面的數(shù)字就是占用多少位,加起來之后一共是64位,8個字節(jié),這也和以前我們說的isa指針占用8字節(jié)相吻合。
- 上面的shiftcls就是存放類對象、元類對象的內(nèi)存地址信息,可以發(fā)現(xiàn)有33位。
怎么取出地址值呢?
找到上面的“define ISA_MASK 0x0000000ffffffff8ULL”用計算器轉(zhuǎn)換成二進制就是“0b111111111111111111111111111111111000”,發(fā)現(xiàn)就是33個1,就是用這個ISA_MASK來取出地址值的,詳細(xì)操作請參考:isa指針和superclass指針
我們發(fā)現(xiàn),ISA_MASK最后三位都是0,這就導(dǎo)致:二進制下,類對象、元類對象的地址值打印出來最后三位一定都是0
真機上才是arm64架構(gòu),我們將項目跑在真機上,打印驗證一下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%p", [ViewController class]);
NSLog(@"%p", object_getClass([ViewController class]));
NSLog(@"%p", [MJPerson class]);
NSLog(@"%p", object_getClass([MJPerson class]));
}
打?。?/p>
0x107f6ed88
0x107f6edb0
0x107f6ee50
0x107f6ee28
上面是16進制,1位16進制相當(dāng)于4位二進制,2位16進制相當(dāng)于8位二進制,8位二進制是1個字節(jié),所以2位16進制就是1個字節(jié)。觀察上面,最后結(jié)尾不是0就是8,如果是0那末尾肯定是0b0000,如果是8那就是01000,所以驗證了上面的結(jié)論。
簡單驗證:
關(guān)于isa里面存放的其他更多信息,這里簡單驗證下:
創(chuàng)建對象,打斷點:
MJPerson *person = [[MJPerson alloc] init];
獲取對象的isa
(lldb) p/x person->isa
(Class) $0 = 0x000000010231bf40 MJPerson
將16進制轉(zhuǎn)換成二進制:

可以發(fā)現(xiàn),一共64位,從上圖的右下到左上,分別對應(yīng)結(jié)構(gòu)體中從上往下存儲的值。
比如:右下最后一位0代表現(xiàn)在是普通的指針沒有經(jīng)過優(yōu)化過(因為我沒跑真機),右下倒數(shù)第二位0表示沒有設(shè)置過關(guān)聯(lián)對象,以此類推。
Demo地址:isa-ISA_MASK