前言學:位域和共用體
一:isa指針--runtime之前的學習
1.1:蘋果應用的按位或、按位與
二:類對象信息
2.1:類對象信息:rw_t
2.2:類對象信息:方法緩存(很關鍵)
2.2:類對象信息:查看緩存
2.3:objc_msgSend
三個階段:消息發送、動態解析、消息轉發
2.4:super
2.5:isKindOfClass AND isMemberOfClass
2.6:一個綜合底層問題:super/棧空間/消息機制/訪問成員變量的本質/
三:Runtime
3.1:類
3.2:成員變量
3.3:屬性
3.4:方法
Objective-C是一門動態性比較強的編程語言,跟C、C++等語言有着很大的不同
Objective-C的動態性是由Runtime API來支撐的
Runtime API提供的接口基本都是C語言的,源碼由C\C++\彙編語言編寫,調用頻次比較高的都是用匯編實現的
前言學:位域和共用體
先學習下c語言的基礎知識:位域和共用體,學了這個可以更加了解isa內部,瞭解了isa,才能更加了解runtime.
api中一旦帶有mask的, 有很多,這都是掩碼,利用mask取出對應的東西
現在想把這三個bool值存放在一個字節中,聲明如下
方案一:& 、| 和 左移運算符 <<
簡單解釋一下
與運算符 &
只有對應的二個二進位都爲1時,結果位纔是1
10010 & 00010 = 00010
或運算符 |
只要對應的二個二進位有一個爲1時,結果位就爲1
00010 | 10000 = 10010
左移運算符 <<
公式 x << 3 就是把x的各二進位左移3位
1<<1 實際就是 0001 << 1 = 0010 轉成十進制後就是 2
1<<4 實際就是 0001 << 4 = 10000 轉成十進制後就是 16
#import "MJPerson.h"
// &可以用來取出特定的位
// 0000 0111
//&0000 0100
//------
// 0000 0100
// 掩碼,一般用來按位與(&)運算的
//#define MJTallMask 1
//#define MJRichMask 2
//#define MJHandsomeMask 4
//#define MJTallMask 0b00000001
//#define MJRichMask 0b00000010
//#define MJHandsomeMask 0b00000100
#define MJTallMask (1<<0) // 1左移一位0 不移動 還是 1
#define MJRichMask (1<<1) // 1左移一位1 0b0000 001 左移一位 0b0000 0010 是2
#define MJHandsomeMask (1<<2) // 1左移兩位 0b0000 001 左移一位 0b0000 0100 是4
@interface MJPerson()
{
char _tallRichHansome;
}
@end
@implementation MJPerson
// 0010 1010
//&1111 1101
//----------
// 0010 1000
- (instancetype)init
{
if (self = [super init]) {
_tallRichHansome = 0b00000100;
}
return self;
}
- (void)setTall:(BOOL)tall
{
if (tall) { // 傳進來的是YES:1
_tallRichHansome |= MJTallMask; // 拿到以前的值 或 上掩碼, _tallRichHansome = _tallRichHansome | MJTallMask;
} else { // 傳進來的是NO:0 其他位是1 只有那位是0 再按位與
_tallRichHansome &= ~MJTallMask; // 按位取反
}
}
- (BOOL)isTall
{
return !!(_tallRichHansome & MJTallMask); // 加兩個! 就是強轉成對應的bool類型,因爲一個!代表取反,再加一個就是對應的bool類型
}
- (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
這樣調用
// 編寫代碼 -> 編譯鏈接 -> 運行
// Runtime:運行時
// 提供了一套C語言API
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.rich = YES;
person.tall = NO;
person.handsome = NO;
NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
// person.tall = YES;
// person.rich = YES;
// person.handsome = NO;
// NSLog(@"%zd", class_getInstanceSize([MJPerson class]));
}
return 0;
}
2018-09-14 11:03:20.098862+0800 Interview01-Runtime[3312:201940] tall:0 rich:1 hansome:0
上面已經實現了取值 並把三個原來的bool屬性存放在了一個字節中。並且取值和賦值都實現了。
詳細看圖解釋一下
- 按位或運算符:
只要你想將某一位置位1,並且其他的都不變,:我只需要找到哪一位 或上 這個掩碼就行
把對應的位 變成想要的東西。其他值不要動。按位與,這樣就把倒數第二位置位0 :拿到掩碼 進行取反,再按位與操作即可
- 按位與運算符:
取反是:~
繼續完善上面的方案 (上述維護起來比較麻煩)可讀性更好些
方案二:結構體的位域實現
位域是指信息在存儲時,並不需要佔用一個完整的字節, 而只需佔幾個或一個二進制位。例如在存放一個開關量時,只有0和1 兩種狀態, 用一位二進位即可。爲了節省存儲空間,並使處理簡便,C語言又提供了一種數據結構,稱爲“位域”或“位段”。所謂“位域”是把一個字節中的二進位劃分爲幾個不同的區域, 並說明每個區域的位數。每個域有一個域名,允許在程序中按域名進行操作。 這樣就可以把幾個不同的對象用一個字節的二進制位域來表示。
注意
-
使用位域的結構體所佔的內存空間爲結構體中佔用內存空間最多的結構體變量所佔用的內存空間大小,如:有long和int,最終結構所佔的內存空間爲long類型所佔用的內存空間(8字節)
#import "MJPerson.h"
//#define MJTallMask (1<<0)
//#define MJRichMask (1<<1)
//#define MJHandsomeMask (1<<2)
@interface MJPerson()
{
// 位域 結構體支持:位域這個結構
struct { // 所以這個整體就是一個字節,裏面有三位是下面的 先寫的位數越低,會在最右邊的一位,後寫的往前
char tall : 1; // char tall; 只是這樣的話會佔三個字節, 但是寫個:1 就代表只佔一位,
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
}
@end
@implementation MJPerson
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (BOOL)isTall
{
// 這裏用雙!:這裏賦值的是1 但是取出來_tallRichHandsome.tall的卻是-1,因爲bool類型是8位,而_tallRichHandsome.tall只有一個二進制位,講一個二進制位強制轉成8位,所以其他位置強制轉成了1,所以用雙!解決。
// 同時領一個解決方案是吧 char rich : 1; 這三個都變成2位,就是 char rich : 2;因爲rich是2位,:0b01 轉成0b0000 0001 倒數第二個是賦值的0 位域的特點。
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
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;
}
方案三:蘋果官方用的方案:依然採取位運算 +共用體
共用體把幾種不同數據類型的變量存放在同一塊內存裏。共用體中的變量共享同一塊內存。
定義共用體類型變量的一般形式:
union 共用體名
{
成員列表
}變量列表;
與結構體類似,變量列表是可選的。如果沒有變量列表,只是定義了一種共用體類型。
(0xff是255 是-1 用一個字節存 都是0xff 區別是:無符號是255 有符號就是-1)
@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;
- (void)setThin:(BOOL)thin;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
- (BOOL)isThin;
@end
#import "MJPerson.h"
#define MJTallMask (1<<0)
#define MJRichMask (1<<1)
#define MJHandsomeMask (1<<2)
#define MJThinMask (1<<3)
/*存取繼續使用位運算, 使用位域來增加可讀性 存放的位置依舊由上面的mask來決定的*/
// 結構體可以擁有很多成員,並且獨立存在。比方說tall 和rich、handsome、thin,每個都佔1個字節
@interface MJPerson()
{
union { // 共用體 :大家共用一塊內存,bits和裏面的結構體 最大佔用是1個字節(裏面的結構體佔一個字節),所以共用1個字節。
char bits;
struct { // 這樣的結構僅僅是爲了可讀性,而不影響運算 :因爲下面我取的是bits 而不是這個裏面的結構體。
char tall : 1;
char rich : 1;
char handsome : 1;
char thin : 1;
};
} _tallRichHandsome;
// union { // 這樣跟以前講的char 字節是沒有什麼區別的。
// char bits;
// } _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
// 如果是4位 掩碼就要變
union {
int bits;
struct {
char tall : 4; // 掩碼 就是 4位都要是4
char rich : 4;
char handsome : 4;
char thin : 4;
};
} _tallRichHandsome;
#define MJTallMask (0b1111<<0) //0b11110000 掩碼就不能按照之前那麼寫
一:isa指針--runtime之前的學習
要想學習Runtime,首先要了解它底層的一些常用數據結構,比如isa指針
在arm64架構之前,isa就是一個普通的指針,存儲着Class、Meta-Class對象的內存地址
從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息
首先在源碼中找到isa指針,看一下isa指針的本質
// 截取objc_object內部分代碼
struct objc_object {
private:
isa_t isa;
}
isa指針其實是一個isa_t類型的共用體,來到isa_t內部查看其結構
// 精簡過的isa_t共用體
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits; // 只需要關注這個即可
#if SUPPORT_PACKED_ISA
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
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)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
# else
# error unknown architecture for packed isa
# endif
#endif
上面精簡一下 就是下面這個
位域詳解
shiftcls 這個就是類和原類的值
這33個1就是取類對象和原類對象的值。
類對象和原類對象 最後三位 一定是0,因爲&的值,在源碼中那個值右邊空出了三位,最後三位需加上這000
一個16進制位代表4個二進制位看打印結果最後一個是0就是代表4個二進制的0,8:0x1000.
在arm64之前,isa就是一個普通的指針,裏面存儲着類對象和原類對象的地址值。從arm64開始,isa經過了優化,它採用共用體的結構,將一個64位的內存數據分來存儲很多東西,其中33位存儲地址值。
1.1:蘋果應用的按位或、按位與
我們在oc項目中經常會用到按位或,看下面例子
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self addObserver:self forKeyPath:@"age" options:options context:NULL];
看UIViewAutoresizing源碼是
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
};
那這個設計原理是什麼呢?下面模擬
#import "ViewController.h"
//typedef enum {
// MJOptionsOne = 1, // 0b0001
// MJOptionsTwo = 2, // 0b0010
// MJOptionsThree = 4, // 0b0100
// MJOptionsFour = 8 // 0b1000
//} MJOptions;
typedef enum {
// MJOptionsNone = 0, // 0b0000
MJOptionsOne = 1<<0, // 0b0001 2的零次方
MJOptionsTwo = 1<<1, // 0b0010 2的一次方
MJOptionsThree = 1<<2, // 0b0100 2的二次方
MJOptionsFour = 1<<3 // 0b1000 2的三次方
} MJOptions;
@interface ViewController ()
@end
@implementation ViewController
/*
0b0001
0b0010
0b1000
------ 上面是所有傳進來的值 或在一起的結果是下面這個
|0b1011
&0b0100 這個是看看傳進來的值都有哪些,隨便按位與上上面的任意值 選的是第三個
-------
0b0000 得到的結果,都是零,那就是沒有傳這個第三個值。
*/
- (void)setOptions:(MJOptions)options
{
if (options & MJOptionsOne) {
NSLog(@"包含了MJOptionsOne");
}
if (options & MJOptionsTwo) {
NSLog(@"包含了MJOptionsTwo");
}
if (options & MJOptionsThree) {
NSLog(@"包含了MJOptionsThree");
}
if (options & MJOptionsFour) {
NSLog(@"包含了MJOptionsFour");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setOptions: MJOptionsOne | MJOptionsFour]; 這種情況下跟下面相加的是一致的,但是這個值是有技巧的都是2的次冪。
// [self setOptions: MJOptionsOne + MJOptionsTwo + MJOptionsFour];
}
@end
設置一個枚舉,枚舉內部值都是設定好的值,在外部傳入的時候 都是按位或,某些方面上可以相加,得到最終一個綜合的傳入的枚舉值,然後拿到這個枚舉綜合值後需要在內部判斷前面傳入了那些值,所以再按位與上值判斷傳入的是否有某個值,就知道傳入的是什麼值了。
二:類對象信息
類對象內部結構和元類對象內部結構一致,只是裏面的數據亦一樣,元類對象是一種特殊的類對象,裏面放的東西比較少,只有類方法。
bits 與上mask得到tw_t結構,rw是r:可讀,w:可寫,加起來就是可讀寫的意思。ro:只讀的意思。
2.1:類對象信息:rw_t
我們先看這個方法列表:
其實這個methods事一個二維數組列表。那我們可以看到ro_t中也有一份結構,也有一個列表,但是thehodList結構確實一維的。
原本RO_T 類的所有的方法 成員變量 協議等信息 一開始都是放在ro_t裏面,當程序運行起來,會將分類和原來的類合併在一起的時候,就會將ro_t裏面的東西 跟分類的東西合併起來 放在rw_t中,相當於有一部分tw_t中的東西是從ro_t中來的。從分類的源碼中可以看到,在合併分類的時候rw的內存是重新分配的,可以看分類合併的源碼
/***********************************************************************
* realizeClass
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClass(Class cls)
{
runtimeLock.assertWriting();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro; // 把rw指向原來的ro !!
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw); // 這裏把原來的 cls指向重新分配內存的rw,!!
}
isMeta = ro->flags & RO_META;
rw->version = isMeta ? 7 : 0; // old runtime went up to 6
// Choose an index for this class.
// Sets cls->instancesRequireRawIsa if indexes no more indexes are available
cls->chooseClassArrayIndex();
if (PrintConnecting) {
_objc_inform("CLASS: realizing class '%s'%s %p %p #%u",
cls->nameForLogging(), isMeta ? " (meta)" : "",
(void*)cls, ro, cls->classArrayIndex());
}
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()));
#if SUPPORT_NONPOINTER_ISA
// Disable non-pointer isa for some classes and/or platforms.
// Set instancesRequireRawIsa.
bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;
if (DisableNonpointerIsa) {
// Non-pointer isa disabled by environment or app SDK version
instancesRequireRawIsa = true;
}
else if (!hackedDispatch && !(ro->flags & RO_META) &&
0 == strcmp(ro->name, "OS_object"))
{
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
else if (supercls && supercls->superclass &&
supercls->instancesRequireRawIsa())
{
// This is also propagated by addSubclass()
// but nonpointer isa setup needs it earlier.
// Special case: instancesRequireRawIsa does not propagate
// from root class to root metaclass
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
}
if (instancesRequireRawIsa) {
cls->setInstancesRequireRawIsa(rawIsaIsInherited);
}
// SUPPORT_NONPOINTER_ISA
#endif
// Update superclass and metaclass in case of remapping
cls->superclass = supercls;
cls->initClassIsa(metacls);
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}
// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// Attach categories
methodizeClass(cls);
return cls;
}
看上文中摘出來的部分 是下面,做了解釋
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro; // 把rw指向原來的ro !!
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw); // 這裏把原來的 cls指向重新分配內存的rw,!!
}
2.2:method_t
這個結構存在於列表信息中,讓我們看一下組成結構
struct method_t {
SEL name; // 選擇器 方法名 @selector 或者sel_registerName獲得
const char *types; // 編碼:返回值類型、參數類型
IMP imp; // 指向函數的指針 指針類型 存放函數地址
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
這裏需要介紹一下範圍值類型和參數,看一個Person返回的方法返回值
v16@0:8
v:void
@:id類型
: seletor類型
i: 返回值類型 int類型
@:是第一個參數是id類型 就是消息接受者
:seletor類型 _cmd 方法名 第二個參數,
i: 第三個參數了 int類型 age
f: 第四個參數,height
最前面的24:代表所有參數佔的字節數.
id 和SEL都是指針 加起來是16個字節,int 和float都是4 加起來是8 所以就是16+ 8 = 24個字節
@0 :@代表第一個參數,這個0代表這個參數從哪裏開始,第一個參數是從0這個位置開始的。也就是0id
:8 :代表第二個參數,說明這個參數從第8個字節開始的參數。
i16 : i:代表int,int前面已經是16個字節了,從16個字節開始的參數。
f20: f:float 表示從第20個字節開始的參數。
這種技術是爲了配合運行時的一種技術,把一個方法的返回值類型和字符類型轉換成字符串的形式儲存起來。
要了解 上面 我們需要知道Type Encoding
2.2:類對象信息:方法緩存(很關鍵)
我們可以知道,一個class結構中有這個結構cache_t cache;
struct objc_class : objc_object {
Class isa;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
Class內部結構中有個方法緩存(cache_t),用散列表(哈希表)來緩存曾經調用過的方法,可以提高方法的查找速度
那我們需要先知道一下正常的沒有cache的方法訪問流程。這個流程不清楚的點這裏
通過isa先找到類,在類中找到方法列表,因爲這是一個數組,所以需要遍歷尋找,如果類中沒有,需要接着通過superclass找到父類的方法列表遍歷尋找,如果沒找到,就一直這樣找,直到NSObject,如果還沒找到就消息轉發,最後報錯。
但是我們cache的作用就在這,當調用過一次的方法,就會放在這個類對象的chache裏面,當下次再調用這個方法的時候,它會直接從cache裏面尋找而不是直接去遍歷查找。這樣就大大縮減了尋找速度(即使這個方法第一次是從基類中查找到的)當然從基類中找到了(先找基類緩存再找方法列表) 也會把這個方法緩存到自己類對象的緩存中(一直是這個流程)。
那我看看一下cache的內部結構 這個散列表 底層結構就是一個數組
那這個尋找過程是什麼樣的呢?
當isa找到cache_t這個結構體,再找buckets這個數組中的bucket_t(這個查找過程是下面的方式,通過哈希算法),然後比較外部調用的key跟bucket_t裏面的key是否一致,如果一致,就證明是一個,然後直接調用這個_imp,也就是這個函數的地址。
補充知識:
散列表知識結構:空間換時間
散列表跟普通遍歷不同 這是利用一套算法,哈希算法(不同人寫的散列表算法是不同的 有些是求餘)。這個詳細解讀請看這裏
這種做法是 犧牲一些內存空間來提高效率,以空間換時間
這個運行過程簡單的說就是,剛開始第一次存放的時候就以這個方法名&mask(散列表長度-1)得到的值就是存放的位置。下次尋找的時候就依舊&mask得到這個值然後找到這個值的位置直接取出來。效率大大的高。
如果mask的值的結果都是一樣的呢
會將得到的值減去1 如果沒有值就存進去,如果還一樣,就繼續減 如果是0 就變成mask(也就是數組長度-1)直到找到有空間爲止 看源碼
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
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);
}
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
mask是變化的,數組長度不夠了,就會擴容,會講緩存清掉,會從頭來一遍。看源碼
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) { // 看這裏free舊的
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
一開始的長度是一定的,不夠了再擴充。
2.2:類對象信息:查看緩存
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
IMP imp(SEL selector)
{
mask_t begin = _mask & (long long)selector;
mask_t i = begin;
do {
if (_buckets[i]._key == 0 || _buckets[i]._key == (long long)selector) {
return _buckets[i]._imp;
}
} while ((i = cache_next(i, _mask)) != begin);
return NULL;
}
};
上面是自定義的MJClassInfo類中的關於imp的寫法,這個跟源碼內部的寫法一致,現在就直接可以取出來,並且取出來的值都是對的
2.3:objc_msgSend
MJGoodStudent *gs = [[MJGoodStudent alloc] init];
[gs personTest];
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("personTest"));
sel_registerName("personTest") = @selector(personTest);
@selector的本質可以說是轉成了sel_registerName 這個c語言函數
// OC的方法調用:消息機制,給方法調用者發送消息
[person personTest];
// objc_msgSend(person, @selector(personTest));
// 消息接收者(receiver):person
// 消息名稱:personTest
objc_msgSend執行流程
OC中的方法調用,其實都是轉換爲objc_msgSend函數的調用
objc_msgSend的執行流程可以分爲3大階段
a:消息發送((找遍所有的類 父類 自己類)如果發送成功,就不會進入動態方法解析階段)
b:動態方法解析(如果消息發送階段沒有找到合適的方法進行調用,允許我們開發者動態創建一個自己的類,到動態解析階段)
c:消息轉發(如果動態方法解析階段,沒有任何的動態調用,就會轉成別的調用。)(如果三個階段都沒有找到則報一個錯誤:unrecognized selector sent to instance)
源碼跟讀
接着詳細解讀三個階段
階段一:begin=消息發送=================================
.macro ENTRY /* name */
.text
.align 5
.globl $0
$0:
.macro:宏定義
看一下objc_msgsend的內部實現:這是彙編語言
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
// x0寄存器:消息接受者 receiver:相當於傳進去的第一個參數 先判斷消息接受者是否<=0 一旦<=0 一旦是nil就會跳轉到LNilOrTagged:
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached 緩存查找
LNilOrTagged:
b.eq LReturnZero // nil check 返回0
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret // 相當於return
END_ENTRY _objc_msgSend
CacheLookup是一個宏 可以看到內部_cmd & mask:查緩存
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp CacheHit:命中,查找到,直接調用
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0 沒有命中
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
查看宏CheckMiss
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL // 照這個因爲剛開始傳進去的就是mormal
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
入口:STATIC_ENTRY 這個__objc_msgSend_uncached 會調用MethodTableLookup
.endmacro
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup
br x17
END_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
彙編中已經找不到__class_lookupMethodAndLoadCache3了,那從c語言中找需要去掉一個下劃線_class_lookupMethodAndLoadCache3
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.read();
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertReading();
// Try this class's cache. 剛纔在彙編中找過,現在又找一遍,怕動態添加後,
// 第一階段:消息發送階段
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel); // 搜索方法
if (meth) { // 方法存在
log_and_fill_cache(cls, meth->imp, sel, inst, cls); // 填充緩存
imp = meth->imp;
goto done; // 返回彙編中
}
} // 在自己的方法列表中沒有找到方法
// Try superclass caches and method lists. 去父類的緩存和方法中查找
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel); // 去父類的緩存中找
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
// 填充到這個本類的class中,this class,而不是父類。
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list. 父類緩存中沒有 從方法中找
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
// 第二階段:動態解析階段 沒有經歷過動態解析,纔會進行
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES; // 標誌爲已經解析了
goto retry; // 返回上面第一階段,繼續消息發送
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 第三階段:消息轉發階段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp; // 這裏返回返回到上一個函數 再返回就是返回到彙編
}
查找getMethodNoSuper_nolock
類對象調用data方法返回的就是rw_t 也即是拿的是rw_t中的methods
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist); // 排好序的方法列表 二分查找
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) { // for循環查找
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
// 這是二分查找的源碼
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// 二分查找 先拿出最中間的值比較 看大小,再往後還是往前找
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
現在形象化的解釋一下
階段二:begin=動態解析=================================
如果當初編寫代碼的時候沒有那個方法,可以在程序運行的時候添加一個方法來實現。
// 這裏接上面 lookUpImpOrForward內部 前面的方法沒有找到,這裏就進行到動態解析階段 解析過一遍,下一次就不會動態解析了
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry; // 這裏找到之後又會回到第一個階段,因爲已經動態添加了一個方法
}
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 這裏會根據你是類還是元類 分別調用方法
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // 讓類調用這個方法
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
動態方法解析階段 會調用這兩個方法
//+ (BOOL)resolveClassMethod:(SEL)sel
//+ (BOOL)resolveInstanceMethod:(SEL)sel
這個對類和元類的流程是一樣的。只有調用方法不一樣,
對於類調用的是resolveInstanceMethod
對於元類 調用的是resolveClassMethod
好,接着用圖形象化表示一下
現在看一下oc代碼
@interface MJPerson : NSObject
- (void)test;
//+ (void)test;
@end
#import "MJPerson.h"
#import <objc/runtime.h>
@implementation MJPerson
void c_other(id self, SEL _cmd)
{
// 來到c語言
NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
// 這是元類調用的方式
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
// if (sel == @selector(test)) {
// // 第一個參數是object_getClass(self)
// class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
// return YES;
// }
// return [super resolveClassMethod:sel];
//}
// 類調用第三種方式:c語言寫作方式
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 動態添加test方法的實現
class_addMethod(self, sel, (IMP)c_other, "v16@0:8");
// 返回YES代表有動態添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)other
{
NSLog(@"%s", __func__);
}
// 類調用第二種方式,通過oc的正式獲取的方式
//+ (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));
//
// // 返回YES代表有動態添加方法
// return YES;
// }
// return [super resolveInstanceMethod:sel];
//}
// typedef struct objc_method *Method; 一個Method代表一個方法
// struct objc_method == struct method_t
// struct method_t *otherMethod = (struct method_t *)class_getInstanceMethod(self, @selector(other));
//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);
//
// // 返回YES代表有動態添加方法
// return YES;
// }
// return [super resolveInstanceMethod:sel];
//}
@end
調用
MJPerson *person = [[MJPerson alloc] init];
[person test];
動態的方法添加到了rw_t的 method_list_t *methods;中 。在goty retry中會添加到緩存中;
應用場景:並不是很常用。可以在用dynamic的時候用。在編譯的時候不生成實現,在運行的時候添加
#import "MJPerson.h"
#import <objc/runtime.h>
@implementation MJPerson
// 提醒編譯器不要自動生成setter和getter的實現、不要自動生成成員變量
@dynamic age;
void setAge(id self, SEL _cmd, int age)
{
NSLog(@"age is %d", age);
}
int age(id self, SEL _cmd)
{
return 120;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(setAge:)) {
class_addMethod(self, sel, (IMP)setAge, "v@:i");
return YES;
} else if (sel == @selector(age)) {
class_addMethod(self, sel, (IMP)age, "i@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//@synthesize age = _age, height = _height;
//- (void)setAge:(int)age
//{
// _age = age;
//}
//
//- (int)age
//{
// return _age;
//}
@end
階段三:begin=消息轉發=================================
消息轉發:將消息轉發給別人。
// 消息轉發階段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
查找_objc_msgForward_impcache這個方法,會發現這個方法在彙編中但是裏面會找不到最終的代碼實現,有國外寫的僞代碼
但是都會調用__forwarding
看一下僞代碼
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 調用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
}
}
if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
// The point of no return.
kill(getpid(), 9);
}
看一下oc代碼,調用邏輯
#import "MJCat.h"
@implementation MJCat
- (void)test
{
NSLog(@"%s", __func__);
}
@end
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
- (void)test;
@end
#import "MJPerson.h"
#import <objc/runtime.h>
#import "MJCat.h"
@implementation MJPerson
//+ (BOOL)resolveInstanceMethod:(SEL)sel
//{
// class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
//}
// 如果沒有實現這個方法 就相當於返回空 就會調用簽名方法methodSignatureForSelector
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
// objc_msgSend([[MJCat alloc] init], aSelector)
return [[MJCat alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
// 方法簽名:返回值類型、參數類型 如果這個返回的是有值的,則就會調用forwardInvocation方法 會把返回結果封裝到NSInvocation中
// 這兩個方法特別之處是:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
// NSInvocation封裝了一個方法調用,包括:方法調用者、方法名、方法參數
// anInvocation.target 方法調用者
// anInvocation.selector 方法名
// [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 一種方式
// anInvocation.target = [[MJCat alloc] init]; // 將來方法調用者就是cat了,方法名和參數不需要給,因爲掐面已經告訴它了
// [anInvocation invoke];
// 第二種方式
[anInvocation invokeWithTarget:[[MJCat alloc] init]];
}
@end
調用
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
[person test];
}
return 0;
}
那有問題會問,爲什麼forwardingTargetForSelector 也可以直接返回對象,爲啥還要簽名?因爲如果一旦返回到簽名這裏,權限就要比forwardingTargetForSelector大的多,可以想做什麼就做什麼。因爲在forwardingTargetForSelector方法裏需要返回對象,然後有這個對象的方法實現。但是在簽名後的forwardInvocation方法中,卻不用,想不做都可以,可以設置一個新的對象,可以不設置對象直接拿參數,從NSInvocation 新包裝的類中取出來,同樣還可以什麼都不做。
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
- (int)test:(int)age;
@end
#import "MJPerson.h"
#import <objc/runtime.h>
#import "MJCat.h"
@implementation MJPerson
// 如果這裏返回爲nil 則這裏就不想調用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test:)) {
// return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
// return [[[MJCat alloc] init] methodSignatureForSelector:aSelector]; // 前提是這個對象有這個方法
}
return [super methodSignatureForSelector:aSelector];
}
// 盡情的來這裏處理 權限很大 即使不做任何事情都可以 可以添加對象 也可以不用
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 0 1 2
// 參數順序:receiver、selector、other arguments
// 1:沒有傳對象,直接取參數
int age;
[anInvocation getArgument:&age atIndex:2];
NSLog(@"%d", age + 10);
// anInvocation.target == [[MJCat alloc] init]
// anInvocation.selector == test:
// anInvocation的參數:15
// [[[MJCat alloc] init] test:15]
// 2:新對象,直接取參數
// [anInvocation invokeWithTarget:[[MJCat alloc] init]];
//
// int ret;
// [anInvocation getReturnValue:&ret];
//
// NSLog(@"%d", ret);
// 3:這個作用域裏什麼代碼都不寫
}
@end
調用
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
[person test:15];
}
return 0;
}
現在講 類的消息轉發
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
+ (void)test;
@end
#import "MJPerson.h"
#import <objc/runtime.h>
#import "MJCat.h"
@implementation MJPerson
// 動態解析階段
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
//
//}
// 類方法的消息轉發是存在的 但是直接打 沒有提示而已
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
// 一旦不返回空 這裏裏面調用的就是objc_msgSend
// objc_msgSend([[MJCat alloc] init], @selector(test))
// [[[MJCat alloc] init] test]
// if (aSelector == @selector(test)) return [[MJCat alloc] init]; // 這個是可以的,因爲內部調用的是objc_msgSend
if (aSelector == @selector(test)) return [MJCat class];
return [super forwardingTargetForSelector:aSelector];
}
//+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
//{
// if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
//
// return [super methodSignatureForSelector:aSelector];
//}
//
//+ (void)forwardInvocation:(NSInvocation *)anInvocation
//{
// NSLog(@"1123");
//}
@end
// 元類對象是一種特殊的類對象
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MJPerson test];
}
return 0;
}
消息轉發用途:解決找不到問題的錯誤
通過perform傳過來的方法,傳過來的東西不確定,降低崩潰率,思路是
#import "MJPerson.h"
@implementation MJPerson
- (void)run
{
NSLog(@"run-123");
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
// 本來能調用的方法
if ([self respondsToSelector:aSelector]) {
return [super methodSignatureForSelector:aSelector];
}
// 找不到的方法
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
// 找不到的方法,都會來到這裏
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 可以再這裏收集一下找不到方法的記錄
NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
}
@end
// NSProxy :專門用來做消息轉發的類
補充一個小知識點
@dynamic
@dynamic是告訴編譯器不用自動生成getter和setter的實現,等到運行時再添加方法實現
一般來說@prperty會自動幫我們生成set和get方法,這個自動生成是因爲@synehesize這個關鍵字
@synehesize age = _age; 爲age這個屬性 生成一個_age的成員變量 並且自動生成set方法 並且賦值 自動生成get方法並返回,
@synehesize age;只寫age, 那成員變量名就叫age。
@dynamic age;沒有set和get方法的實現了,但是不影響外面的聲明,因爲你依舊可以調用。
這個時候就可以用消息動態解析 的過程了了。
2.4:super
對於這個經常有一個面試題就是這樣的
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"[self class] = %@", [self class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"--------------------------------");
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[super superclass] = %@", [super superclass]);
}
return self;
}
上面打印的結果分別是什麼呢?結果是
2018-09-20 13:48:20.964936+0800 Interview05-super[14679:292783] [self class] = MJStudent
2018-09-20 13:48:20.965191+0800 Interview05-super[14679:292783] [self superclass] = MJPerson
2018-09-20 13:48:20.965203+0800 Interview05-super[14679:292783] --------------------------------
2018-09-20 13:48:20.965215+0800 Interview05-super[14679:292783] [super class] = MJStudent
2018-09-20 13:48:20.965226+0800 Interview05-super[14679:292783] [super superclass] = MJPerson
那對於[super class] 爲什麼結果也是student呢?爲啥不是父類person呢?爲啥 [super superclass]結果是person呢
我們來看super的內部結構,先看下面這個代碼
- (void)run
{
[super run];
[super class];
}
源碼是
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
static void _I_MJStudent_run(MJStudent * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))}, sel_registerName("run"));
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)(
(__rw_objc_super){
(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))},
sel_registerName("class"));
}
整理一下就是下面這個樣子
objc_msgSendSuper(
(__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("MJStudent")) // class_getSuperclass 拿到student的父類
},
sel_registerName("run")); // 簡化就是下面的形式
struct objc_super arg = {self, [MJPerson class]};
objc_msgSendSuper(arg, @selector(run));
可以看到objc_msgSendSuper裏面有兩個值,一個是__rw_objc_super類型的結構體,一個是selector,結構體一個是消息接受者,一個是接受者的父類
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父類
};
再從objc源碼中查找objc_msgSendSuper是啥意思 只有一段英語註釋
/**
* Sends a message with a simple return value to the superclass of an instance of a class.
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
* @return The return value of the method identified by \e op.
* @see objc_msgSend
*/
從這段註釋中可以得知,第一個就是消息接受者,第二個就是父類,作用是啥呢,就是start searching for the method implementation.也就是從哪裏開始搜索這個方法。
所以,當調用[super run];這個方法的時候,首先會把自己和自己的父類封裝在objc_super結構體中,然後跟方法selector一起放入objc_msgSendSuper中,查找的過程就是,super調用的receiver仍然是MJStudent對象,但是查找的時候是從父類開始查找,並不是student查找。如果是正常的方法調用,是從自己的方法中開始找,而super的話是從父類中開始找。
so,緊接着說class。
首先我們需要知道,class這個類的實現是
@implementation NSObject
- (Class)class
{
return object_getClass(self);
}
- (Class)superclass
{
return class_getSuperclass(object_getClass(self));
}
@end
所以返回值取決於self是誰,也就是方法調用者,也就是消息接受着:receiver。 很明顯,你傳入的是誰,這個調用者就是誰,返回的根據這個傳入者返回。所以當[super class]的時候其實就是發送了一個這樣的消息
objc_msgSendSuper({self, [MJPerson class]}, @selector(class));
而這個消息的接收者是self,但是會先從person中查找class,因爲是從父類查找,但是父類沒有class,一直找到了NSObject的class,這個class返回的是object_getClass(self);也就是消息接收者self,也就是student,所以這得到的結果是MJStudent。
而[super superclass],這個也是從person中查找class,接收者是student,也就是消息接收者是student,所以傳進去的是student,但是superclass返回的是父類,所以student的父類就是person。so,結果是MJPerson。
so,總結一下就是
[super message] 的底層實現
1.消息接收者仍然是子類對象
2.從父類開始查找方法的實現
super的本質
之前我們說super內部調用的是objc_msgSendSuper,但其實真正的不是這樣的,真正轉成的底層代碼是別的東西,真正生成的跟我們之前看到的c++代碼是有點差異的。我們可以在super處打個斷點,然後Debug -> Debug Workingflow -> Aways show Disassembly,開始看彙編代碼,會發現真正調用的是這個objc_msgSendSuper2
super調用,底層會轉換爲objc_msgSendSuper2函數的調用,接收2個參數
struct objc_super2
SEL
receiver是消息接收者
current_class是receiver的Class對象
這裏會發現傳進去的發生了變化,current_class是當前類對象,實際上函數內部邏輯也發生了變化,當拿到這個結構體的時候,會調用第二個成員的superclass,找到父類,從它父類身上開始搜索方法。
看底層彙編源碼
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass 這裏就是superclass
CacheLookup NORMAL
END_ENTRY _objc_msgSendSuper2
從另外一個角度證明是objc_msgSendSuper2 :看結構體中放的是什麼
可以根據下面cls跳過16個字節找到那個存儲的是啥即可(這裏看不懂的請看下面這個題即可)。
所以這裏調用的是objc_msgSendSuper2 因爲第二個傳進去的是當前控制器的類型 而不是UIViewcontroller。
所以要知道這個c++代碼大部分是一樣的,但是某些細節性的東西是不一樣的,是有些詫異的,但是還是可以做參考的
還可以通過另外一種方式來查看彙編代碼
將person.m文件轉成彙編代碼(不用將程序跑起來): Product -> Perform Action -> Assemble "MJPerson.m"
其實
LLVM 編譯器 (中間代碼是這個編譯器特有的) 中間代碼是通用的,希望跨平臺,不用指定平臺 但是語法又是全新的一種
OC -> 中間代碼(.ll) -> 彙編、機器代碼
LLVM的中間代碼(IR)
Objective-C在變爲機器代碼之前,會被LLVM編譯器轉換爲中間代碼(Intermediate Representation)
可以使用以下命令行指令生成中間代碼
clang -emit-llvm -S main.m
語法簡介
@ - 全局變量
% - 局部變量
alloca - 在當前執行的函數的堆棧幀中分配內存,當該函數返回到其調用者時,將自動釋放內存
i32 - 32位4字節的整數
align - 對齊
load - 讀出,store 寫入
icmp - 兩個整數值比較,返回布爾值
br - 選擇分支,根據條件來轉向label,不根據條件跳轉的話類似 goto
label - 代碼標籤
call - 調用函數
具體可以參考官方文檔:https://llvm.org/docs/LangRef.html
2.5:isKindOfClass AND isMemberOfClass
這兩個用途,是比較是否是這個類 或者這個類的子類,看應用
int main(int argc, const char * argv[]) {
@autoreleasepool {
id person = [[MJPerson alloc] init];
// 是否剛好等於右邊的類型
NSLog(@"%d", [person isMemberOfClass:[MJPerson class]]);
NSLog(@"%d", [person isMemberOfClass:[NSObject class]]);
// 左邊是否是有右邊的類或者子類
// 實例對象的類對象是類對象
NSLog(@"%d", [person isKindOfClass:[MJPerson class]]); // 1
NSLog(@"%d", [person isKindOfClass:[NSObject class]]); // 1
// 類對象的類對象是元類對象
NSLog(@"%d", [[MJPerson class] isMemberOfClass:object_getClass([MJPerson class])]); // 1
NSLog(@"%d", [MJPerson isKindOfClass:object_getClass([NSObject class])]); // 1
NSLog(@"%d", [MJPerson isKindOfClass:[NSObject class]]);// 1 這個比較特殊 因爲一直到基類的元類對象,基類的元類對象的superclass指向基類的類對象,所以nsobject==nsobject
}
return 0;
}
上面已經給出答案了,看這個需要看源碼
@implementation NSObject
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
@end
可以看到,對於
實例方法isMemberOfClass:比較的是自己的類對象,所以這個需要傳入的就是自己的類對象[Person class]
實例方法isKindOfClass:比較的是不是自己的類或者自己的子類,因爲這有一個for循環,一直尋找父類然後比較,需要傳入的也是類對象[Person class]
類方法isMemberOfClass:比較的是自己的元類對象,所以這個需要傳入的就是自己的元類對象object_getClass([MJPerson class])
類方法isKindOfClass:比較的是不是自己的原類對象或者原類對象的子類,所以這個需要傳入的也是自己的元類對象object_getClass([MJPerson class])
這個裏面需要注意一點就是[MJPerson isKindOfClass:[NSObject class]] 這個不需要傳元類對象都可以,這個比較特殊 因爲一直到基類的元類對象,基類的元類對象的superclass指向基類的類對象,所以nsobject==nsobject。
so
// 這句代碼的方法調用者不管是哪個類(只要是NSObject體系下的),都返回YES
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0
NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0
總結
實例對象:是拿類對象來比較
類對象:是拿元類對象來比較
2.6:一個綜合底層問題:super/棧空間/消息機制/訪問成員變量的本質/
@interface MJPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)print;
@end
#import "MJPerson.h"
@implementation MJPerson
- (void)print
{
NSLog(@"my name is %@", self->_name); // 這個self這個指針找到實例對象的內存,在尋找的就是MJPerson_IMP之後的isa(8個字節)之後的8個字節也就是_name。
}
@end
調用
- (void)viewDidLoad {
[super viewDidLoad];
NSString *test = @"123";
// NSObject *obj2 = [[NSObject alloc] init];
//
id cls = [MJPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
這個結果是啥呢,是123
2018-09-20 19:57:08.796869+0800 Interview02-super[25924:719905] my name is 123
那爲啥是這樣呢,如果我們把test注掉,結果又發生了變化,變成
2018-09-20 20:35:03.636348+0800 Interview02-super[26220:792569] my name is <ViewController: 0x7fe44151f0e0>
那爲啥是這樣呢?
先看一個小知識
// 局部變量分配在棧空間
// 棧空間分配,從高地址到低地址 連續分配
void test()
{
long long a = 4; // 0x7ffee638bff8
long long b = 5; // 0x7ffee638bff0
long long c = 6; // 0x7ffee638bfe8
long long d = 7; // 0x7ffee638bfe0
NSLog(@"%p %p %p %p", &a, &b, &c, &d);
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
會發現在棧上分配的局部變量 都是連續的,從高地址,到低地址,這個非常重要。接着分析
看這個,這兩條路線是一致的,下面只是我們普通的指針路線,也是尋找路線,上面是這個題目裏給出的obj指向cls的地址,而這個cls存儲的是Person的地址。所以,理論上這兩條線是一樣的,唯一的不一樣就是cls這裏沒有另外的變量,而isa這裏是有_name這個變量的。
那現在我們需要知道一個很重要的知識,就是:
正常person尋找name的原理是:self.name:
這個self這個指針找到實例對象的內存,在尋找的就是跳過MJPerson_IMP的isa(8個字節)找到之後的8個字節也就是_name。
這個很重要是因爲這個解釋了爲啥打印出來時test的123.
請看下面
這個test cls obj三個局部變量都在棧空間 test這個局部變量內存地址最大 而且順序排列 所以找到的cls後面的8個字節 所以打印出來的就是123。
但是爲啥不寫變量之後,會有打印:Viewcontroller呢?
看下圖
因爲在cls之前調用了[super viewDidLoad];這個很關鍵啊,因爲super內部實現會發現,它實現的方法是這樣的
struct abc = {
self,
[ViewController class]
};
objc_msgSendSuper2(abc, sel_registerName("viewDidLoad"));
相當於這個時候定義了一個abc的結構體變量,這個變量有兩個成員,self和[viewcontroller class],注意,這裏是結構體,而不是結構體指針。所以這裏成員變量的內存佔用情況如上圖所示,所以obj指向name的時候就會跳過cls這個8個字節,找下8個字節也就是self了,也就是UIViewcontroller了。
如果這裏把[super viewDidLoad]; 去掉,那麼打印的會是誰呢?這個就不一定了,因爲不知道會打印誰了。就會產生壞內存訪問了。
棧空間的分配:都是從高地址向低地址分配的
這裏插入一個圖,以便分析理解 :棧:先創建先放入(地址高),後放入地址低,查找順序,從高地址向低地址。
三:Runtime
3.1:類
首先看一下所有方法
動態創建一個類(參數:父類,類名,額外的內存空間)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
註冊一個類(要在類註冊之前添加成員變量)
void objc_registerClassPair(Class cls)
銷燬一個類
void objc_disposeClassPair(Class cls)
獲取isa指向的Class
Class object_getClass(id obj)
設置isa指向的Class
Class object_setClass(id obj, Class cls)
判斷一個OC對象是否爲Class
BOOL object_isClass(id obj)
判斷一個Class是否爲元類
BOOL class_isMetaClass(Class cls)
獲取父類
Class class_getSuperclass(Class cls)
void run(id self, SEL _cmd)
{
NSLog(@"_____ %@ - %@", self, NSStringFromSelector(_cmd));
}
1: 動態修改內存中的語言,如果是c++ java是無法修改的
void testcard()
{
MJPerson *person = [[MJPerson alloc] init];
[person run];
object_setClass(person, [MJCar class]);
[person run];
}
這裏再setclass之後 run調用的就是car的run方法
2: 動態創建類
void testClass()
{
Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
// 建議在註冊類之前添加這些成員變量、屬性和方法
//(成員變量是隻讀的,類的結構一旦創建,就不能再添加成員變量 ro_t 方法的添加可以放在註冊之後,
// 因爲方法是放在rw_t中)
class_addIvar(newClass, "_age", 4, 1, @encode(int));
class_addIvar(newClass, "_weight", 4, 1, @encode(int));
class_addMethod(newClass, @selector(run), (IMP)run, "v@:");
// 註冊類
objc_registerClassPair(newClass);
// MJPerson *person = [[MJPerson alloc] init];
// object_setClass(person, newClass);
// [person run];
id dog = [[newClass alloc] init];
[dog setValue:@10 forKey:@"_age"];
[dog setValue:@20 forKey:@"_weight"];
[dog run];
//
// NSLog(@"%@ %@", [dog valueForKey:@"_age"], [dog valueForKey:@"_weight"]);
// 在不需要這個類時釋放
objc_disposeClassPair(newClass);
}
3.2:成員變量
獲取一個實例變量信息 (獲取的描述信息,並不是實際值和信息)
Ivar class_getInstanceVariable(Class cls, const char *name)
拷貝實例變量列表(最後需要調用free釋放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
設置和獲取成員變量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
動態添加成員變量(已經註冊的類是不能動態添加成員變量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
獲取成員變量的相關信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
3.2.1 獲取成員變量信息
void testIvars()
{
// 3: 獲取成員變量信息 獲取的描述信息,並不是實際值和信息
Ivar ageIvar = class_getInstanceVariable([MJPerson class], "_age");
NSLog(@"%s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));
// 3.1: 設置和獲取成員變量的值
Ivar nameIvar = class_getInstanceVariable([MJPerson class], "_name");
MJPerson *person = [[MJPerson alloc] init];
object_setIvar(person, nameIvar, @"123"); // 設置的name string
object_setIvar(person, ageIvar, (__bridge id)(void *)10);
NSLog(@"%@ %d", person.name, person.age);
// 4: 成員變量的數量 copy一個成員變量列表 最常用
unsigned int count;
Ivar *ivars = class_copyIvarList([MJPerson class], &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成員變量
Ivar ivar = ivars[i]; // 指針可以當做數組來用,相當於是*(ivars + i) 指針跟着移動 然後取出地址
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars); // 需要釋放
}
3.2.2字典轉模型
void testModel()
{
// 5:字典轉模型功能
NSDictionary *json = @{
@"id" : @20,
@"age" : @20,
@"weight" : @60,
@"name" : @"Jack"
// @"no" : @30
};
MJPerson *person = [MJPerson mj_objectWithJson:json];
[MJCar mj_objectWithJson:json];
MJStudent *student = [MJStudent mj_objectWithJson:json];
NSLog(@"123");
}
#import "NSObject+Json.h"
#import <objc/runtime.h>
@implementation NSObject (Json)
// 這裏只是非常簡單的字典轉模型
//(萬一這裏有繼承體系,而父類的屬性沒有賦值,因爲方法獲取體系只能獲取本類的,
// 這個就還需要獲取superclass,還有可能數值是空的,還有模型嵌套模型的問題 還有id的問題)
+ (instancetype)mj_objectWithJson:(NSDictionary *)json
{
id obj = [[self alloc] init];
unsigned int count;
Ivar *ivars = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成員變量
Ivar ivar = ivars[i];
NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
[name deleteCharactersInRange:NSMakeRange(0, 1)];// 刪除下劃線
// 設值
id value = json[name];
// 這樣寫不合適,因爲這是一個框架,不能寫死
if ([name isEqualToString:@"ID"]) {
value = json[@"id"];
}
[obj setValue:value forKey:name];
}
free(ivars);
return obj;
}
@end
3.2.3:修改系統類的未公開成員變量
- (void)viewDidLoad {
[super viewDidLoad];
// 方法2 通過runtime來獲得內部有啥成員變量 然後設置那個顏色值
// unsigned int count;
// Ivar *ivars = class_copyIvarList([UITextField 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);
self.textField.placeholder = @"請輸入用戶名";
// 方法2 後續 直接比下面更一步到位
[self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
// 方法2 後續 用kvc直接賦值
UILabel *placeholderLabel = [self.textField valueForKeyPath:@"_placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];
// 方法1
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
attrs[NSForegroundColorAttributeName] = [UIColor redColor];
self.textField.attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:@"請輸入用戶名" attributes:attrs];
}
3.3:屬性
獲取一個屬性
objc_property_t class_getProperty(Class cls, const char *name)
拷貝屬性列表(最後需要調用free釋放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
動態添加屬性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
動態替換屬性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
獲取屬性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
3.4:方法
獲得一個實例方法、類方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
方法實現相關操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)
拷貝方法列表(最後需要調用free釋放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
動態添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
動態替換方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
獲取方法的相關信息(帶有copy的需要調用free去釋放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
選擇器相關
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
用block作爲方法實現
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)
3.4.1:替換方法
void test()
{
MJPerson *person = [[MJPerson alloc] init];
// 1:替換方法:對象方法,(一般要替換一個類方法 這裏需要傳進去的就是元類對象,需注意)
class_replaceMethod([MJPerson class], @selector(run), (IMP)myrun, "v");
// 2:imp_implementationWithBlock 這裏傳入的是block,將block包裝秤了一個imp方法
class_replaceMethod([MJPerson class], @selector(run), imp_implementationWithBlock(^{
NSLog(@"123123");
}), "v");
[person run];
}
void myrun()
{
NSLog(@"---myrun");
}
3.4.2:交換方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
// 3 交換方法實現,很常用,
Method runMethod = class_getInstanceMethod([MJPerson class], @selector(run));
Method testMethod = class_getInstanceMethod([MJPerson class], @selector(test));
method_exchangeImplementations(runMethod, testMethod);
[person run];
}
return 0;
}
#import "MJPerson.h"
@implementation MJPerson
- (void)run
{
NSLog(@"%s", __func__);
}
- (void)test
{
NSLog(@"%s", __func__);
}
@end
比較大的價值是交換別人寫的框架或者系統寫的一些類,(比方,我需要拿到整個項目所有的按鈕的點擊事件,但是還需要實現原來的方法 跟覆蓋不同)
例子1:
#import "UIControl+Extension.h"
#import <objc/runtime.h>
@implementation UIControl (Extension)
+ (void)load
{
// hook:鉤子函數
Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));
method_exchangeImplementations(method1, method2);
}
- (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
// 調用系統原來的實現 :此時兩個方法已經置換了
[self mj_sendAction:action to:target forEvent:event];
// [target performSelector:action];
// if ([self isKindOfClass:[UIButton class]]) {
// // 攔截了所有按鈕的事件
//
// }
}
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (IBAction)click1 {
NSLog(@"%s", __func__);
}
- (IBAction)click2 {
NSLog(@"%s", __func__);
}
- (IBAction)click3 {
NSLog(@"%s", __func__);
}
@end
交換本質:根據@selector(sendAction:to:forEvent)去尋找,rw_t中method_t中的IMP, 交換的是IMP. 一旦調用這個交換方法,就會清空緩存,內部源代碼
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp; // 交換方法
m2->imp = m1_imp; // 交換方法
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil); // 清空緩存
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
static void flushCaches(Class cls)
{
runtimeLock.assertWriting();
mutex_locker_t lock(cacheUpdateLock);
if (cls) {
foreach_realized_class_and_subclass(cls, ^(Class c){
cache_erase_nolock(c); // erase 橡皮擦 抹掉
});
}
else {
foreach_realized_class_and_metaclass(^(Class c){
cache_erase_nolock(c);
});
}
}
例子2:
數組或者字典的判空處理。(詳細數組請看這裏)
#import "NSMutableArray+Extension.h"
#import <objc/runtime.h>
@implementation NSMutableArray (Extension)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 類簇:NSString、NSArray、NSDictionary,真實類型是其他類型
Class cls = NSClassFromString(@"__NSArrayM"); // 這個類型 一定要寫對,要不然交換不成功
Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:));
method_exchangeImplementations(method1, method2);
});
}
- (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index
{
if (anObject == nil) return;
[self mj_insertObject:anObject atIndex:index];
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 調用
NSString *obj = nil;
NSMutableArray *array = [NSMutableArray array];
[array addObject:@"jack"];
[array insertObject:obj atIndex:0];
}
字典爲空(集合類型的要注意是可變的還是不可變的,詳細字典請看:這裏)
#import "NSMutableDictionary+Extension.h"
#import <objc/runtime.h>
@implementation NSMutableDictionary (Extension)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"__NSDictionaryM"); // 不可變 (最終父類)
Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
Method method2 = class_getInstanceMethod(cls, @selector(mj_setObject:forKeyedSubscript:));
method_exchangeImplementations(method1, method2);
Class cls2 = NSClassFromString(@"__NSDictionaryI"); // 可變 (最終父類)
Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
Method method4 = class_getInstanceMethod(cls2, @selector(mj_objectForKeyedSubscript:));
method_exchangeImplementations(method3, method4);
});
}
// 賦值操作
- (void)mj_setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
{
if (!key) return;
[self mj_setObject:obj forKeyedSubscript:key];
}
// 取值操作
- (id)mj_objectForKeyedSubscript:(id)key
{
if (!key) return nil;
return [self mj_objectForKeyedSubscript:key];
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSString *obj = nil;
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"name"] = @"jack";
dict[obj] = @"rose";
dict[@"age"] = obj;
NSLog(@"%@", dict);
NSDictionary *dict2 = @{@"name" : [[NSObject alloc] init],
@"age" : @"jack"};
NSString *value = dict2[nil];
NSLog(@"%@", [dict2 class]);
}
例子3:設置字體。
面試:
1:講述一下OC的消息機制
OC中的方法調用其實都是轉成了objc_msgSend函數的調用,給receiver(方法調用者)發送了一條消息(selector方法名)
objc_msgSend底層有3大階段
消息發送(當前類、父類中查找)、動態方法解析、消息轉發
2:消息轉發機制流程
看前面三個圖片總結
3:什麼是Runtime?平時項目中有用過麼?
OC是一門動態性比較強的編程語言,允許很多操作推遲到程序運行時再進行
OC的動態性就是由Runtime來支撐和實現的,Runtime是一套C語言的API,封裝了很多動態性相關的函數
平時編寫的OC代碼,底層都是轉換成了Runtime API進行調用
具體應用
利用關聯對象(AssociatedObject)給分類添加屬性
遍歷類的所有成員變量(修改textfield的佔位文字顏色、字典轉模型、自動歸檔解檔)
交換方法實現(交換系統的方法)
利用消息轉發機制解決方法找不到的異常問題
weak弱指針 底層實現 依賴runtime
......