一道面試題引發(fā)的思考

之前接觸到了一道面試題目产禾,分析之后覺得這道題目很有意思绝骚,考察了很多的底層知識抖棘。記錄下來以便幫自己整理思路...

有這樣的一個簡單的Person類:

// ------ .h中
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
// ------ .m中
@implementation Person
- (void)print{
    NSLog(@"---self.name is---%@------",self.name);
}
@end

然后在ViewController中是這樣子的:

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj print];
}
問題就是:- (void)print;方法是否可以調(diào)用匿醒,如果可以調(diào)用卵牍,打印結(jié)果是什么?

運行一下程序,查看打印結(jié)果:

---self.name is---<ViewController: 0x159d09ae0>------

當(dāng)我看到打印結(jié)果的時候是一臉懵逼狀態(tài)削罩,我猜對了可以調(diào)用- (void)print;方法瞄勾,但是卻沒有猜對打印的結(jié)果。

那么下面我們就一步步去分析弥激,為何可以調(diào)用方法以及打印結(jié)果是這樣的进陡。

為何可以調(diào)用?

平時我們調(diào)用方法的時候是這個樣子的:

Person *person = [[Person alloc] init];
[person print];

這兩句代碼做了什么呢微服?我是這樣理解的:在函數(shù)棧內(nèi)存放了一個叫person的指針趾疚,指針里的存放的是Person類的一個instance實例對象在堆空間中的內(nèi)存地址。調(diào)用- (void)print;方法是通過person指針找到instance對象以蕴,再通過instance對象的isa指針找到到Person類對象糙麦。從Person類對象中的方法緩存或方法列表中取出方法,進行調(diào)用舒裤。

Snip20180607_2.png

現(xiàn)在我們再來看面試題中的變量之間的關(guān)系:


Snip20180607_3.png

cls中存放的是Person類對象的地址喳资,那么功能上等價于instance對象的isa指針。
那么從流程上似乎就可以說的通了腾供,通過一個指針找到isa或存儲著類對象地址的指針仆邓,再通過它找到Person類對象。

對于計算機來講沒有類或者對象伴鳖,計算機只需要知道节值,去哪里讀寫數(shù)據(jù),讀取/寫入多大的數(shù)據(jù)榜聂。
我一開始有個疑問搞疗,因為我們知道從64位CPU開始,isa并不是直接指向類對象须肆,而是要&上一個ISA_MASK值匿乃,來獲取真正的類對象地址(用33位來存儲指向的地址,其余位存儲一些其他的信息豌汇,如:引用計數(shù)幢炸,是否關(guān)聯(lián)對象,是否有析構(gòu)函數(shù)等)拒贱。那么cls中存儲的是類對象的真實地址宛徊,所以clsisa功能類似佛嬉,值不一定相同,那么它們怎么都能找到類對象呢闸天?

測試發(fā)現(xiàn)暖呕,雖然值不一定相同,但是在objc_msgSend的時候苞氮,通過clsisa&ISA_MASK結(jié)果是相同的湾揽。所以,都可以找到正確的類對象葱淳。

以ARM64平臺為例:

# if __arm64__
# define ISA_MASK        0x0000000ffffffff8ULL

最后一位為8,換成二進制就是1000钝腺,那么假如一個數(shù)A&ISA_MASK抛姑,A & ISA_MASK的作用就是將A中高28位和低3位清零赞厕,取出中間33位數(shù)來。
A & ISA_MASK & ISA_MASK & ISA_MASK...的結(jié)果依然和一次按位與結(jié)果相同定硝。

總結(jié)一下吧皿桑,在面試題中調(diào)用- (void)print;方法,轉(zhuǎn)換成底層的objc_msgSend方法的時候蔬啡,由于和正常時實例對象調(diào)用方法流程相同诲侮,可以通過cls找到類對象,從而找到- (void)print;方法箱蟆,那么就可以調(diào)用方法成功了沟绪。

打印結(jié)果為何如此?

Personinstance對象空猜,在內(nèi)存中的結(jié)構(gòu):

Snip20180607_4.png

isa是個指針绽慈,在64位處理器中,指針占用8個字節(jié)辈毯。那么訪問成員變量_name的時候坝疼,就是在isa地址+8個字節(jié)就可以訪問到_name了。

那么在這道面試題中谆沃,方法調(diào)用者是cls钝凶,而cls是在函數(shù)棧中,那么我們就要分析函數(shù)調(diào)用棧唁影。

隨之這里又考察了一個點耕陷,super關(guān)鍵字的理解。

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
    };

super關(guān)鍵字在底層會生成一個結(jié)構(gòu)體据沈,結(jié)構(gòu)體兩個成員哟沫,一個receiver,一個receiversuper_class。這兩個成員都放在棧中卓舵。

Snip20180607_8.png

通過編程經(jīng)驗或者如果懂得匯編代碼南用,可以比較輕松畫出上面的棧圖。

附上debug模式下,編譯器生成的匯編代碼(release模式下編譯器會做優(yōu)化裹虫,產(chǎn)生的匯編代碼不同):

