Objective-C 底層對象探究-上

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

目錄

1. 背景

  • 學習不迷茫,無阻我飛揚!你們好我是Tommy!今天對iOS對象alloc方法進行了詳細研究,目的是爲了瞭解對象底層的本質、和對象在內存中的結構。若是你也有一樣的興趣?不要懷疑的閱讀下去吧!~

2. 底層探索的三個方法

  • 經過符號斷點:
    • 首先咱們將斷點打到 ZXPerson *p1 = [ZXPerson alloc];這段代碼來已此做爲咱們探索的入口。
    • 開始編譯運行以後咱們來到斷點,經過按住control 點擊Setp into以後,咱們會進入到彙編頁面。

    圖片.png

    • 在彙編頂部顯示的alloc_test`objc_alloc 那麼咱們就清楚alloc以後是調用的 objc_alloc這個函數,緊接着咱們進行對這個函數添加符號斷點繼續調試。

    圖片.png 圖片.png 圖片.png 圖片.png

    • 點擊continue繼續跟蹤,發現新添加的符號斷點已經斷住了,咱們從彙編界面分析在調用objc_alloc以後,會調用一個叫作_objc_rootAllocWithZone的函數,最後會調用一個objc_msgSend的函數發送消息,然後就完成了alloc的流程。
  • 經過彙編:
    • 首先咱們先經過Xcode設置開啓彙編顯示功能,Debug > DebugWorkFlow >Always show Disassembly 以後運行。

    圖片.png這時候咱們看到紅框標示的那段語句,從後面的提示咱們就能夠得知,這段代碼實際就是調用了objc_alloc的方法了。後續咱們再根據objc_alloc增長符號斷點便可。git

    ps:這裏分享點彙編的小知識 咱們看到的bl這個指令是屬於 ARM 架構的彙編語法,他的功能是跳轉到 0x104182564 這個地址去執行相關程序,而且把他下一步執行的地址0x1041821fc保存到 lr寄存器(又能夠叫作x30寄存器) 中以便在objc_alloc執行ret命令以後能夠回來繼續往下執行github

  • 經過符號斷點快速定位:
    • 最後一種方法最暴力,當斷點執行到ZXPerson *p1 = [ZXPerson alloc];時,咱們直接添加alloc的符號斷點便可立刻定位。那麼,到此咱們3中探索的方法就結束完了。

    圖片.png

3. 如何進行源碼調試

  • 當咱們知道探索方法以及入口以後,咱們怎麼能有效的進行代碼跟蹤呢?若是是下載源碼進行靜態分析顯然讓人以爲不是那麼爽,若是能夠作到就跟調試咱們本身編寫的程序同樣那就太完美了吧。是否真的能實現呢?答案當時是可行的,下面咱們就來搞起!web

  • 首先咱們現須要去蘋果開源網站去下載源碼,根據咱們上面探索的結果發現alloc的底層都是由objc來負責的,因此咱們須要的就是objc4-818.2的源碼。可是!當你興沖沖的下載完畢打開項目而且編譯時,你就發現根本編譯不經過會有不少錯誤。怎麼辦?後端

  • 方案一:你們能夠參考這個文章來進行處理解決。解決源碼順利編譯方法步驟sass

  • 方案二(推薦):就是直接拿人家編譯好的下載便可。最新macOS源碼編譯開源項目 圖片.png 圖片.pngmarkdown

  • 以上都準備好以後咱們來打開項目,咱們建立一個target選擇命令行工具。架構

    圖片.png

  • 建立完成以後,將咱們以前的ZXPerson拷貝到target目錄下面。app

    圖片.png圖片.pngps:注意這裏有一個坑點,就是在 Build Phases的 Compile Sources下把 main.m 文件奪挪到第一位來,要不可能不會觸發斷點,我不知道是否是個人Xcode(v12.5)問題,你們能夠試一下編輯器

  • main.m文件裏編寫ZXPerson初始化的代碼,同時加上斷點。 圖片.png

  • 最後,在新建的target的build Settings 中搜索runtime ,找到Enable Hardened Runtime選項,將其改成NO;而後繼續在Build PhasesDependencies中引入objc庫。

    圖片.png 圖片.png 接着激動人心的時刻來了,選則好target運行便可。當觸發斷點時點擊Step into能夠繼續跟蹤時咱們就大功告成了! 圖片.png 圖片.png 圖片.png到這裏我們已經具有進行源碼調試的能力了,下面就能夠探索一下alloc的主線流程啦。

    ps:注意有一個調試技巧,每次想跟蹤對象時,先把除 ZXPerson *p1 = [ZXPerson alloc]; 以外的斷點關閉,等斷點斷在 ZXPerson *p1 = [ZXPerson alloc]; 時,再把相關的斷點打開,不然會有其餘對象觸發斷點,而就不是咱們想追蹤的 ZXPerson 對象了。

4. 編譯器的優化(LLVM優化)

  • 這部份內容我只想簡單的描述一下,不想作過多的解釋,由於這個知識點咱們平時並不須要特別關注,只要理解原理便可。

  • 原理: 當咱們編寫完Objective-C程序完成編譯以後,最終都會以彙編形式進行執行,那麼在這個編譯過程當中,編譯器(LLVM)會對咱們的代碼進行優化處理,具體他會根據程序來縮減、刪除、簡化等方式進行處理,例如:咱們定義了一個變量 NSString *str 可是並無使用它,雖然這個變量是存在於咱們的程序代碼中的,可是最終編輯器會將這段代碼進行刪除,這個過程就是編譯器的優化,你們只用理解這個概念便可。

  • 在Xcode中控制優化等級:

    • Build Settings 中搜索optimi 回到看到一個Optimization Level選項後面就是能夠調整優化級別例如:圖片.png圖片.png
    • 這時咱們會發現,當處於Release時默認就是最快且最小模式,而在Debug模式下就是默認不優化的狀態。圖片.png

5. alloc的主線流程

  • 第一步:咱們先來到了objc_alloc方法,經過名稱咱們大體能夠猜到,經過[cls alloc]方式alloc對象時,都應該先走到這裏。圖片.png

  • 第二步:來到allAlloc方法,這個方法有幾個分支咱們先不用管,先把分支打上斷點,經過斷點咱們發現這裏直接走到最後return語句,這段話經過objc_msgSend方法cls類的alloc方法發送消息。圖片.png

  • 第三步:咱們來到alloc這個只是過渡方法直接無視繼續往下。圖片.png

  • 第四步:來到objc_rootAlloc方法,仍是過渡方法直接無視繼續往下。圖片.png

  • 第五步:又來到了allAlloc方法,此次進入了objc_rootAllocWithZone方法。圖片.png

  • 第七步:又是一個過渡方法直接無視。圖片.png

  • 第五步:進入class_createInstanceFromZone方法,咱們先經過這個方法返回值來分析,經過查看咱們發現返回的是一個叫obj變量,在往上查找就看到了obj變量的初始化代碼,咱們能夠總體的分析出來大體的邏輯,首先經過instanceSize來獲得對象在內存所需的大小;而後對obj對象從新分配內存空間,這個obj對象能夠理解成是一個空對象,它自己並無說明含義,由於咱們alloc的是ZXPerson類的對象,因此還須要將objZXPerson類創建綁定關係,而來聯繫這層關係的就是咱們熟悉的Isa。後面hasCxxDtor是將C++的相關功能也賦值給這個obj。圖片.png圖片.png

  • 說了這麼多咱們一塊兒來驗證一下obj對象的變化,直接上圖更直觀。

    圖片.png 圖片.png 圖片.png

  • 經過LLDB調試咱們分別打印了obj在內存裏面的變化。最後返回obj 圖片.png

  • 最後附上一個流程圖: 圖片.png

6. 對象在內存中的結構

  • 一個類的實例在建立以後不添加任何代碼的狀況下,在內存中佔用的大小是8字節,爲何是8字節呢?由於實例對象在內存結構中存放在第一位的是Isa指針,而指針的大小就是佔用8字節。咱們能夠經過增長斷點進行驗證; 圖片.png

  • 如上圖,咱們能夠再左側實例對象中觀察到Isa的指針地址爲0x011d8001000080e9,而後咱們利用LLDB在右側輸入x zxp(顯示 zxp 指針的內存狀況),等待打印出結果後咱們就會看到,首個8字節的地址,由於iOS屬於小端模式因此在讀取內存時是從右往左讀,咱們能夠p 0x011d8001000080e9打印一下看看是否會顯示Isa的內容,結果出來以後並無跟我預想的同樣,緣由是須要&上ISA_MASK,爲何須要&上ISA_MASKISA_MASK值是什麼?帶着這兩個問題咱們一塊兒來尋找答案。

  • 咱們從alloc流程中已經得知,與初始化Isa相關的事情都是在_class_createInstanceFromZone()函數中實現的,那麼我直接來到改函數的initInstanceIsa()方法,而後跟進查看一下; 圖片.png

  • 跟進以後發現了叫initIsa()方法,繼續前進。 圖片.png

  • 到這裏咱們看到了程序再給一個叫newisa的對象賦值,而這個對象的類型是isa_t,咱們都知道Isa指向的是該對象的類信息,這裏已經明顯的有setClass()的方法,咱們只需看看是否有getClass方法?該方法中是否有咱們想找的東西。 圖片.png

  • 繼續跟蹤isa_t,果真發現了getClass方法,繼續跟進查到了clsbits &= ISA_MASK經過上面的註釋,大體猜到是MASK是一個掩碼,目的是爲了屏蔽除了類指針與簽名以外的一些東西。那麼咱們再看一下ISA_MASK內容是什麼?

    圖片.png 圖片.png

  • 經過搜索咱們找到了。我這裏由於是非ram64架構的,因此匹配到了這個0x0000000ffffffff8ULL 圖片.png

  • 最後咱們來驗證一下!果真打印的是Isa指針指向的類 圖片.png

  • 剛纔咱們是在不添加任何代碼的狀況下,如今咱們增長几個屬性變量看一下內存的變化;而後咱們這回用過x/4gx zxp方式對打印進行格式化(每隔4段以16進制的數據進行展現)結果以下: 圖片.png 圖片.png 圖片.png

  • 咱們發現zxp對象第一個位置仍是Isa,後面的數據分別存儲了zxNamezxAgezxSexzxHieght,優化的部分不知道你們是否看出來了,zxAgezxSex由於是int類型(佔4字節)與char類型(佔1字節)因此共用了8字節的空間,這就是內存對齊(有關內存對齊的內容我會在下一篇中介紹)。下面咱們分別來驗證一下: 圖片.png

  • 結構示意圖: 圖片.png

總結:

  • 咱們知道了如何經過三種方式來探索底層代碼;
  • 經過下載編譯好的源碼項目,使咱們能夠經過調試來進行探索。
  • LLVM是有優化策略的,能夠在Xocde中能夠手動修改。
  • alloc的主線流程
  • 對象在內存中的結構
寫到最後
導航:
相關文章
相關標籤/搜索