【iOS】類和分類的加載過程

目錄

前言

_objc_init方法

environ_init

tis->init方法

static_init方法

💡 _objc_init 是由 libc 調用的,目的是:

??“必須自己實現” 是什么意思?

runtime_init

exception_init

cache_t::init

_imp_implementationWithBlock_init

_dyld_objc_notify_register

dyld與Objc的關聯

map_images的調用時機

dyld與Objc關聯

map_images

_read_images

創建表

修復預編譯階段的@selector的混亂問題

錯誤混亂的類處理

修復重映射一些沒有被鏡像文件加載進來的類

修復一些消息

當類里面有協議時:readProtocol 讀取協議

修復沒被加載的協議

分類處理

類的加載處理

沒有被處理的類,優化那些被侵犯的類

關鍵步驟

readClass

realizeClassWithoutSwift

讀取 data 數據,并設置 ro、rw

遞歸調用 realizeClassWithoutSwift 完善 繼承鏈

通過 methodizeClass 方法化類

methodizeClass

attachToClass方法

attachCategories方法

attachLists方法

分類

分類的本質

分類的加載

分類加載時機

load_images

prepare_load_methods實現

schedule_class_load方法

add_category_to_loadable_list

call_load_methods

initalize分析


前言

其實在之前分析dyld的加載流程的時候已經有涉及到一些有關類和分類加載的過程了,這篇文章來探索一下類和分類的加載過程在底層的實現,研究的起點是_objc_init。

_objc_init方法

可以看出來_objc_init的實現中主要是調用了許多的init方法。

environ_init

environ_init()方法是初始化一系列環境變量,并讀取影響運行時的環境變量。

常用的環境變量有以下這些:

  • DYLD_PRINT_STATISTICS:設置 DYLD_PRINT_STATISTICS 為YES,控制臺就會打印 App 的加載時長,包括整體加載時長和動態庫加載時長,即main函數之前的啟動時間(查看pre-main耗時),可以通過設置了解其耗時部分,并對其進行啟動優化

  • OBJC_DISABLE_NONPOINTER_ISA:杜絕生成相應的nonpointer isa(nonpointer isa指針地址 末尾為1 ),生成的都是普通的isa

  • OBJC_PRINT_LOAD_METHODS:打印 Class 及 Category 的 + (void)load 方法的調用信息

  • NSDoubleLocalizedStrings:項目做國際化本地化(Localized)的時候是一個挺耗時的工作,想要檢測國際化翻譯好的語言文字UI會變成什么樣子,可以指定這個啟動項.可以設置 NSDoubleLocalizedStrings 為YES

  • NSShowNonLocalizedStrings:在完成國際化的時候,偶爾會有一些字符串沒有做本地化,這時就可以設置NSShowNonLocalizedStrings 為YES,所有沒有被本地化的字符串全都會變成大寫

tis->init方法

tls->init()方法是關于線程key的綁定,主要是本地線程池的初始化以及析構。

static_init方法

static_init()方法注釋中提到該方法會運行C++靜態構造函數(只會運行系統級別的構造函數) 在dyld調用靜態構造函數之前,libc會調用_objc_init,所以必須自己去實現。

為什么要在static_init之前調用_objc_init:

💡 _objc_init 是由 libc 調用的,目的是:

在所有 ObjC 相關代碼執行之前,先初始化 ObjC Runtime(注冊類、創建基本元類、初始化 TLS、Hook、AutoreleasePool 等)。

這意味著:

  • dyld 還沒執行 C++ 靜態構造函數

  • 而我們已經通過 _objc_init 把 ObjC 的 runtime 環境搭建好了

  • 這樣之后無論運行什么構造函數,如果里面使用了 ObjC 的對象/類/方法,都不會崩潰

??“必須自己實現” 是什么意思?

這是指 Objective-C Runtime 不能依賴 dyld 的 static_init() 去自動初始化自己(因為它太晚了),所以:

ObjC Runtime 必須自己注冊一個早期初始化入口 —— _objc_init,并讓 libc 來調用它。

這個初始化包括:

  • 注冊 TLS

  • 初始化 runtime 狀態(runtime_init()

  • hook 一些系統函數(如 malloc, pthread 等)

  • 初始化 autorelease pool

  • 加載 image 等

runtime_init

運行時初始化,主要分為兩部分:分類初始化類的表初始化

exception_init

exception_init()負責初始化libobjc的異常處理系統,注冊異常處理的回調,從而監控異常的處理

cache_t::init

負責緩存初始化

_imp_implementationWithBlock_init

啟動回調機制

_dyld_objc_notify_register

這個方法就是注冊dyld,具體實現之前已經在分析dyld加載流程的時候分析過了。

從_dyld_objc_notify_register方法的注釋中可以得出:

  • 僅供objc運行時使用

  • 注冊處理程序,以便在映射、取消映射和初始化objc圖像時調用

  • dyld將會通過一個包含objc-image-info的鏡像文件的數組回調mapped函數

_dyld_objc_notify_register中的三個參數含義如下:

  • map_images:dyld將image(鏡像文件)加載進內存時,會觸發該函數

  • load_image:dyld初始化image會觸發該函數

  • unmap_image:dyld將image移除時,會觸發該函數

dyld與Objc的關聯

dyld源碼中_dyld_objc_notify_register的實現

libobjc源碼中_dyld_objc_notify_register的調用

結合這兩段代碼可以得出:

  • mapped 等價于 map_images

  • init 等價于 load_images

  • unmapped 等價于 unmap_image

再在dyld源碼中查看registerObjCNotifiers的實現:

可以看到作為參數傳進去的三個函數一一被用來進行了賦值操作,所以會存在以下等價關系:

  • sNotifyObjCMapped == mapped == map_images

  • sNotifyObjCInit == init == load_images

  • sNotifyObjCUnmapped == unmapped == unmap_image

map_images的調用時機

關于load_images的調用時機在講述dyld的加載流程時已經講解過了,在notifySingle方法中,通過sNotifyObjCInit來調用。接下來我們分析一下map_images的調用時機

在dyld中全局搜索sNotifyObjCMapped,可以發現是在notifyBatchPartial方法中調用的

搜索notifyBatchPartial,可以看到它是在registerObjCNotifiers方法中調用的

到這里,我們再來梳理一遍dyld的流程:

  1. 在recursiveInitialization方法中會調用bool hasInitializers = this->doInitialization(context);這個方法是用來判斷image是否已加載。

  2. 同時doInitialization這個方法會調用doImageInit和doModInitFunctions(context),這兩個方法會進入libSystem框架里調用libSystem_initializer方法,最后就會調用_objc_init方法

  3. _objc_init會調用_dyld_objc_notify_register將map_images、load_images、unmap_image傳入dyld方法registerObjCNotifiers

  4. 在registerObjCNotifiers方法中,我們把_dyld_objc_notify_register傳入的map_images賦值給sNotifyObjCMapped,將load_images賦值給sNotifyObjCInit,將unmap_image賦值給sNotifyObjCUnmapped

  5. 在registerObjCNotifiers方法中,我們將傳參賦值后就開始調用notifyBatchPartial()

  6. notifyBatchPartial方法中會調用(*sNotifyObjCMapped)(objcImageCount, paths, mhs);觸發map_images方法

  7. dyld的recursiveInitialization方法在調用完bool hasInitializers = this->doInitialization(context)方法后,會調用notifySingle()方法

  8. 在notifySingle()中會調用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());上面我們將load_images賦值給了sNotifyObjCInit,所以此時就會觸發load_images方法

  9. sNotifyObjCUnmapped會在removeImage方法里觸發,字面理解就是刪除Image(映射的鏡像文件)

所以有以下結論:map_images是先于load_images調用,即先map_images,再load_images

dyld與Objc關聯

結合以上分析和之前對dyld的分析,可以總結出dyld與Objc的關聯就是通過回調完成的:

  • dyld中注冊回調函數,可以理解為 添加觀察者

  • objcdyld注冊,可以理解為發送通知

  • 觸發回調,可以理解為執行通知selector

下面我們來看看map_images、load_images、unmap_image都做了什么

  • map_images:主要是管理文件中和動態庫中所有的符號,即class、protocol、selector、category

  • load_images:加載執行load方法

  • unmap_image: 卸載移除數據

map_images

先說明為什么map_images有&,而load_images沒有:

  • map_images是 引用類型,外界變了,跟著變

  • load_images是值類型,不傳遞值

map_images的作用是將Mach-O中的類信息加載到內存,map_images調用map_images_nolock,map_images_nolock調用_read_images來加載鏡像文件

_read_images

_read_images這個函數的作用主要是加載類信息,即類、分類、協議等

先大致說一下_read_images的流程:

  1. 條件控制進行的一次加載——創建表(類的哈希表)

  2. 修復預編譯階段的@selector的混亂問題

  3. 錯誤混亂的類處理

  4. 修復重映射一些沒有被鏡像文件加載進來的類

  5. 修復一些消息

  6. 當類里面有協議時:readProtocol讀取協議

  7. 修復沒有被加載的協議

  8. 分類處理

  9. 類的加載處理

  10. 沒有被處理的類,優化那些被侵犯的類

現在看不懂沒關系,接下來我們來一個部分一個部分講清楚這個函數做了些什么

創建表

這個部分只有在第一次進入函數時會執行,他會創建一個用來存放類的哈希表,這個表里存放的類是那些不在共享緩存且已命名的類,無論類是否實現,容量是類數量的4/3。

修復預編譯階段的@selector的混亂問題

這段代碼的作用簡單地說可以用一句話概括:把編譯時生成的“假的 selector 地址”,統一替換成運行時真正注冊過的“合法 selector 地址”。

為什么會混亂?

@selector(foo) 編譯器生成的是靜態字符串指針(在 __objc_selrefs 中)。

但在 runtime 調用方法時,必須保證 SEL 是統一注冊過、地址唯一的。

如果不統一注冊,那么同樣的 @selector(foo) 在多個模塊中可能是不同地址,會導致消息發送、isEqual, NSStringFromSelector 等出現 bug。

打一個通俗的比方:

你可以把 Selector (@selector(...)) 想象成你給朋友寫的一封信的收件人地址

  • 編譯器階段寫的是:“張三,某市某區某號”,但這是你隨手寫的,可能不是張三真正的地址。

  • 運行時系統需要確認:“張三”真實住在哪里?(selector 實際對應哪塊內存)

  • 如果你不校正這個地址,信可能寄不到正確的人手中,或者你認為兩個“張三”其實是不同的人。

所以這個方法做的事就是:

  1. 翻出所有你寫的“張三地址”(selector 指針)

  2. 拿去“戶籍局”查一查(通過 sel_registerNameNoLock 注冊)

  3. 如果地址不對,系統會換成正確的地址

錯誤混亂的類處理

這里其實就是把Mach-O文件中的所有類都取出來,再遍歷進行處理。

在readClass之前,cls只是一個地址,在執行完readClass之后,原始的類地址才能被解析為一個有效的類對象。

除此之外,這里還會檢查共享緩存中的類是否已被覆蓋,如果覆蓋就需要重新處理。

對于未來類,如果解析后的類地址發生變化(即if (newCls != cls && newCls)),就記錄到resolvedFutureClasses數組,這些類需要后續非懶加載初始化(立即分配可讀寫數據class_rw_t)。

這段代碼的作用通俗來說,可以想象成你在整理一個雜亂的工具箱,確保所有工具都放在正確的位置,并且能正常使用。具體來說:

  1. 檢查是否有“外人改動” 先看一眼工具箱,確認有沒有人偷偷替換了里面的工具(比如通過插件或特殊配置修改了系統默認的類)。

  2. 逐個翻找工具箱的每個分區 打開每一個小格子(每個程序依賴的庫或框架),看看里面裝的是什么。

  3. 判斷是否需要整理 如果某個分區已經是整理好的(預優化過的類),就跳過不管;如果被改動過,才需要手動處理。

  4. 拿出所有工具的“設計圖紙” 從分區的某個固定位置(Mach-O文件的__objc_classlist段)掏出一疊設計圖,這些圖紙對應著程序里所有定義好的類。

  5. 核對每張設計圖 把圖紙一張張展開檢查:

    • 如果是正常的圖紙(普通類),直接貼上標簽(類名),放到對應位置。

    • 如果發現某張圖紙寫的是“臨時占位符”(未來類),比如之前不知道這個類具體長什么樣,現在終于找到真正的圖紙了,就立刻替換掉占位符,并記錄下來:“這幾個類需要馬上組裝好,不能偷懶”。

  6. 標記需要立刻組裝的工具 把那些替換過占位符的類單獨記在小本本上(resolvedFutureClasses數組),后續要立刻把它們拼裝成完整的工具(分配內存、關聯方法等),而不是等到第一次用的時候才臨時拼裝(懶加載)。

修復重映射一些沒有被鏡像文件加載進來的類

主要是將未映射的ClassSuper Class進行重映射,也就是將編譯時的引用地址依賴的未確定的鏡像基地址修正為運行時實際的地址,確保所有類引用指向正確內存位置

修復一些消息

這個部分是在處理一些歷史遺留的特殊消息發送機制,確保它們能在新系統中正常工作

當類里面有協議時:readProtocol 讀取協議

從源碼和注釋中我們可以看出來,大致分為三步:

第一步,通過NXMapTable *protocol_map = protocols();創建protocol哈希表,表的名稱為protocol_map

第二步,通過_getObjc2ProtocolList 獲取到Mach-O中的靜態段__objc_protolist協議列表,即從編譯器中讀取并初始化protocol

循環遍歷協議列表,通過readProtocol方法將協議添加到protocol_map哈希表中

修復沒被加載的協議

在 Objective-C 的運行時中,協議 @protocol 也會在編譯階段生成引用,在 Mach-O 文件中它們會被放到一個叫 _objc_protorefs (與_objc_protolist不同)的段里。

在運行時加載鏡像(Mach-O 文件)時:某些協議可能在預編譯優化(preoptimized)階段就已經被指向了共享緩存(dyld shared cache)中的協議定義;但是如果鏡像是后來才加載的,比如動態庫或插件(Bundle),這些協議引用可能仍然指向一個未修正的地址(stub 或未來協議)。為了保證協議指針指向真正的定義,就要在這里修正它們。

這段話看起來復雜,可以理解成一句話:「在協議的真實定義還沒有被加載之前,其他地方就已經引用了它,所以在協議真正加載之后,必須把原來的引用修正為真實地址。」

分類處理

這段代碼主要是處理分類,需要在分類初始化并將數據加載到類后才執行,對于運行時出現的分類,將分類的發現推遲到對_dyld_objc_notify_register的調用完成后的第一個load_images調用為止。

這是為了解決啟動時加載過早的問題:

  • load_images() 是 Runtime 加載新 Mach-O 鏡像(如動態庫、插件)時的回調;

  • didInitialAttachCategories 是一個標記,代表初次附加分類是否完成;

  • Runtime 只有在收到 _dyld_objc_notify_register() 的通知后,才開始做真正的分類附加;

  • 因為某些分類定義可能在系統框架里過早加載,如果這時就處理分類可能錯過關聯主類(主類還沒加載)。

所以:

👉 為了避免“分類加載時主類還沒準備好”的問題,分類的附加操作被延后到

  • _dyld_objc_notify_register() 調用完;

  • 并且是在 第一個 load_images() 時執行;

  • 保證主類和分類都已在內存中,分類才能正確附加上。

總結成一句話就是:Runtime 為了保證分類附加的時機正確,會延遲處理一些分類,直到確保主類已經加載,分類數據也加載完成之后,再統一合并附加。

那么問題來了:既然這段代碼是在map_images()這個函數里的,那怎么會在load_images()之后執行呢?

  • 答案是當調用load_images()時,系統底層會調用 load_images() → 然后再次調用 map_images(),加載這個新鏡像。

那是不是意味著第一次調用map_images時,對于分類沒有進行任何操作呢?

  • 答案是確實如此:分類是 編譯時寫入 Mach-O 文件的靜態結構但只有在運行時才會“附加到主類上”,這個過程我們稱為 分類的附加(attach)

  • map_images() 第一次執行時,不處理分類;只有在 Runtime 初始化完成后,再次調用 map_images() 時,才會在內部調用 load_categories_nolock() 去處理分類。

類的加載處理

這一段就是實現類的加載處理,實現非懶加載類

  • 通過_getObjc2NonlazyClassList獲取Mach-O的靜態段__objc_nlclslist非懶加載類表(這是編譯器標記的那些類,它們要在程序啟動時就立刻初始化)

  • 通過addClassTableEntry將非懶加載類插入類表,存儲到內存,如果已經添加就不會載添加,需要確保整個結構都被添加

  • 通過realizeClassWithoutSwift實現當前的類,因為前面 ③中的readClass讀取到內存的僅僅只有地址+名稱,類的data數據并沒有加載出來

  • ?? 真正實現這個類,realizeClassWithoutSwift這一步很關鍵:

    • 把類的元信息(ro -> rw 等)建立起來;

    • 設置方法列表、屬性、協議;

    • 準備好實例大小、布局;

    • 做好準備以便可以創建對象;

    • ?? 如果類實現了 +load,此時 load 方法也會被調用。(錯誤,load在load_images階段調用)

關于懶加載類和非懶加載類:

如果實現了+load方法,就是非懶加載類,否則就是懶加載類

為什么實現load方法就會變成非懶加載類?

因為 +load 是在類加載后立即執行的,如果類沒有先實現(realize),就無法安全執行 +load,也就可能錯過一些初始化邏輯,比如方法交換(swizzling)等。(load方法會在load_images 調用)

懶加載類在什么時候調用?:

只有在第一次使用(例如 alloc/init)時才會加載。節省啟動性能。

沒有被處理的類,優化那些被侵犯的類

這一步負責處理在運行時動態解析的“未來類”(Future Classes),并確保它們被正確初始化

初始化未來類,如果是調試模式,強制初始化所有懶加載類(即使未被使用)

這里有一個誤區:懶加載類VS未來類

未來類與懶加載類是兩個完全獨立的概念,并且未來類不可能是懶加載類,懶加載類不可能是未來類。

關鍵步驟

在上述流程中,有兩個函數非常重要,分別是 readClass和realizeClassWithoutSwift

readClass

首先是readClass,readClass主要是讀取類,在未調用該方法前,cls只是一個地址,執行該方法后,cls是類的名稱

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{const char *mangledName = cls->nonlazyMangledName();//獲取類的名字
//    printf("%s -哎呦不錯!- %s \n",__func__,mangledName);// tcj 玩的 ----如果想進入自定義的類,自己加一個判斷
//    const char *TCJPersonName = "TCJPerson";
//    if (strcmp(mangledName, TCJPersonName) == 0) {
//        auto cj_ro = (const class_ro_t *)cls->data();
//        printf("%s -- 哎呦不錯!--%s\n", __func__,mangledName);
//    }//當前類的父類中若有丟失的weak-linked類,則返回nilif (missingWeakSuperclass(cls)) {// No superclass (probably weak-linked). // Disavow any knowledge of this subclass.if (PrintConnecting) {_objc_inform("CLASS: IGNORING class '%s' with ""missing weak-linked superclass", cls->nameForLogging());}addRemappedClass(cls, nil);cls->setSuperclass(nil);return nil;}cls->fixupBackwardDeployingStableSwift();//判斷是不是后期要處理的類//正常情況下,不會走到popFutureNamedClass,因為這是專門針對未來待處理的類的操作//通過斷點調試,不會走到if流程里面,因此也不會對ro、rw進行操作Class replacing = nil;if (mangledName != nullptr) {if (Class newCls = popFutureNamedClass(mangledName)) {// This name was previously allocated as a future class.// Copy objc_class to future class's struct.// Preserve future's rw data block.if (newCls->isAnySwift()) {_objc_fatal("Can't complete future class request for '%s' ""because the real class is too big.",cls->nameForLogging());}//讀取mach-o的data,設置ro、rw//經過調試,并不會走到這里class_rw_t *rw = newCls->data();const class_ro_t *old_ro = rw->ro();memcpy(newCls, cls, sizeof(objc_class));// Manually set address-discriminated ptrauthed fields// so that newCls gets the correct signatures.newCls->setSuperclass(cls->getSuperclass());newCls->initIsa(cls->getIsa());rw->set_ro((class_ro_t *)newCls->data());newCls->setData(rw);freeIfMutable((char *)old_ro->getName());free((void *)old_ro);addRemappedClass(cls, newCls);replacing = cls;cls = newCls;}}//判斷是否類是否已經加載到內存if (headerIsPreoptimized  &&  !replacing) {// class list built in shared cache// fixme strict assert doesn't work because of duplicates// ASSERT(cls == getClass(name));ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));} else {if (mangledName) { //some Swift generic classes can lazily generate their namesaddNamedClass(cls, mangledName, replacing);//加載共享緩存中的類} else {Class meta = cls->ISA();const class_ro_t *metaRO = meta->bits.safe_ro();ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");}addClassTableEntry(cls);//插入表,即相當于從mach-O文件 讀取到 內存 中}// for future reference: shared cache never contains MH_BUNDLEsif (headerIsBundle) {cls->data()->flags |= RO_FROM_BUNDLE;cls->ISA()->data()->flags |= RO_FROM_BUNDLE;}return cls;
}

readClass的流程主要分為以下幾步:

① 通過mangledName獲取類的名字

② 當前類的父類中若有丟失的weak-linked類(weak-linked 類(弱鏈接類)是一種特殊的類引用方式,允許應用在編譯時引用某個類,但在運行時才檢查該類是否實際存在),則返回nil

③ 通過addNamedClass將當前類添加到已經創建好的gdb_objc_realized_classes哈希表(名稱映射表),該表用于存放所有類

④ 通過addClassTableEntry,將已經可用的類結構(已讀入內存,已具備基本結構(isa、ro/rw 等))添加到allocatedClasses表(類哈希表),這個表在_objc_init中的runtime_init就初始化創建了

綜上所述,readClass的主要作用就是將Mach-O中的類讀取到內存,即插入表中將 Mach-O 文件中解析出的 class 地址轉換為真正可用的類對象,并放入類表中,為后續使用做好準備),但是目前的類僅有兩個信息:地址以及名稱,而mach-O的其中的data數據還未讀取出來

realizeClassWithoutSwift

realizeClassWithoutSwift方法主要作用是實現類,將類的data數據加載到內存中,主要有以下幾部分操作:

  • ① 讀取data數據,并設置ro、rw

  • ② 遞歸調用realizeClassWithoutSwift完善繼承鏈

  • ③ 通過methodizeClass方法化類

讀取 data 數據,并設置 ro、rw

這一步負責讀取classdata數據,并將其強轉為ro,以及rw初始化ro拷貝一份到rw中的ro

  • ro 表示 readOnly,即只讀,其在編譯時就已經確定了內存,包含類名稱、方法、協議和實例變量的信息,由于是只讀的,所以屬于Clean Memory,而Clean Memory是指加載后不會發生更改的內存

  • rw 表示 readWrite,即可讀可寫,由于其動態性,可能會往類中添加屬性、方法、添加協議,但其實在rw中只有10%的類真正的更改了它們的方法,所以有了rwe,即類的額外信息。對于那些確實需要額外信息的類,分配一塊 rwe這是一個rw的可選擴展字段,不需要額外信息的類就不會分配 rwe),并將其滑入類中供其使用。其中rw就屬于dirty memory,而 dirty memory是指在進程運行時會發生更改的內存,類結構一經使用就會變成 ditry memory,因為運行時會向它寫入新數據,例如創建一個新的方法緩存,并從類中指向它

遞歸調用 realizeClassWithoutSwift 完善 繼承鏈

遞歸調用realizeClassWithoutSwift完善繼承鏈,并設置當前類、父類、元類的rw

  • 遞歸調用 realizeClassWithoutSwift設置父類、元類

  • 設置父類和元類的isa指向

  • 通過addSubclassaddRootClass設置父子的雙向鏈表指向關系,即父類中可以找到子類,子類中可以找到父類

realizeClassWithoutSwift遞歸調用時,isa找到根元類之后,根元類的isa是指向自己,并不會返回nil,所以有以下遞歸終止條件,其目的是保證類只加載一次

  • 如果類不存在,則返回nil

  • 如果類已經實現,則直接返回cls

通過 methodizeClass 方法化類

該方法會:

  1. ro 中提取類本身的方法、屬性、協議,存入 rw 的動態列表(如 methodspropertiesprotocols)。

  2. 從運行時獲取分類的方法、屬性、協議,合并到 rw 的對應列表中。

  3. 確保 rw 包含所有可訪問的成員,供運行時動態查詢。

🙋問題來了:之前已經用ro給rw賦過值了,為什么還要再給rw寫入方法、屬性和協議呢?

🔑原因是當我們對rw賦值后:

  • 此時 rw 僅包含 ro只讀數據副本,但未處理動態數據(如分類)。

  • rw 的方法列表、屬性列表等動態數據字段(如 methodspropertiesprotocols)尚未填充。

methodizeClass

關于methodizeClass,它的實現源碼如下:

static void methodizeClass(Class cls, Class previously)
{runtimeLock.assertLocked();
?bool isMeta = cls->isMetaClass();auto rw = cls->data();auto ro = rw->ro();auto rwe = rw->ext();
?// Methodizing for the first timeif (PrintConnecting) {_objc_inform("CLASS: methodizing class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : "");}
?// Install methods and properties that the class implements itself.method_list_t *list = ro->baseMethods();if (list) {prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);if (rwe) rwe->methods.attachLists(&list, 1);}
?property_list_t *proplist = ro->baseProperties;if (rwe && proplist) {rwe->properties.attachLists(&proplist, 1);}
?protocol_list_t *protolist = ro->baseProtocols;if (rwe && protolist) {rwe->protocols.attachLists(&protolist, 1);}
?// Root classes get bonus method implementations if they don't have // them already. These apply before category replacements.if (cls->isRootMetaclass()) {// root metaclassaddMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);}
?// Attach categories.if (previously) {if (isMeta) {objc::unattachedCategories.attachToClass(cls, previously,ATTACH_METACLASS);} else {// When a class relocates, categories with class methods// may be registered on the class itself rather than on// the metaclass. Tell attachToClass to look for those.objc::unattachedCategories.attachToClass(cls, previously,ATTACH_CLASS_AND_METACLASS);}}objc::unattachedCategories.attachToClass(cls, cls,isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
?
#if DEBUG// Debug: sanity-check all SELs; log method list contentsfor (const auto& meth : rw->methods()) {if (PrintConnecting) {_objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', cls->nameForLogging(), sel_getName(meth.name()));}ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());}
#endif
}

就像之前所說,methodizeClass主要就是兩步:

  • 屬性列表、方法列表、協議列表等貼到rwe

  • 附加分類中的方法

rwe的邏輯

方法列表加入rwe的邏輯如下:

  • 獲取robaseMethods

  • 通過prepareMethodLists方法排序

  • rwe進行處理即通過attachLists插入

prepareMethodLists內部通過fixupMethodList方法排序,排序的邏輯是根據selector address排序

attachToClass方法

在方法化類的方法methodizeClass中,還用到了幾個比較重要的方法,比如attachToClass方法,它進行的操作是將分類添加到主類中

attachToClass中的外部循環是找到一個分類就會進到attachCategories一次,即找一個就循環一次,在這個方法里可以確定分類和對應主類,在attachCategories方法中會進行數據、協議、方法的添加

attachCategories方法
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags)
{if (slowpath(PrintReplacedMethods)) {printReplacements(cls, cats_list, cats_count);}if (slowpath(PrintConnecting)) {_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");}
?/** Only a few classes have more than 64 categories during launch.* This uses a little stack, and avoids malloc.** Categories must be added in the proper order, which is back* to front. To do that with the chunking, we iterate cats_list* from front to back, build up the local buffers backwards,* and call attachLists on the chunks. attachLists prepends the* lists, so the final result is in the expected order.*/constexpr uint32_t ATTACH_BUFSIZ = 64;method_list_t ? *mlists[ATTACH_BUFSIZ];property_list_t *proplists[ATTACH_BUFSIZ];protocol_list_t *protolists[ATTACH_BUFSIZ];
?uint32_t mcount = 0;uint32_t propcount = 0;uint32_t protocount = 0;bool fromBundle = NO;bool isMeta = (flags & ATTACH_METACLASS);auto rwe = cls->data()->extAllocIfNeeded();
?for (uint32_t i = 0; i < cats_count; i++) {auto& entry = cats_list[i];
?method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {if (mcount == ATTACH_BUFSIZ) {prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);rwe->methods.attachLists(mlists, mcount);mcount = 0;}mlists[ATTACH_BUFSIZ - ++mcount] = mlist;fromBundle |= entry.hi->isBundle();}
?property_list_t *proplist =entry.cat->propertiesForMeta(isMeta, entry.hi);if (proplist) {if (propcount == ATTACH_BUFSIZ) {rwe->properties.attachLists(proplists, propcount);propcount = 0;}proplists[ATTACH_BUFSIZ - ++propcount] = proplist;}
?protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);if (protolist) {if (protocount == ATTACH_BUFSIZ) {rwe->protocols.attachLists(protolists, protocount);protocount = 0;}protolists[ATTACH_BUFSIZ - ++protocount] = protolist;}}
?if (mcount > 0) {prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,NO, fromBundle, __func__);rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);if (flags & ATTACH_EXISTING) {flushCaches(cls, __func__, [](Class c){// constant caches have been dealt with in prepareMethodLists// if the class still is constant here, it's fine to keepreturn !c->cache.isConstantOptimizedCache();});}}
?rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
?rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

attachCategories 函數負責將分類(Category)中的方法、屬性和協議合并到目標類中,確保分類內容在運行時生效。

auto rwe = cls->data()->extAllocIfNeeded();這一行是第一步,負責進行rwe的創建(因為現在要往本類添加屬性、方法、協議等,即對原來的 clean memory要進行處理了,所以需要先對rwe進行初始化)

進入extAllocIfNeeded方法的源碼實現,判斷rwe是否存在,如果存在則直接獲取,如果不存在則開辟

進入extAlloc源碼實現,即對rwe 0-1的過程,在此過程中,就將本類的data數據加載進去了

需要注意的幾個點是:

  • 逆序存儲正序附加:通過倒序填充緩沖區,正序附加,確保后加載的分類內容優先生效。

  • 分批處理:每滿64個分類處理一次,平衡性能與內存占用。

  • 方法覆蓋:分類方法插入到類方法列表頭部,實現“后編譯的分類覆蓋先編譯的類或分類方法”。

attachLists方法

注意到在添加分類的方法時,是通過attachLists方法插入數據的,并且不止方法,屬性和協議都是通過attachLists方法插入數據的。

其實,方法列表和屬性列表都繼承自entsize_list_tt,協議則是類似entsize_list_tt實現,都是二維數組。

那么我們來看看attachLists方法的實現:

從源碼中可以看見,插入表存在三種情況:

  • 多對多: 如果當前調用attachListslist_array_tt二維數組中有多個一維數組

    • 通過malloc根據新的容量大小,開辟一個數組,類型是 array_t,通過array()獲取

    • 倒序遍歷把原來的數據移動到容器的末尾

    • 遍歷新的數據移動到容器的起始位置

  • 0對1: 如果調用attachListslist_array_tt二維數組為空且新增大小數目為 1

    • 直接賦值addedList的第一個list

  • 1對多: 如果當前調用attachListslist_array_tt二維數組只有一個一維數組

    • 通過malloc開辟一個容量和大小的集合,類型是 array_t,即創建一個數組,放到array中,通過array()獲取

    • 由于只有一個一維數組,所以直接賦值到新Array的最后一個位置

    • 循環遍歷從數組起始位置存入新的list,其中array()->lists 表示首位元素位置

這就是為什么子類可以重寫父類的方法,也是為什么分類可以重寫類的方法,要加一個newlist的目的是由于要使用這個newlist中的方法,這個newlist對于用戶的價值要高,即優先調用

總結一下:attachLists方法主要是將分類的數據加載到rwe

  • 首先加載本類的data數據,此時的rwe沒有數據為空,走0對1流程

  • 加入一個分類時,此時的rwe僅有一個list,即本類的list,走1對多流程

  • 加入一個分類時,此時的rwe中有兩個list,即本類+分類的list,走多對多流程

分類

從上面的內容中,我們已經知道怎么把分類加到主類上了,接下來我們從分類的角度來分析一下分類

分類的本質

首先探索一下分類的本質。TCJPerson定義分類TCJ

用Clang進行反編譯得到C++代碼,可以看到分類是存儲在MachO文件的__DATA段的__objc_catlist中,還可以看到TCJPerson分類的結構。

可以發現TCJPerson改為_CATEGORY_TCJPerson_,并且被_category_t修飾,可以看到_category_t的結構

可以看見_category_t是個結構體,里面保存有名稱(類的名字)、cls、對象方法列表、類方法列表、協議、屬性

為什么分類的方法要將實例方法和類方法分開存呢?

  • 分類有兩個方法列表是因為分類是沒有元分類的,分類的方法是在運行時通過attachToClass插入到class

查看方法列表的反編譯代碼

可以看到有三個對象方法和一個類方法,格式為:sel+簽名+地址,和method_t結構體一樣

再看看屬性

發現存在屬性的變量名但是沒有對應的set和get方法

分類的加載

在之前的部分提到了類有懶加載類和非懶加載類,二者的加載時機不同,那么如果涉及到分類,又是何時進行加載呢?

首先先回顧一下分類是如何進行加載的:

  • 分類數據在attachCategories方法中加載,分類的加載遵循這樣一個規則——越晚加進來,越在前面

  • 在methodizeClass中,通過attatchToClass方法將分類數據添加到主類。methodizeClass方法中類的數據和分類數據分開處理,因為編譯階段已經確定好了方法的歸屬位置(即實例方法存儲在中,類方法存儲在元類中),而分類是后面才加進來的。

分類加載時機

關于分類的加載時機,有一條規律:只要有一個分類是非懶加載分類,那么所有的分類都會被標記位非懶加載分類。

因為加載一個分類,意味著類已經開辟了rwe,那么就不會再次懶加載,重新去處理主類了。

根據類和分類是否實現+load方法,我們可以得到4種情況:

  • 非懶加載類+非懶加載分類:類在read_images中加載,而分類數據如之前所述在第一次進入read_images時,不會加載分類數據,所以此時無法合并分類。接著會再調用一次_load_images,隨后再次調用map_images,這時再運行到read_images時就可以成功methodizeClass,合并分類。

  • 非懶加載類與懶加載分類:類會在read_images中加載,而分類不會像非懶加載分類一樣在read_images時合并到主類,而是會被暫時加入 _unattachedCategories 列表中,等待后續時機觸發合并

  • 懶加載類與懶加載分類:第一次發送消息給類時,類會實現并且分類會合并到主類上

  • 懶加載類與非懶加載分類:懶加載類 + 非懶加載分類的數據加載,只要分類實現了load,會迫使主類提前加載,即主類強行轉換為非懶加載類樣式

在這四種情況中,分類的數據都是在load_images調用map_images時read_class()來加載到內存的,區別只在于類何時實現以及分類何時附加到主類

load_images

load_images方法的主要作用是加載鏡像文件,其中有兩個比較重要的方法:prepare_load_methods(加載) 和 call_load_methods(調用)

Load_images源碼如下:

這里的加載所有分類,其實是在分類表中遍歷,檢查分類的主類是否已經實現,如果已經實現就把分類合并上去,否則放到 _unattachedCategories中,這一步其實正是之前提到的對懶加載分類的處理。懶加載類實現了之后,在這里就會把分類合并上去

prepare_load_methods實現

如圖所示,這個方法會把類及其父類和分類的load方法都放到數組中。

這里有兩個方法:schedule_class_load、add_category_to_loadable_list

schedule_class_load方法

關于schedule_class_load方法:

這個方法根據類的繼承鏈遞歸調用獲取load,直到cls不存在才結束遞歸,這樣做的目的是為了確保父類的load優先加載

add_class_to_loadable_list:

在schedule_class_load方法中調用了這個方法,此方法主要是將load方法cls類名一起加到loadable_classes表中

getLoadMethod:

在add_class_to_loadable_list方法中調用了這個方法

這個方法主要就是用來獲取方法列表中sel為load的方法

add_category_to_loadable_list

主要是獲取所有的非懶加載分類中的load方法,將分類名+load方法加入表loadable_categories

call_load_methods

這個方法有三步操作:

  • 反復調用類的+load,直到不再有

  • 調用一次分類的+load

  • 如果有類或更多未嘗試的分類,則運行更多的+load

方法的實現中主要就是兩個方法:call_class_loads和 call_category_loads

call_class_loads主要加載類的load方法,而call_category_loads主要是加載一次分類的load方法

initalize分析

關于initialize,它通常是在某個類接收到第一條消息之前調用,它的調用鏈是lookUpImpOrForward->realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass

initializeNonMetaClass遞歸調用父類initialize,然后調用callInitialize

void initializeNonMetaClass(Class cls)
{ASSERT(!cls->isMetaClass());
?Class supercls;bool reallyInitialize = NO;
?// Make sure super is done initializing BEFORE beginning to initialize cls.// See note about deadlock above.supercls = cls->getSuperclass();if (supercls ?&& ?!supercls->isInitialized()) {initializeNonMetaClass(supercls);}// Try to atomically set CLS_INITIALIZING.SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;{monitor_locker_t lock(classInitLock);if (!cls->isInitialized() && !cls->isInitializing()) {cls->setInitializing();reallyInitialize = YES;
?// Grab a copy of the will-initialize funcs with the lock held.localWillInitializeFuncs.initFrom(willInitializeFuncs);}}if (reallyInitialize) {// We successfully set the CLS_INITIALIZING bit. Initialize the class.// Record that we're initializing this class so we can message it._setThisThreadIsInitializingClass(cls);
?if (MultithreadedForkChild) {// LOL JK we don't really call +initialize methods after fork().performForkChildInitialize(cls, supercls);return;}for (auto callback : localWillInitializeFuncs)callback.f(callback.context, cls);
?// Send the +initialize message.// Note that +initialize is sent to the superclass (again) if // this class doesn't implement +initialize. 2157218if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",objc_thread_self(), cls->nameForLogging());}
?// Exceptions: A +initialize call that throws an exception // is deemed to be a complete and successful +initialize.//// Only __OBJC2__ adds these handlers. !__OBJC2__ has a// bootstrapping problem of this versus CF's call to// objc_exception_set_functions().
#if __OBJC2__@try
#endif{callInitialize(cls);
?if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",objc_thread_self(), cls->nameForLogging());}}
#if __OBJC2__@catch (...) {if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: +[%s initialize] ""threw an exception",objc_thread_self(), cls->nameForLogging());}@throw;}@finally
#endif{// Done initializing.lockAndFinishInitializing(cls, supercls);}return;}else if (cls->isInitializing()) {// We couldn't set INITIALIZING because INITIALIZING was already set.// If this thread set it earlier, continue normally.// If some other thread set it, block until initialize is done.// It's ok if INITIALIZING changes to INITIALIZED while we're here, // ? because we safely check for INITIALIZED inside the lock // ? before blocking.if (_thisThreadIsInitializingClass(cls)) {return;} else if (!MultithreadedForkChild) {waitForInitializeToComplete(cls);return;} else {// We're on the child side of fork(), facing a class that// was initializing by some other thread when fork() was called._setThisThreadIsInitializingClass(cls);performForkChildInitialize(cls, supercls);}}else if (cls->isInitialized()) {// Set CLS_INITIALIZING failed because someone else already // ? initialized the class. Continue normally.// NOTE this check must come AFTER the ISINITIALIZING case.// Otherwise: Another thread is initializing this class. ISINITIALIZED // ? is false. Skip this clause. Then the other thread finishes // ? initialization and sets INITIALIZING=no and INITIALIZED=yes. // ? Skip the ISINITIALIZING clause. Die horribly.return;}else {// We shouldn't be here. _objc_fatal("thread-safe class init in objc runtime is buggy!");}
}

callInitialize是一個普通的消息發送

關于initialize:

  • initialize在類或者其子類的第一個方法被調用前(發送消息前)調用

  • 只在類中添加initialize但不使用的情況下,是不會調用initialize

  • 父類的initialize方法會比子類先執行

  • 當子類未實現initialize方法時,會調用父類initialize方法;子類實現initialize方法時,會覆蓋父類initialize方法

  • 當有多個分類都實現了initialize方法,會覆蓋類中的方法,只執行一個(會執行最后被加載到內存中的分類的方法)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90856.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90856.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90856.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

大模型算法面試筆記——常用優化器SGD,Momentum,Adagrad,RMSProp,Adam

常用參數&#xff1a;ttt-步數&#xff0c;α\alphaα-學習率&#xff0c;θ\thetaθ-參數&#xff0c;f(θ)f(\theta)f(θ)-目標函數&#xff0c;gtg_tgt?-梯度&#xff0c;β1\beta_1β1?-一階矩衰減系數&#xff0c;通常取0.9&#xff0c;β2\beta_2β2?-二階矩&#xff…

【計算機畢業設計】基于SSM的小型超市管理系統+LW

博主介紹&#xff1a;?全網粉絲3W,csdn特邀作者、CSDN新星計劃導師、Java領域優質創作者,掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域和學生畢業項目實戰,高校老師/講師/同行前輩交流? 技術范圍&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、…

火線、零線、地線

我們可以用 “水流” 來比喻 “電流”&#xff0c;這樣理解起來會很簡單&#xff1a;想象一下你家的電路就像一個 “閉合的水循環系統”&#xff1a;&#x1f525; 1. 火線 (Live Wire) - 好比 “進水管的高壓端”作用&#xff1a; 從發電廠或變壓器輸送 高壓電 到你家的插座或…

基于Vue3.0+Express的前后端分離的任務清單管理系統

文章目錄 一、前端 0、項目介紹 0.1 主要功能介紹 0.2 UI展示 1、首頁 2、待辦事項管理 2.1 添加待辦事項 2.2 展示待辦事項 2.3 修改待辦事項 2.4 刪除待辦事項 3、分類管理 3.1 添加分類 3.2 展示分類 3.3 修改分類 3.4 刪除分類 4、團隊成員管理 4.1 展示團隊成員 二、后端 …

基于單片機智能交通燈設計

傳送門 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目速選一覽表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目功能速覽 概述 隨著城市化進程的加快&#xff0c;城市交通流量日益增大&#xff0c;傳統的固定配時交通燈已難以…

Datawhale AI夏令營——列車信息智能問答——科大訊飛AI大賽(基于結構化數據的用戶意圖理解和知識問答挑戰賽)

前言 坐火車的你&#xff0c;遇到過這樣的場景嗎&#xff1f; 一次又一次查車次信息&#xff1f;趕火車狂奔&#xff0c;找檢票口找到懷疑人生…想查“最早到北京的車”&#xff1f;時刻表翻到眼瞎&#xff01;列車晚點&#xff1f;新出發時間算到腦殼疼&#xff01; 我們這次將…

UVA11990 ``Dynamic‘‘ Inversion

UVA11990 Dynamic Inversion題目鏈接題意輸入格式輸出格式分析CDQ分治嵌套&#xff08;樹狀數組套BST&#xff09;分塊k-D Tree題目鏈接 UVA11990 Dynamic’’ Inversion 題意 給一個 1~n 的排列A&#xff0c;要求按照某種順序刪除一些數&#xff08;其他數順序不變&#xff0…

銀河麒麟“安裝器”安裝方法

書接上回&#xff1a;銀河麒麟安裝軟件商店方法-CSDN博客 過了幾天發現當時一不小心把系統自帶的“安裝器”軟件也卸載掉了&#xff0c;導致現在deb文件只能通過命令行安裝&#xff0c;尋思這可不行&#xff0c;就想一下應該怎么安裝。 首先&#xff0c;為了確認一下安裝器的…

計算機畢設分享-基于SpringBoot的健身房管理系統(開題報告+前后端源碼+Lun文+開發文檔+數據庫設計文檔)

基于SpringBoot的健身房管理系統分享一套完整的基于SpringBoot的健身房管理系統畢業設計&#xff08;開題報告完整前后端源碼Lun文 開發文檔數據庫設計文檔&#xff09;系統分為三個角色功能如下&#xff1a;用戶功能需求描述管理員功能需求描述教練功能需求描述開題報告系統功…

代碼審計與web安全選擇題1

軟件供應鏈安全的基礎是&#xff08; &#xff09;A.完善的需求分析B.源代碼安全C.滲透測試D.軟件測試參考答案&#xff1a;B保證源代碼安全的主要措施包括&#xff08; &#xff09;A.開發工具和環境的安全B.代碼安全C.滲透測試D.代碼審計E.軟件的說明文檔完整參考…

python基本數據類型 數據類型轉換 數字 菜鳥教程筆記

python基本數據類型 數據類型轉換 數字 菜鳥教程筆記 1.基本數據類型 Python 中的變量不需要聲明。每個變量在使用前都必須賦值&#xff0c;變量賦值以后該變量才會被創建。 在 Python 中&#xff0c;變量就是變量&#xff0c;它沒有類型&#xff0c;我們所說的"類型"…

USRP X410 X440 5G及未來通信技術的非地面網絡(NTN)

概述 在本白皮書中&#xff0c;我們將介紹NTN的現狀、正處于探索階段的一些新應用&#xff0c;以及最重要的一點&#xff0c;我們需要克服哪些技術挑戰才能讓這個市場充滿活力。最后&#xff0c;我們將概述為實現實用高效的測試&#xff0c;NI圍繞NTN所做的努力&#xff0c;該測…

基于SpringBoot+Vue的電腦維修管理系統(WebSocket實時聊天、Echarts圖形化分析)

“ &#x1f388;系統亮點&#xff1a;WebSocket實時聊天、Echarts圖形化分析”01系統開發工具與環境搭建—前后端分離架構項目架構&#xff1a;B/S架構運行環境&#xff1a;win10/win11、jdk17小程序端&#xff1a;技術&#xff1a;Uniapp&#xff1b;UI庫&#xff1a;colorUI…

2025.7.28總結

今天真有點小煩&#xff0c;工作有些不太順利&#xff0c;我是真沒想到&#xff0c;阻塞我工作開展得竟然是我的主管。當初需求澄清的時候&#xff0c;開發說要申請一個便攜&#xff0c;我當時申請的時候也跟主管說了&#xff0c;需求測試的時候要使用到&#xff0c;但主管要我…

DBA常用數據庫查詢語句

1 數據庫信息 1.1 數據庫概要 select a.name "DB Name",e.global_name "Global Name",c.host_name "Host Name",c.instance_name "Instance Name" ,DECODE(c.logins,RESTRICTED,YES,NO) "Restricted Mode",a.log_mode &quo…

【c++深入系列】:萬字詳解priority_queue(附模擬實現的源碼)

&#x1f525; 本文專欄&#xff1a;c &#x1f338;作者主頁&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客勵志語錄&#xff1a; 真正的強大&#xff0c;不是從不跌倒&#xff0c;而是每次跌倒后都能笑著站起來 ★★★ 本文前置知識&#xff1a; 模版 引入 那么pri…

分享一個腳本,從mysql導出數據csv到hdfs臨時目錄

想從mysql導出一個表到csv文件&#xff0c;然后上傳到hdfs&#xff0c;開始使用sqoop&#xff0c;結果各種問題頻出&#xff1a; https://blog.csdn.net/weixin_45357522/article/details/149498030 https://blog.csdn.net/weixin_45357522/article/details/149449413 特別是那…

OpenLayers 綜合案例-區域掩膜

看過的知識不等于學會。唯有用心總結、系統記錄&#xff0c;并通過溫故知新反復實踐&#xff0c;才能真正掌握一二 作為一名摸爬滾打三年的前端開發&#xff0c;開源社區給了我飯碗&#xff0c;我也將所學的知識體系回饋給大家&#xff0c;助你少走彎路&#xff01; OpenLayers…

30天打牢數模基礎-神經網絡基礎講解

一、代碼說明本代碼基于模擬房價數據集&#xff0c;使用scikit-learn庫中的MLPRegressor&#xff08;多層感知器回歸&#xff09;實現神經網絡模型&#xff0c;解決房價預測問題。代碼邏輯清晰&#xff0c;適合數模小白入門&#xff0c;包含數據預處理、模型構建、訓練評估、新…

Linux應用開發基礎知識——LInux學習FreeType編程(七)

目錄 一、使用freetype 顯示一個文字 二、使用 freetype 顯示一行文字 1. 了解笛卡爾坐標系 2. 每個字符的大小可能不同 3. 怎么在指定位置顯示一行文字 4. freetype 的幾個重要數據結構 4.1、FT_Library結構體 4.2、FT_Face結構體 4.3、FT_GlyphSlot結構體 4.4、FT_G…