朋友發給我一到面試題,問:ios
@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Speaker
- (void)speak {
NSLog(@"Speaker's name: %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Speaker class]; // 1
void *obj = &cls; // 2
[(__bridge id)obj speak]; // 3
}
@end
複製代碼
固然,本着 反正不是真面試 的態度,直接跑一下不就好了,嘿嘿。面試
//輸出
Speaker's name: <ViewController: 0x7fcc84e09e90>
複製代碼
能夠看到運行時成功的,但輸出的結果讓我有點懵逼???緣由有2點:shell
ViewController
對象?下面咱們仔細分析一下。iphone
第一步函數
id cls = [Speaker class]; // 1
複製代碼
這一步獲取到了Speaker
的類對象,id
表示將其轉換爲一個對象指針,實際類型爲struct objc_object *
。flex
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
複製代碼
而 [Speaker class] 的返回類型爲Class
,其實類型爲struct objc_class *
。ui
typedef struct objc_class *Class;
複製代碼
雖然,咱們寫的類型爲struct objc_object *
,但其本質仍是struct objc_class *
。atom
id cls = [Speaker class];
if (object_isClass(cls)) {
NSLog(@"object_isClass");
}
// 輸出
object_isClass
複製代碼
也就是說這一步拿到的 本質仍是類對象。spa
id cls = [Speaker class];
[cls speak];
// 直接發送消息,是會崩潰的
+[Speaker speak]: unrecognized selector sent to class 0x106824f08
複製代碼
第二步、第三步指針
void *obj = &cls; // 2
複製代碼
這一步纔是關鍵。
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
複製代碼
能夠看到struct objc_object
這個結構體的首字段是 isa 指向一個Class
。
也就是說,咱們若是有一個指向Class
的地址的指針,至關於這個對象就已經可使用了。
@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Speaker
- (void)speak {
NSLog(@"speak");
}
@end
struct my_object {
Class isa;
};
struct my_object *getObject() {
// id cls = [Speaker class]; id類型的實質是一個指針,因此cls是一個指針
// void *obj = &cls; 這裏取cls的地址,至關於[Speaker class]如今被一個 指針 的 指針 所指向
// 下面 struct my_object * 是一個指針,isa 是一個也是一個指針
// 因此也等效於[Speaker class]如今被一個 指針 的 指針 所指向
struct my_object *obj = (struct my_object *)malloc(sizeof(struct my_object));
obj->isa = [Speaker class];
return obj;
}
- (void)viewDidLoad {
[super viewDidLoad];
struct my_object *obj = getObject();
id obj1 = (__bridge id)obj;
[obj1 speak]; // 3
free(obj);
}
複製代碼
咱們能夠看到,經過id
類型轉換obj1
也被Xcode識別爲了Speaker
實例對象,並且咱們調用 [obj1 speak] 也順利輸出了。
至關於消息 objc_msgSend 執行過程當中經過 obj1
順利訪問到了 isa
對象,在Speaker
類中找到了speak
實例方法,併成功調用。
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Speaker
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Speaker class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
複製代碼
咱們將這個 ViewController.m 文件編譯爲 ViewController.cpp 來看一下。
在 終端 中切換到 ViewController.m 所在目錄,並輸入如下命令:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc -fobjc-arc -mios-version-min=8.0.0 -fobjc-runtime=ios-8.0.0 ViewController.m
複製代碼
執行完畢後咱們能夠在同一個目錄下找到 ViewController.cpp 文件。
打開 ViewController.cpp ,並搜索 ViewController_viewDidLoad 便可找到下面的方法:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
id cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Speaker"), sel_registerName("class"));
void *obj = &cls;
((void (*)(id, SEL))(void *)objc_msgSend)((id)(__bridgeid)obj, sel_registerName("speak"));
}
複製代碼
看起來有點複雜,咱們把非必要的格式轉換去掉:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")); // 1
id cls = objc_msgSend(objc_getClass("Speaker"), sel_registerName("class")); // 2
void *obj = &cls; // 3
objc_msgSend(obj, sel_registerName("speak")); // 4
}
複製代碼
能夠看到:
objc_msgSend 會傳入兩個隱式參數self
和_cmd
,想必你們已經很熟悉了。
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼
而 objc_msgSendSuper 須要傳入另外一個結構體 struct objc_super *
。
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
複製代碼
{self, class_getSuperclass(objc_getClass("ViewController"))} 實際上就是在初始化一個struct objc_super
結構體。
知道這些以後,再閱讀上面的代碼就沒有什麼難度了。
void sum(NSNumber *a, NSNumber *b) {
NSLog(@"a地址 = %p", &a);
NSLog(@"b地址 = %p", &b);
printf("%d", a.intValue + b.intValue);
}
- (void)viewDidLoad {
[super viewDidLoad];
sum(@(1), @(2));
NSNumber *c = @(4);
NSLog(@"c地址 = %p", &c);
}
複製代碼
咱們在給函數傳入參數時,參數會做爲自動變量入棧 :
並且咱們能夠看到入棧的順序是a
先入棧,b
後入棧,由於 棧從高地址到低地址分配內存 。
可是在初始化一個結構體的時候,這個順序是相反的:
咱們看到 two_number tn = {@(1), @(2)}; 先傳入的是1
後傳入的2
,但實際狀況是2
先入棧,1
後入棧。
按照上面2條規則,下面代碼第5步以前的變量入棧的順序應該是:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) { // 1
objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")); // 2
id cls = objc_msgSend(objc_getClass("Speaker"), sel_registerName("class")); // 3
void *obj = &cls; // 4
objc_msgSend(obj, sel_registerName("speak")); // 5
}
複製代碼
self
、_cmd
爲函數的隱式參數,依次先入棧。
objc_msgSendSuper 初始化了一個結構體,這個結構體的參數會入棧。
又由於參數入棧是從右到左的順序入棧:
class_getSuperclass(objc_getClass("ViewController"))
self
後入棧cls
本地變量賦值爲Speaker
類,最後入棧
那麼入棧的順序爲self
、_cmd
、class_getSuperclass(objc_getClass("ViewController"))
、self
、Speaker
類。下面咱們驗證一下:
@interface Speaker : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Speaker
- (void)speak {
NSLog(@"Speaker self: %p, _name: %p", self, &_name);
NSLog(@"Speaker's name: %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Speaker class]; // 1
void *obj = &cls; // 2
NSLog(@"棧區變量");
void *start = (void *)&self;
void *end = (void *)&obj;
long count = (start - end) / 0x8;
for (long i = 0; i < count; i++) {
void *address = start - 0x8 * i;
if (i == 1) {
NSLog(@"%p: %s", address, *(char **)(address));
} else {
NSLog(@"%p: %@", address, *(void **)address);
}
}
NSLog(@"obj speak");
[(__bridge id)obj speak]; // 3
}
@end
// 打印
Demo[32768:1105890] 棧區變量
Demo[32768:1105890] 0x7ffeec17c648: <ViewController: 0x7fb445607ee0>
Demo[32768:1105890] 0x7ffeec17c640: viewDidLoad
Demo[32768:1105890] 0x7ffeec17c638: ViewController //這裏比較怪
Demo[32768:1105890] 0x7ffeec17c630: <ViewController: 0x7fb445607ee0>
Demo[32768:1105890] 0x7ffeec17c628: Speaker
Demo[32768:1105890] obj speak
Demo[32768:1105890] Speaker self: 0x7ffeec17c628, _name: 0x7ffeec17c630
Demo[32768:1105890] Speaker's name: <ViewController: 0x7fb445607ee0>
複製代碼
從輸出能夠看到,棧區的打印順序和咱們的分析基本吻合。
下面咱們看一下爲何Speaker
實例對象的 self.name 訪問到的是ViewController
實例對象。
Speaker
實例對象,若是咱們經過 [[Speaker alloc] init] 初始化的話,會在堆區分配內存。但如今,咱們是使用棧區指針指向了Speaker
類對象地址,"假裝"成了一個Speaker
實例對象,因此傳入的self
值爲棧區的地址:0x7ffeec17c628 。
從上面的輸出咱們能夠看到,name
屬性的實例變量_name
在Speaker
實例對象 self + 0x8 的地址,即 0x7ffeec17c630 。
根據輸出_name
實例變量訪問的地址 0x7ffeec17c630 ,找到棧區對應的數據 0x7ffeec17c630: <ViewController: 0x7fb445607ee0> ,因此輸出爲 Speaker's name: <ViewController: 0x7fb445607ee0> 。
經過這個面試題咱們得出了一下結論:
Objective-C中的對象是一個指向class_object地址的變量,即 id obj = &class_object
對象的實例變量 void *ivar = &obj + offset(N)
但這裏還有一個疑問:
咱們看到直接調用 [super viewDidLoad]; ,棧區的第3個變量爲ViewController
類。
但根據咱們用Clang重寫的代碼 [super viewDidLoad]; 實現作替換:
- (void)viewDidLoad {
((void (*)(struct objc_super *, SEL))(void *)objc_msgSendSuper)(&((struct objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}), sel_registerName("viewDidLoad"));
id cls = [Speaker class]; // 1
void *obj = &cls; // 2
NSLog(@"棧區變量");
void *start = (void *)&self;
void *end = (void *)&obj;
long count = (start - end) / 0x8;
for (long i = 0; i < count; i++) {
void *address = start - 0x8 * i;
if (i == 1) {
NSLog(@"%p: %s", address, *(char **)(address));
} else {
NSLog(@"%p: %@", address, *(void **)address);
}
}
NSLog(@"obj speak");
[(__bridge id)obj speak]; // 3
}
// 輸出
Demo[33008:1114325] 棧區變量
Demo[33008:1114325] 0x7ffee4983648: <ViewController: 0x7f9e0bf07fd0>
Demo[33008:1114325] 0x7ffee4983640: viewDidLoad
Demo[33008:1114325] 0x7ffee4983638: UIViewController // 這裏符合預期
Demo[33008:1114325] 0x7ffee4983630: <ViewController: 0x7f9e0bf07fd0>
Demo[33008:1114325] 0x7ffee4983628: Speaker
Demo[33008:1114325] obj speak
Demo[33008:1114325] Speaker self: 0x7ffee4983628, _name: 0x7ffee4983630
Demo[33008:1114325] Speaker's name: <ViewController: 0x7f9e0bf07fd0>
複製代碼
咱們看到棧區的第3個變量爲UIViewController
類,這個輸出是符合預期的,由於class_getSuperclass(objc_getClass("ViewController"))
咱們獲取的就是父類。
但爲何直接調用 [super viewDidLoad]; ,棧區的第3個變量爲ViewController
類,這個問題難道是Xcode的Bug???
若是以爲本文對你有所幫助,給我點個贊吧~ 👍🏻