TEST`-[ViewController viewDidLoad]:
    0x100352524 <+0>:   sub    sp, sp, #0x40             ; =0x40 // 提升sp
    0x100352528 <+4>:   stp    x29, x30, [sp, #0x30]             // 保護x29肿嘲,x30寄存器
    0x10035252c <+8>:   add    x29, sp, #0x30            ; =0x30 // 提升fp(x29)
    0x100352530 <+12>:  add    x8, sp, #0x10             ; =0x10 
    0x100352534 <+16>:  adrp   x9, 2
    0x100352538 <+20>:  add    x9, x9, #0xdf8            ; =0xdf8 
    0x10035253c <+24>:  adrp   x10, 2
    0x100352540 <+28>:  add    x10, x10, #0xe30          ; =0xe30 //猜測:應(yīng)該是查找UIViewCOntroller類
    
    0x100352544 <+32>:  stur   x0, [x29, #-0x8]     //在棧中存self
    0x100352548 <+36>:  stur   x1, [x29, #-0x10]    //在棧中存方法viewDidLoad
    0x10035254c <+40>:  ldur   x0, [x29, #-0x8]
    0x100352550 <+44>:  str    x0, [sp, #0x10]      //在棧中再存入一個self
    0x100352554 <+48>:  ldr    x10, [x10]
    0x100352558 <+52>:  str    x10, [sp, #0x18]     //將UIViewCOntroller存入棧中
    0x10035255c <+56>:  ldr    x1, [x9]
    0x100352560 <+60>:  mov    x0, x8
    0x100352564 <+64>:  bl     0x100352b00               ; symbol stub for: objc_msgSendSuper2      //調(diào)用objc_msgSendSuper2方法
    0x100352568 <+68>:  adrp   x8, 2
    0x10035256c <+72>:  add    x8, x8, #0xe00            ; =0xe00 
    0x100352570 <+76>:  adrp   x9, 2
    0x100352574 <+80>:  add    x9, x9, #0xe20            ; =0xe20 
    0x100352578 <+84>:  ldr    x9, [x9]
    0x10035257c <+88>:  ldr    x1, [x8]
    0x100352580 <+92>:  mov    x0, x9
    0x100352584 <+96>:  bl     0x100352af4               ; symbol stub for: objc_msgSend
    0x100352588 <+100>: mov    x29, x29
    0x10035258c <+104>: bl     0x100352b18               ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x100352590 <+108>: adrp   x8, 2
    0x100352594 <+112>: add    x8, x8, #0xe08            ; =0xe08 
    0x100352598 <+116>: add    x9, sp, #0x8              ; =0x8 
    0x10035259c <+120>: str    x0, [sp, #0x8]  //將Person類對象地址放入棧中
    0x1003525a0 <+124>: str    x9, [sp]        // 將cls的地址放入棧中
    0x1003525a4 <+128>: ldr    x0, [sp]
    0x1003525a8 <+132>: ldr    x1, [x8]
    0x1003525ac <+136>: bl     0x100352af4               ; symbol stub for: objc_msgSend
    0x1003525b0 <+140>: add    x0, sp, #0x8              ; =0x8 
    0x1003525b4 <+144>: mov    x8, #0x0
->  0x1003525b8 <+148>: mov    x1, x8
    0x1003525bc <+152>: bl     0x100352b30               ; symbol stub for: objc_storeStrong
    0x1003525c0 <+156>: ldp    x29, x30, [sp, #0x30]
    0x1003525c4 <+160>: add    sp, sp, #0x40             ; =0x40 
    0x1003525c8 <+164>: ret    

這樣在訪問_name的時候,其實是訪問椫空間內(nèi)的cls地址值+8個字節(jié)的地址里存放的內(nèi)容雳窟,就是viewController對象了。

可能有的同學(xué)還是對super關(guān)鍵字不是很理解匣屡,沒事封救,下面這道題目分析一下:

self和super的測試

FGObject1:
@interface FGObject1 : NSObject

@end

@implementation FGObject1

- (Class)class
{
    return [NSObject class];
}

- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"FGObject1 --- %@ --- %@",[self class],[super class]);
    }
    return self;
}
@end
FGObject2:
@interface FGObject2 : FGObject1

@end

@implementation FGObject2

- (Class)class
{
    return [UIView class];
}

- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"FGObject2 --- %@ --- %@",[self class],[super class]);
    }
    return self;
}

@end
    
當(dāng)分別創(chuàng)建它們兩個不同的對象的時候,控制臺如何輸出捣作?
FGObject1 *object1 = [[FGObject1 alloc] init];
FGObject2 *object2 = [[FGObject2 alloc] init];

輸出結(jié)果:

2018-04-27 ARCStudy[80422:11252273] FGObject1 --- NSObject --- FGObject1
2018-04-27 ARCStudy[80422:11252273] FGObject1 --- UIView --- FGObject2
2018-04-27 ARCStudy[80422:11252273] FGObject2 --- UIView --- NSObject

那么結(jié)果你答對了嗎?

方法中的隱藏參數(shù)

我們經(jīng)常在方法中使用self關(guān)鍵字來引用實例本身誉结,但從沒有想過為什么self就能取到調(diào)用當(dāng)前方法的對象吧。其實self的內(nèi)容是在方法運行時被偷偷的動態(tài)傳入的券躁。

當(dāng)objc_msgSend找到方法對應(yīng)的實現(xiàn)時惩坑,它將直接調(diào)用該方法實現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏的參數(shù):

  1. 接收消息的對象(也就是self指向的內(nèi)容)
  2. 方法選擇器(_cmd指向的內(nèi)容)

之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)也拜。它們是在代碼被編譯時被插入實現(xiàn)中的以舒。

在這兩個參數(shù)中,self更有用慢哈。實際上蔓钟,它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑。

而當(dāng)方法中的super關(guān)鍵字接收到消息時卵贱,編譯器會創(chuàng)建一個objc_super結(jié)構(gòu)體:

struct objc_super {id receiver; Class class;};
/** 
 * 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
 */
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

附錄蘋果官方對objc_msgSendSuper的注釋滥沫,寫的比較清楚,receive是類的instance對象艰赞,而查找方法IMP時是從superclass來查找的佣谐。

這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。receiver仍然是self本身方妖,這點需要注意狭魂。

如果我們這樣來寫:

- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"FGObject1 --- %@ --- %@",[self class],[super class]);
    }
    return self;
}

打印結(jié)果:

2018-04-27 ARCStudy[80476:11297894] FGObject1 --- FGObject1 --- FGObject1

因為當(dāng)我們想通過[super class]獲取超類時,編譯器只是將指向selfid指針和classSEL傳遞給了objc_msgSendSuper函數(shù)党觅,因為只有在NSObject類才能找到class方法雌澄,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class))杯瞻,傳入的第一個參數(shù)是指向selfid指針镐牺,與調(diào)用[self class]相同,所以我們得到的永遠都是self的類型魁莉。

Snip20180427_10.png

那么最上面那個測試的結(jié)果呢睬涧?不同就是在類中都重寫了- (Class)class募胃;方法。

FGObject1初始化時:

  • [self class]:調(diào)用自己重寫的方法畦浓,返回NSObject痹束。
  • [super class]:給父類NSObject發(fā)送消息,那么就是去查找實例對象isa指針指向的類讶请,所以結(jié)果是FGObject1祷嘶。

FGObject2初始化時:

  • [super init][self class]:會調(diào)用FGObject2- (Class)class;方法夺溢,打印UIView论巍。
  • [super init][super class]:給父類NSObject發(fā)送消息,那么就是去查找實例對象isa指針指向的類风响,所以結(jié)果是FGObject2嘉汰。
  • 輪到自己的[self class]時,調(diào)用自己的- (Class)class钞诡;方法郑现,打印UIView
  • 輪到自己的[super class]時荧降,調(diào)用FGObject1- (Class)class;方法攒读,打印的是NSObject朵诫。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市薄扁,隨后出現(xiàn)的幾起案子剪返,更是在濱河造成了極大的恐慌,老刑警劉巖邓梅,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脱盲,死亡現(xiàn)場離奇詭異,居然都是意外死亡日缨,警方通過查閱死者的電腦和手機钱反,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匣距,“玉大人面哥,你說我怎么就攤上這事∫愦” “怎么了尚卫?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尸红。 經(jīng)常有香客問我吱涉,道長刹泄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任怎爵,我火速辦了婚禮循签,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疙咸。我一直安慰自己县匠,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布撒轮。 她就那樣靜靜地躺著乞旦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪题山。 梳的紋絲不亂的頭發(fā)上兰粉,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音顶瞳,去河邊找鬼玖姑。 笑死,一個胖子當(dāng)著我的面吹牛慨菱,可吹牛的內(nèi)容都是我干的焰络。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼符喝,長吁一口氣:“原來是場噩夢啊……” “哼闪彼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起协饲,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤畏腕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茉稠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體描馅,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年而线,在試婚紗的時候發(fā)現(xiàn)自己被綠了铭污。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡吞获,死狀恐怖况凉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情各拷,我是刑警寧澤刁绒,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站烤黍,受9級特大地震影響知市,放射性物質(zhì)發(fā)生泄漏傻盟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一嫂丙、第九天 我趴在偏房一處隱蔽的房頂上張望娘赴。 院中可真熱鬧,春花似錦跟啤、人聲如沸诽表。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竿奏。三九已至,卻和暖如春腥放,著一層夾襖步出監(jiān)牢的瞬間泛啸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工秃症, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留候址,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓种柑,卻偏偏與公主長得像岗仑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子莹规,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉赔蒲,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評論 0 9
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,089評論 1 32
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • 1 經(jīng)常會聽到這樣的問題: 我要報志愿了能給些意見嗎? 我快畢業(yè)了應(yīng)該找什么樣的工作呢良漱? 我覺得工作不開心應(yīng)該怎么...
    貓口閱讀 1,271評論 0 3
  • 一 蘇洛有劍 原本這是一把劍的故事,是有一把傳奇色彩的劍欢际,據(jù)說那劍先是由中原大家鑄造出鼎母市,又被前朝的某位將軍持著斬...
    月下破曉閱讀 586評論 3 1