對於RunTime恐怕幾乎每一個做iOS的人都聽說過,都用過吧,但是對於其具體實現好多人應該都不太清楚吧,今天我這分4部分,詳細的講解一下Runtime,讓大家對Runtime有一個全域性的瞭解
- 1、isa解析
- 2、方法快取
- 3、objc_msgSend執行流程
- 4、RunTime的相關API
isa指標
我們在研究OC物件的時候已經知道了,實力物件的isa
指向類物件,類物件的isa
指向元類物件。其實這樣說還是有一點不對的,應該說在arm64架構
之前,isa就是一個普通的指標,儲存著Class
、 Meta-Class
物件的記憶體地址;但是從arm64
之後,對isa
進行了優化,變成了一個共用體(union)
結構,還使用位域
來存放跟多的資訊。
我們在這裡下載runtime原始碼,然後查詢struct objc_object
裡面的isa
,這裡我們只研究arm64架構isa
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
複製程式碼
我們發現isa
的結構是這種共用體(union)
結構,其實使用這種共用體是一種優化,isa
不在單獨存放的是一個指標資訊了,裡面存放了更多的其他資訊。
概念
想要明白isa
變成共用體(union)
結構,是一種優化,我們需要先了解一些概念
- 1、位運算
- 2、位元組和位
- 3、位域
- 4、共用體
位運算
位運算的運算子有下面幾個
- 1、左移:
<<
- 2、右移:
>>
- 3、按位或:
|
- 4、按位與:
&
- 5、按位取反:
~
- 6、按位異或:
^
其功能是參與運算的兩數各對應的二進位相異或,當兩對應的二進位相異時,結果為1
與操作&
與操作&
:都是1則為1,一個0就是0。可以用來取出來特定的位。例如一個二進位制0b 0000 0111
,我們分別想取出第一位1
和第四位0
。
0000 0111 0000 0111
&0000 0001 &0000 1000
-------------- --------------
0000 0001 0000 0000
複製程式碼
我們可以發現我們使用按位與&的時候,我們如果想取出哪一位,把改為設定為1,其他位設定為0就可以了。
介紹到了&
,我再來介紹一個概念,掩碼:一般用來按位與(&)運算的
,具體有什麼作用,我們下面會進行講解
或操作|
或操作|
:一個是1,則為1,全部是0才為0。
例如一個二進位制0b 0101 1010
。
0101 1010
| 0001 1100
--------------
0101 1110
複製程式碼
如果我們想要某一位,就該該位或上一個0
左移:<<
二進位制位全部左移若干位,左邊的丟棄,右邊補0
- 1、1<<0 1左移0位,0b0000 0001
- 2、1<<1 1左移1位,0b0000 0010
- 3、1<<2 1左移2位,0b0000 0100
- 4、1<<3 1左移3位,0b0000 1000
右移:>>
二進位制右移若干位,正數左邊補0,負數左邊補1,右邊丟棄。
例如 12>>2 0000 1100 = 12 0000 0011 = 2 (右移後)
特點:每右移一位,就除以一次2。a>>n 就是 a除以2的n次方
位元組和位
- Bit意為“位”,是計算機運算的基礎,屬於二進位制的範疇;
- Byte意為“位元組”,是計算機檔案大小的基本計算單位;
通常用bit來作資料傳輸的單位,因為物理層,資料鏈路層的傳輸對於使用者是透明的,而這種通訊傳輸是基於二進位制的傳輸。在應用層通常是用byte來作單位,表示檔案的大小,在使用者看來就是可見的資料大小
換算 1 Byte = 8 Bits 1 KB = 1024 Bytes 1 MB = 1024 KB 1 GB = 1024 MB 另外,Byte通常簡寫為B(大寫),而bit通常簡寫為b(小寫)。可以這麼記憶,大寫的為大單位,實際數值小,小寫的為小單位,實際數值較大,1B=8b。
位域
所謂”位域“是把一個位元組中的二進位劃分為幾 個不同的區域, 並說明每個區域的位數。每個域有一個域名,允許在程式中按域名進行操作。它實際上是C語言提供的一種資料結構。
使用位域的好處是:
- 1.有些資訊在儲存時,並不需要佔用一個完整的位元組, 而只需佔幾個或一個二進位制位。例如在存放一個開關量時,只有0和1 兩種狀態, 用一位二進位即可。這樣節省儲存空間,而且處理簡便。 這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示。
- 2.可以很方便的利用位域把一個變數給按位分解。比如只需要4個大小在0到3的隨即數,就可以只rand()一次,然後每個位域取2個二進位制位即可,省時省空間
struct 位域結構名 { 位域列表 }; 其中位域列表的形式為: 型別說明符 位域名:位域長度;
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
複製程式碼
4、共用體
union中可以定義多個成員,union的大小由最大的成員的大小決定
;
union成員共享同一塊大小的記憶體,一次只能使用其中的一個成員; 對union某一個成員賦值,會覆蓋其他成員的值(但前提是成員所佔位元組數相同,當成員所佔位元組數不同時只會覆蓋相應位元組上的值,比如對char成員賦值就不會把整個int成員覆蓋掉,因為char只佔一個位元組,而int佔四個位元組); union量的存放順序是所有成員都從低地址開始存放的。
案例
例如我們建立一個Person
類,裡面有三個Bool
屬性,tall
、rich
、handsome
。
@property (nonatomic,assign) BOOL tall;
@property (nonatomic,assign) BOOL rich;
@property (nonatomic,assign) BOOL handsome;
複製程式碼
我們知道這三個屬性佔用了3個位元組
。其實這個時候我們可以考慮到使用位域
或者共用體
的概念,使用位(Bit)的0和1來代表這三個屬性的YES NO
,那個三個屬性就只是佔用了2個位元組
位域程式碼
@interface Person()
{
// 位域
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (BOOL)isTall
{
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;
}
複製程式碼
為什麼會出現!!
,我們知道!(-1) == NO
,!
上一個存在的值是NO
,!!
兩次那麼只會出現YES 和 NO了。
共用體
其實我們觀察isa的型別,發現isa其實是使用的共用體
,
#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)
@interface Person()
{
union {
int bits;
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
} _tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= TallMask;
} else {
_tallRichHandsome.bits &= ~TallMask;
}
}
- (BOOL)isTall
{
return !!(_tallRichHandsome.bits & TallMask);
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= RichMask;
} else {
_tallRichHandsome.bits &= ~RichMask;
}
}
- (BOOL)isRich
{
return !!(_tallRichHandsome.bits & RichMask);
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
} else {
_tallRichHandsome.bits &= ~HandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHandsome.bits & HandsomeMask);
}
複製程式碼
#define TallMask (1<<0)
這是掩碼,為了方便閱讀。
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
複製程式碼
其實也僅僅是方便閱讀的作用,讓我們知道tall
、rich
、handsome
是在哪一位上,去掉並不影響程式碼。
擴充套件:位運算應用
其實我們可以看到蘋果官方文件上面有很多地方運用到了位運算
typedef NS_ENUM(NSInteger, LXDAuthorizationType)
{
LXDAuthorizationTypeNone = 0,
LXDAuthorizationTypePush = 1 << 0, ///< 推送授權
LXDAuthorizationTypeLocation = 1 << 1, ///< 定位授權
LXDAuthorizationTypeCamera = 1 << 2, ///< 相機授權
LXDAuthorizationTypePhoto = 1 << 3, ///< 相簿授權
LXDAuthorizationTypeAudio = 1 << 4, ///< 麥克風授權
LXDAuthorizationTypeContacts = 1 << 5, ///< 通訊錄授權
};
複製程式碼
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
複製程式碼
太多了,我就不一一列舉了。其實我們在有些情況下也可以參考這樣的設計。 例如
typedef enum {
OptionsOne = 1<<0, // 0b0001
OptionsTwo = 1<<1, // 0b0010
OptionsThree = 1<<2, // 0b0100
OptionsFour = 1<<3 // 0b1000
} Options
- (void)setOptions:(Options)options
{
if (options & OptionsOne) {
NSLog(@"包含了OptionsOne");
}
if (options & OptionsTwo) {
NSLog(@"包含了OptionsTwo");
}
if (options & OptionsThree) {
NSLog(@"包含了OptionsThree");
}
if (options & OptionsFour) {
NSLog(@"包含了OptionsFour");
}
}
呼叫上面方法
[self setOptions: OptionsOne | OptionsFour];
複製程式碼
總結
最後我們在看一下isa結構吧
- 1、
nonpointer
:0,代表普通的指標,儲存著Class、Meta-Class物件的記憶體地址;1,代表優化過,使用位域儲存更多的資訊 - 2、
has_assoc
:是否有設定過關聯物件,如果沒有,釋放時會更快 - 4、
shiftcls
:儲存著Class、Meta-Class物件的記憶體地址資訊 - 5、
magic
:用於在除錯時分辨物件是否未完成初始化 - 6、
weakly_referenced
:是否有被弱引用指向過,如果沒有,釋放時會更快 - 7、
deallocating
:物件是否正在釋放 - 8、
extra_rc
:裡面儲存的值是引用計數器 - 9、
has_sidetable_rc
:引用計數器是否過大無法儲存在isa中;如果為1,那麼引用計數會儲存在一個叫SideTable的類的屬性中
第三條解釋不知道為啥違反政治安全問題了,不讓寫,只能截圖了
方法快取
我們先來整體的看一下結構
- 1、class類中只要有
isa指標
、superClass
、cache方法快取
、bits具體的類資訊
- 2、
bits & FAST_DATA_MASK
指向一個新的結構體Class_rw_t
,裡面包含著methods方法列表
、properties屬性列表
、protocols協議列表
、class_ro_t類的初始化資訊
等一些類資訊
Class_rw_t
Class_rw_t
裡面的methods方法列表
、properties屬性列表
都是二維陣列,是可讀可寫的,包含類的初始內容
,分類的內容
class_ro_t
class_ro_t
裡面的baseMethodList,baseProtocols,Ivars,baseProperties是一維陣列,是只讀的,包含類的初始化內容
method_t
method_t
是對方法的封裝
struct method_t{
SEL name;//函式名
const char *types;//編碼(返回值型別,引數型別)
IMP imp;//指向函式的指標(函式地址)
}
複製程式碼
IMP
IMP代表函式的具體實現
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
複製程式碼
第一個引數是指向self的指標(如果是例項方法,則是類例項的記憶體地址;如果是類方法,則是指向元類的指標),第二個引數是方法選擇器(selector)
SEL
SEL代表方法名,一般叫做選擇器,底層結構跟char *
類似
- 可以通過
@selector()
和sel_registerName()
獲得 - 可以通過
sel_getName()
和NSStringFromSelector()
轉成字串 - 不同類中相同名字的方法,所對應的方法的選擇器是相同的
- 具體實現
typedef struct objc_selector *SEL
types
types包含了函式返回值,引數編碼的字串
結構為:返回值 引數1 引數2...引數N
iOS中提供了一個叫做@encode
的指令,可以將具體的型別表示成字串編碼
例如
// "i24@0:8i16f20"
// 0id 8SEL 16int 20float == 24
- (int)test:(int)age height:(float)height
複製程式碼
每一個方法都有兩個預設引數self
和_msg
我們可以查到id
型別為@
,SEL
型別為:
- 1、第一個引數
i
返回值 - 2、第二個引數
@
是id 型別的self
- 3、第三個引數
:
是SEL 型別的_msg
- 4、第四個引數
i
是Int age
- 5、第五個引數
f
是float height
其中載入的數字其實是跟所佔位元組有關
- 1、
24
總共佔有多少位元組 - 2、
@0
是id 型別的self
的起始位置為0 - 3、
:8
是因為id 型別的self
佔位元組為8,所以SEL 型別的_msg`的起始位置為8
方法快取
Class內部結構中有一個方法快取cache_t
,用雜湊表(雜湊表)來快取曾經呼叫過的方法,可以提高方法的查詢速度。
cache_t
結構體裡面有三個元素
-
buckets
雜湊表,是一個陣列,陣列裡面的每一個元素就是一個bucket_t
,bucket_t
裡面存放兩個_key
SEL作為key_imp
函式的記憶體地址
-
_mask
雜湊表的長度 -
_occupied
已經快取的方法數量
為什麼會用到方法快取
這張圖片是我們方法產找路徑,如果我們的一個類有多個父類,需要呼叫父類方法,他的查詢路徑為
- 1、先遍歷自己所有的方法
- 2、如果在自己類中找不到方法,則遍歷父類所有方法,沒有查詢到呼叫方法之前,一直重複該動作
如果每一次方法呼叫都是走這樣的步驟,對於
系統級方法
來說,其實還是比較消耗資源的,為了應對這個情況。出現了方法快取
,呼叫過的方法,都放在快取列表中,下次查詢方法的時候,現在快取中查詢,如果快取中查詢不到,然後在執行上面的方法查詢流程。
雜湊表結構
雜湊表的結構大概就像上面那樣,陣列的下標是通過@selector(方法名)&_mask
來求得,具體每一個陣列的元素是一個結構體,裡面包含兩個元素_imp
和@selector(方法名)作為的key
我們在上一篇文章中知道,一個值與&上一個_mask
,得出的結果一定小於等於_mask
值,而_mask
值為陣列長度-1,所以任何時候,也不會越界。
其實這就是雜湊表的演算法,也有一些其他的演算法,取餘
,一個值取餘
和&
的效果是相同的。
但是這其實是有幾個疑慮的
- 1、初始
_mask
是多少? - 初始_mask
我簡單了嘗試了一下,第一次可能給3 - 2、隨著方法的增加,方法數量超過
_mask
值了怎麼辦 - 隨著方法的增多,方法數量肯定會超過_mask
,這個時候會清空快取雜湊表,然後_mask
*2 - 3、如果兩個值
&_mask
的值相同了怎麼辦 - 如果兩個值&_mask
的值相同時,第二個&
減一,知道找到空值,如果減到0還沒有找到空位置,那就放在最大位置 - 4、在沒有存放
cach_t
的陣列位置怎麼處理- 在沒有佔用是,會在空位置的值為
NULL
- 在沒有佔用是,會在空位置的值為
原始碼檢視
我們在objc-cache.mm
檔案中查詢bucket_t * cache_t::find(cache_key_t k, id receiver)
方法。
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
複製程式碼
計算index值
mask_t begin = cache_hash(k, m);
複製程式碼
這個方式是計算下標的,我們點選進入檢視具體實現,就是@selector(方法名)&_mask
當兩個值求的下標相同時
(i = cache_next(i, m)) != begin
複製程式碼
具體實現為
arm64
和x86
實現方法不一樣
這裡有一個MJ老師
封裝的能夠檢視物件各種屬性的方法,想要使用的可以在這裡檢視
objc_msgSend執行流程
OC中的方法呼叫,其實都是轉化為objc_msgSend
函式的呼叫,objc_msgSend
的執行流程可以分為3大階段
- 1、訊息傳送
- 2、動態方法解析
- 3、訊息轉發
訊息傳送
訊息傳送流程是我們平時最經常使用的流程,其他的像動態方法解析
和訊息轉發
其實是補救措施。具體流程如下
- 1、首先判斷訊息接受者
receiver
是否為nil,如果為nil直接退出訊息傳送 - 2、如果存在訊息接受者
receiverClass
,首先在訊息接受者receiverClass
的cache
中查詢方法,如果找到方法,直接呼叫。如果找不到,往下進行 - 3、沒有在訊息接受者
receiverClass
的cache
中找到方法,則從receiverClass
的class_rw_t
中查詢方法,如果找到方法,執行方法,並把該方法快取到receiverClass
的cache
中;如果沒有找到,往下進行 - 4、沒有在
receiverClass
中找到方法,則通過superClass指標
找到superClass
,也是現在快取中查詢,如果找到,執行方法,並把該方法快取到receiverClass
的cache
中;如果沒有找到,往下進行 - 5、沒有在訊息接受者
superClass
的cache
中找到方法,則從superClass
的class_rw_t
中查詢方法,如果找到方法,執行方法,並把該方法快取到receiverClass
的cache
中;如果沒有找到,重複4、5步驟。如果找不到了superClass
了,往下進行 - 6、如果在最底層的
superClass
也找不到該方法,則要轉到動態方法解析
動態方法解析
-
開發者可以實現以下方法,來動態新增方法實現
- +resolveInstanceMethod:
- +resolveClassMethod:
-
動態解析過後,會重新走“訊息傳送”的流程,從receiverClass的cache中查詢方法這一步開始執行
我們建立一個Person
類,然後在.h
檔案中寫一個- (void)test
,但是不寫具體實現,然後呼叫。會列印出最常見的unrecognized selector sent to instance 0x100559b60
。
動態方法解析1
動態方法解析需要呼叫resolveInstanceMethod
或者resolveClassMethod
一個對應例項方法,一個對應類方法。我們這裡是例項方法使用resolveInstanceMethod
我們看一下resolveInstanceMethod
的解釋,在我們需要執行動態方法解析
的時候我們最好返回YES
。
- (void)other{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
//獲取其他方法
Method method = class_getInstanceMethod(self, @selector(other));
//動態新增test的方法
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
}
return [super resolveInstanceMethod:sel];
}
@end
複製程式碼
在class_addMethod
方法中我們需要imp
,types
,但是OC並沒有提供相關屬性,所有我們可以呼叫相關方法來獲取相關引數
動態方法解析2
這裡我們在隨便驗證一下method
的結構是不是這種
struct method_t {
SEL sel;
char *types;
IMP imp;
};
複製程式碼
我們程式碼改成這樣
struct method_t {
SEL sel;
char *types;
IMP imp;
};
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
//獲取其他方法
struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
//動態新增test的方法
class_addMethod(self, sel, method->imp, method->types);
return YES;
}
return [super resolveInstanceMethod:sel];
}
複製程式碼
動態方法解析3
其實我們還可以用C語言驗證一下,提示:C語言中函式方法就是函式的地址
void c_other(id self, SEL _cmd)
{
NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
class_addMethod(self, sel, (IMP)c_other, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
複製程式碼
訊息轉發
如果方法一個方法在訊息傳送階段
沒有找到相關方法,也沒有進行動態方法解析
,這個時候就會走到訊息轉發階段了。
- 呼叫
forwardingTargetForSelector
,返回值不為nil時,會呼叫objc_msgSend(返回值, SEL)
- 呼叫
methodSignatureForSelector
,返回值不為nil,呼叫forwardInvocation:
方法;返回值為nil時,呼叫doesNotRecognizeSelector:
方法 - 開發者可以在forwardInvocation:方法中自定義任何邏輯
- 以上方法都有物件方法、類方法2個版本(前面可以是加號+,也可以是減號-)
forwardingTargetForSelector
我們建立一個命令列專案,建立兩個類,person
和Student
,在person.h
裡面寫一個例項方法,但是不去實現相關方法。
@interface Person : NSObject
- (void)test;
@end
@interface Student : NSObject
- (void)test;
@end
#import "Student.h"
@implementation Student
- (void)test{
NSLog(@"%s",__func__);
}
@end
複製程式碼
呼叫的時候回報出我們最常見的錯誤unrecognized selector sent to instance 0x100747a50
如果我們在person
裡面實現這個方法
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(test)) {
return [[Student alloc]init];
}
return nil;
}
複製程式碼
呼叫forwardingTargetForSelector
,返回值不為nil時,會呼叫objc_msgSend(返回值, SEL)
,結果就是呼叫了objc_msgSend(Student,test)
methodSignatureForSelector(方法簽名)
當forwardingTargetForSelector
返回值為nil,或者都沒有呼叫該方法的時候,系統會呼叫methodSignatureForSelector
方法。呼叫methodSignatureForSelector
,返回值不為nil,呼叫forwardInvocation:
方法;返回值為nil時,呼叫doesNotRecognizeSelector:
方法
對於方法簽名的生成方式
- 1、
[NSMethodSignature signatureWithObjCTypes:"i@:i"]
- 2、
[[[Student alloc]init] methodSignatureForSelector:aSelector];
實現方法簽名以後我們還要實現forwardInvocation
方法,當呼叫person
的test
的方法的時候,就會走到這個方法中
NSInvocation封裝了一個方法呼叫,包括:方法呼叫者、方法名、方法引數
- anInvocation.target 方法呼叫者
- anInvocation.selector 方法名
- [anInvocation getArgument:NULL atIndex:0]
我們也可以先執行NSLog(@"========");
在執行Student的test方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"========");
anInvocation.target = [[Student alloc]init];
[anInvocation invoke];
// [anInvocation invokeWithTarget:[[Student alloc] init]];
}
複製程式碼
其中這兩個方法是一樣的
[anInvocation invokeWithTarget:[[Student alloc] init]];
anInvocation.target = [[Student alloc]init];
[anInvocation invoke];
複製程式碼
其實這個方法還是比較有用的,像網上一些對bug處理都會用到這個方法
RunTime的相關API
類方法
- 1、
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
動態建立一個類(引數:父類,類名,額外的記憶體空間) - 2、
void objc_registerClassPair(Class cls)
註冊一個類(要在類註冊之前新增成員變數) - 3、
void objc_disposeClassPair(Class cls)
銷燬一個類 - 4、
Class object_getClass(id obj)
獲取isa指向的Class - 5、
Class object_setClass(id obj, Class cls)
設定isa指向的Class - 6、
BOOL object_isClass(id obj)
判斷一個OC物件是否為Class - 7、
BOOL class_isMetaClass(Class cls)
判斷一個Class是否為元類 - 8、
Class class_getSuperclass(Class cls)
獲取父類
我在方法快取講過,在建立一個例項物件以後,裡面的成員變數就固定了,不能在修改了。因此我們在用objc_registerClassPair
註冊類的時候,我們必須把成員變數寫在註冊之前。
簡單使用,因為這裡面的都是runtime底層方法寫的,所有點語法和set方法都不可以使用,如果想要遍歷裡面的屬性和方法還是需要使用runtime
提供的方法
建立類
// 建立類
Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
class_addIvar(newClass, "_age", 4, 1, @encode(int));
class_addIvar(newClass, "_weight", 4, 1, @encode(int));
//註冊類
objc_registerClassPair(newClass);
// 成員變數的數量
unsigned int count;
Ivar *ivars = class_copyIvarList(newClass, &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成員變數
Ivar ivar = ivars[i];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);
// 在不需要這個類時釋放
objc_disposeClassPair(newClass);
複製程式碼
設定isa指向的Class
Person *p = [[Person alloc]init];
object_setClass(p, [Cat class]);
NSLog(@"%@",p);
複製程式碼
成員變數
- 1、
Ivar class_getInstanceVariable(Class cls, const char *name)
獲取一個例項變數資訊 - 2、
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
拷貝例項變數列表(最後需要呼叫free釋放) - 3、
void object_setIvar(id obj, Ivar ivar, id value)
設定成員變數的值 - 4、
id object_getIvar(id obj, Ivar ivar)
獲取成員變數的值 - 5、
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
動態新增成員變數(已經註冊的類是不能動態新增成員變數的) - 6、
const char *ivar_getName(Ivar v), const char *ivar_getTypeEncoding(Ivar v)
獲取成員變數的相關資訊
最常用的方法就是獲取類的成員變數
unsigned int count;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成員變數
Ivar ivar = ivars[i];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);
複製程式碼
常用的方案
- 1、JSON轉Model
- 2、常看寫控制元件都有哪些元素,然後進行修改
屬性
-
1、
objc_property_t class_getProperty(Class cls, const char *name)
獲取一個屬性 -
2、
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
拷貝屬性列表(最後需要呼叫free釋放) -
3、
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
動態新增屬性 -
4、
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
動態替換屬性 -
5、
const char *property_getName(objc_property_t property)
獲取屬性的一些資訊 -
6、
const char *property_getAttributes(objc_property_t property)
獲取屬性的一些資訊方法
-
1、獲得一個例項方法、類方法 -
Method class_getInstanceMethod(Class cls, SEL name)
-Method class_getClassMethod(Class cls, SEL name)
-
2、方法實現相關操作 -
IMP class_getMethodImplementation(Class cls, SEL name)
-IMP method_setImplementation(Method m, IMP imp)
-void method_exchangeImplementations(Method m1, Method m2)
-
3、拷貝方法列表(最後需要呼叫free釋放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
-
4、動態新增方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
-
5、動態替換方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
-
6、選擇器相關
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
-
7、用block作為方法實現
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)
最常見的就是動態方法交換
Method runMethod = class_getInstanceMethod([Person class], @selector(run));
Method testMethod = class_getInstanceMethod([Person class], @selector(test));
method_exchangeImplementations(runMethod, testMethod)
複製程式碼
還有一個方法替換
MJPerson *person = [[Person alloc] init];
// class_replaceMethod([Person class], @selector(run), (IMP)myrun, "v");
class_replaceMethod([Person class], @selector(run), imp_implementationWithBlock(^{
NSLog(@"123123");
}), "v");
[person run];
複製程式碼
我們經常會看一些面試題,但是好多面試題我們都是知其然不知其所以然,你如果認真的看了我上面總結的幾十篇文章,那麼你也會知其所以然。