「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」前端
ZXPerson *p1 = [ZXPerson alloc];
這段代碼來已此做爲咱們探索的入口。control
點擊Setp into
以後,咱們會進入到彙編頁面。alloc_test`objc_alloc
那麼咱們就清楚alloc
以後是調用的 objc_alloc
這個函數,緊接着咱們進行對這個函數添加符號斷點繼續調試。
continue
繼續跟蹤,發現新添加的符號斷點已經斷住了,咱們從彙編界面分析在調用objc_alloc
以後,會調用一個叫作_objc_rootAllocWithZone
的函數,最後會調用一個objc_msgSend
的函數發送消息,然後就完成了alloc的流程。Debug
> DebugWorkFlow
>Always show Disassembly
以後運行。這時候咱們看到紅框標示的那段語句,從後面的提示咱們就能夠得知,這段代碼實際就是調用了objc_alloc的方法了。後續咱們再根據objc_alloc增長符號斷點便可。git
ps:這裏分享點彙編的小知識 咱們看到的bl這個指令是屬於 ARM 架構的彙編語法,他的功能是跳轉到 0x104182564 這個地址去執行相關程序,而且把他下一步執行的地址0x1041821fc保存到 lr寄存器(又能夠叫作x30寄存器) 中以便在objc_alloc執行ret命令以後能夠回來繼續往下執行
github
ZXPerson *p1 = [ZXPerson alloc];
時,咱們直接添加alloc的符號斷點便可立刻定位。那麼,到此咱們3中探索的方法就結束完了。當咱們知道探索方法以及入口以後,咱們怎麼能有效的進行代碼跟蹤呢?若是是下載源碼進行靜態分析顯然讓人以爲不是那麼爽,若是能夠作到就跟調試咱們本身編寫的程序同樣那就太完美了吧。是否真的能實現呢?答案當時是可行的,下面咱們就來搞起!web
首先咱們現須要去蘋果開源網站去下載源碼,根據咱們上面探索的結果發現alloc的底層都是由objc來負責的,因此咱們須要的就是objc4-818.2的源碼。可是!當你興沖沖的下載完畢打開項目而且編譯時,你就發現根本編譯不經過會有不少錯誤。怎麼辦?後端
方案一:你們能夠參考這個文章來進行處理解決。解決源碼順利編譯方法步驟sass
方案二(推薦):就是直接拿人家編譯好的下載便可。最新macOS源碼編譯開源項目 markdown
以上都準備好以後咱們來打開項目,咱們建立一個target選擇命令行工具。架構
建立完成以後,將咱們以前的ZXPerson
拷貝到target目錄下面。app
在 ps:注意這裏有一個坑點,就是在 Build Phases的 Compile Sources下把 main.m 文件奪挪到第一位來,要不可能不會觸發斷點,我不知道是否是個人Xcode(v12.5)問題,你們能夠試一下
編輯器
main.m
文件裏編寫ZXPerson
初始化的代碼,同時加上斷點。
最後,在新建的target的build Settings
中搜索runtime
,找到Enable Hardened Runtime
選項,將其改成NO
;而後繼續在Build Phases
的Dependencies
中引入objc
庫。
接着激動人心的時刻來了,選則好target運行便可。當觸發斷點時點擊Step into能夠繼續跟蹤時咱們就大功告成了! 到這裏我們已經具有進行源碼調試的能力了,下面就能夠探索一下alloc的主線流程啦。
ps:注意有一個調試技巧,每次想跟蹤對象時,先把除 ZXPerson *p1 = [ZXPerson alloc]; 以外的斷點關閉,等斷點斷在 ZXPerson *p1 = [ZXPerson alloc]; 時,再把相關的斷點打開,不然會有其餘對象觸發斷點,而就不是咱們想追蹤的 ZXPerson 對象了。
這部份內容我只想簡單的描述一下,不想作過多的解釋,由於這個知識點咱們平時並不須要特別關注,只要理解原理便可。
原理: 當咱們編寫完Objective-C程序完成編譯以後,最終都會以彙編形式進行執行,那麼在這個編譯過程當中,編譯器(LLVM)會對咱們的代碼進行優化處理,具體他會根據程序來縮減、刪除、簡化等方式進行處理,例如:咱們定義了一個變量 NSString *str
可是並無使用它,雖然這個變量是存在於咱們的程序代碼中的,可是最終編輯器會將這段代碼進行刪除,這個過程就是編譯器的優化,你們只用理解這個概念便可。
在Xcode中控制優化等級:
Build Settings
中搜索optimi
回到看到一個Optimization Level
選項後面就是能夠調整優化級別例如:Release
時默認就是最快且最小
模式,而在Debug
模式下就是默認不優化的狀態。第一步:咱們先來到了objc_alloc
方法,經過名稱咱們大體能夠猜到,經過[cls alloc]
方式alloc對象時,都應該先走到這裏。
第二步:來到allAlloc
方法,這個方法有幾個分支咱們先不用管,先把分支打上斷點,經過斷點咱們發現這裏直接走到最後return
語句,這段話經過objc_msgSend方法
向cls
類的alloc
方法發送消息。
第三步:咱們來到alloc
這個只是過渡方法直接無視繼續往下。
第四步:來到objc_rootAlloc
方法,仍是過渡方法直接無視繼續往下。
第五步:又來到了allAlloc
方法,此次進入了objc_rootAllocWithZone
方法。
第七步:又是一個過渡方法直接無視。
第五步:進入class_createInstanceFromZone
方法,咱們先經過這個方法返回值來分析,經過查看咱們發現返回的是一個叫obj
變量,在往上查找就看到了obj
變量的初始化代碼,咱們能夠總體的分析出來大體的邏輯,首先經過instanceSize
來獲得對象在內存所需的大小;而後對obj
對象從新分配內存空間,這個obj
對象能夠理解成是一個空對象,它自己並無說明含義,由於咱們alloc
的是ZXPerson
類的對象,因此還須要將obj
與ZXPerson
類創建綁定關係,而來聯繫這層關係的就是咱們熟悉的Isa
。後面hasCxxDtor
是將C++的相關功能也賦值給這個obj。
說了這麼多咱們一塊兒來驗證一下obj對象的變化,直接上圖更直觀。
經過LLDB調試咱們分別打印了obj在內存裏面的變化。最後返回obj
最後附上一個流程圖:
一個類的實例在建立以後不添加任何代碼的狀況下,在內存中佔用的大小是8字節,爲何是8字節呢?由於實例對象在內存結構中存放在第一位的是Isa指針,而指針的大小就是佔用8字節。咱們能夠經過增長斷點進行驗證;
如上圖,咱們能夠再左側實例對象中觀察到Isa的指針地址爲0x011d8001000080e9
,而後咱們利用LLDB在右側輸入x zxp
(顯示 zxp 指針的內存狀況),等待打印出結果後咱們就會看到,首個8字節的地址,由於iOS屬於小端模式因此在讀取內存時是從右往左讀
,咱們能夠p 0x011d8001000080e9
打印一下看看是否會顯示Isa的內容,結果出來以後並無跟我預想的同樣,緣由是須要&上ISA_MASK
,爲何須要&上ISA_MASK
?ISA_MASK
值是什麼?帶着這兩個問題咱們一塊兒來尋找答案。
咱們從alloc流程中已經得知,與初始化Isa相關的事情都是在_class_createInstanceFromZone()
函數中實現的,那麼我直接來到改函數的initInstanceIsa()
方法,而後跟進查看一下;
跟進以後發現了叫initIsa()
方法,繼續前進。
到這裏咱們看到了程序再給一個叫newisa
的對象賦值,而這個對象的類型是isa_t
,咱們都知道Isa指向的是該對象的類信息,這裏已經明顯的有setClass()
的方法,咱們只需看看是否有getClass
方法?該方法中是否有咱們想找的東西。
繼續跟蹤isa_t
,果真發現了getClass
方法,繼續跟進查到了clsbits &= ISA_MASK
經過上面的註釋,大體猜到是MASK是一個掩碼,目的是爲了屏蔽除了類指針與簽名以外的一些東西。那麼咱們再看一下ISA_MASK
內容是什麼?
經過搜索咱們找到了。我這裏由於是非ram64架構的,因此匹配到了這個0x0000000ffffffff8ULL
最後咱們來驗證一下!果真打印的是Isa指針指向的類
剛纔咱們是在不添加任何代碼的狀況下,如今咱們增長几個屬性變量看一下內存的變化;而後咱們這回用過x/4gx zxp
方式對打印進行格式化(每隔4段以16進制的數據進行展現)結果以下:
咱們發現zxp
對象第一個位置仍是Isa
,後面的數據分別存儲了zxName
、zxAge
、zxSex
、zxHieght
,優化的部分不知道你們是否看出來了,zxAge
、zxSex
由於是int類型(佔4字節)與char類型(佔1字節)因此共用了8字節的空間,這就是內存對齊
(有關內存對齊的內容我會在下一篇中介紹)。下面咱們分別來驗證一下:
結構示意圖: