2020年阿里、字節:一套高效的iOS面試題(一)


runtime相關(參考源碼objc-runtimeobjc4)

結構模型

1、介紹下runtime的內存模型(isa、對象、類、metaclass、結構體的存儲信息等)

對象:OC中的對象指向的是一個objc_object指針類型,typedef struct objc_object *id;從它的結構體中可以看出,它包括一個isa指針,指向的是這個對象的類對象,一個對象實例就是通過這個isa找到它自己的Class,而這個Class中存儲的就是這個實例的方法列表、屬性列表、成員變量列表等相關信息的。

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

類:在OC中的類是用Class來表示的,實際上它指向的是一個objc_class的指針類型,typedef struct objc_class *Class;對應的結構體如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

}

從結構體中定義的變量可知,OC的Class類型包括如下數據(即:元數據metadata):super_class(父類類對象);name(類對象的名稱);version、info(版本和相關信息);instance_size(實例內存大小);ivars(實例變量列表);methodLists(方法列表);cache(緩存);protocols(實現的協議列表);

當然也包括一個isa指針,這說明Class也是一個對象類型,所以我們稱之為類對象,這里的isa指向的是元類對象(metaclass),元類中保存了創建類對象(Class)的類方法的全部信息。

以下圖中可以清楚的了解到OC對象、類、元類之間的關系

從圖中可知,最終的基類(NSObject)的元類對象isa指向的是自己本身,從而形成一個閉環。

元類(Meta Class):是一個類對象的類,即:Class的類,這里保存了類方法等相關信息。

我們再看一下類對象中存儲的方法、屬性、成員變量等信息的結構體

objc_ivar_list:存儲了類的成員變量,可以通過object_getIvar或class_copyIvarList獲取;另外這兩個方法是用來獲取類的屬性列表的class_getProperty和class_copyPropertyList,屬性和成員變量是有區別的。

struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
} 

objc_method_list:存儲了類的方法列表,可以通過class_copyMethodList獲取。

結構體如下:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
} 

objc_protocol_list:儲存了類的協議列表,可以通過class_copyProtocolList獲取。

結構體如下:

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};

 

 

2、為什么要設計metaclass

metaclass代表的是類對象的對象,它存儲了類的類方法,它的目的是將實例和類的相關方法列表以及構建信息區分開來,方便各司其職,符合單一職責設計原則。

其實這里涉及到了關於面向對象設計的一些東西,具體可以參考這篇文章

 

3、class_copyIvarList & class_copyPropertyList區別

class_copyIvarList:獲取的是類的成員變量列表,即:@interface{中聲明的變量}

class_copyPropertyList:獲取的是類的屬性列表,即:通過@property聲明的屬性

 

4、class_rw_t 和 class_ro_t 的區別

class_rw_t:代表的是可讀寫的內存區,這塊區域中存儲的數據是可以更改的。

class_ro_t:代表的是只讀的內存區,這塊區域中存儲的數據是不可以更改的。

OC對象中存儲的屬性、方法、遵循的協議數據其實被存儲在這兩塊兒內存區域的,而我們通過runtime動態修改類的方法時,是修改在class_rw_t區域中存儲的方法列表。

參考這篇文章

 

5、category如何被加載的,兩個category的load方法的加載順序,兩個category的同名方法的加載順序

category的加載是在運行時發生的,加載過程是,把category的實例方法、屬性、協議添加到類對象上。把category的類方法、屬性、協議添加到metaclass上。

category的load方法執行順序是根據類的編譯順序決定的,即:xcode中的Build Phases中的Compile Sources中的文件從上到下的順序加載的。

category並不會替換掉同名的方法的,也就是說如果 category 和原來類都有 methodA,那么 category 附加完成之后,類的方法列表里會有兩個 methodA,並且category添加的methodA會排在原有類的methodA的前面,因此如果存在category的同名方法,那么在調用的時候,則會先找到最后一個編譯的 category 里的對應方法。

參考這篇文章

 

6、category & extension區別,能給NSObject添加Extension嗎,結果如何?

 category:分類

  • 給類添加新的方法
  • 不能給類添加成員變量
  • 通過@property定義的變量,只能生成對應的getter和setter的方法聲明,但是不能實現getter和setter方法,同時也不能生成帶下划線的成員屬性
  • 是運行期決定的

注意:為什么不能添加屬性,原因就是category是運行期決定的,在運行期類的內存布局已經確定,如果添加實例變量會破壞類的內存布局,會產生意想不到的錯誤。

extension:擴展

  • 可以給類添加成員變量,但是是私有的
  • 可以給類添加方法,但是是私有的
  • 添加的屬性和方法是類的一部分,在編譯期就決定的。在編譯器和頭文件的@interface和實現文件里的@implement一起形成了一個完整的類。
  • 伴隨着類的產生而產生,也隨着類的消失而消失
  • 必須有類的源碼才可以給類添加extension,所以對於系統一些類,如nsstring,就無法添加類擴展

不能給NSObject添加Extension,因為在extension中添加的方法或屬性必須在源類的文件的.m文件中實現才可以,即:你必須有一個類的源碼才能添加一個類的extension。

 

7、消息轉發機制,消息轉發機制和其他語言的消息機制優劣對比

 消息轉發機制:當接收者收到消息后,無法處理該消息時(即:找不到調用的方法SEL),就會啟動消息轉發機制,流程如下:

第一階段:咨詢接收者,詢問它是否可以動態增加這個方法實現

第二階段:在第一階段中,接收者無法動態增加這個方法實現,那么系統將詢問是否有其他對象可能執行該方法,如果可以,系統將轉發給這個對象處理。

第三階段:在第二階段中,如果沒有其他對象可以處理,那么系統將該消息相關的細節封裝成NSInvocation對象,再給接收者最后一次機會,如果這里仍然無法處理,接收者將收到doesNotRecognizeSelector方法調用,此時程序將crash。

具體方法如下:

// 第一階段 咨詢接收者是否可以動態添加方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
+ (BOOL)resolveClassMethod:(SEL)selector //處理的是類方法

// 第二階段:詢問是否有其他對象可以處理
- (id)forwardingTargetForSelector:(SEL)selector

// 第三階段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)invocation

參考這篇文章

 

8、在方法調用的時候,方法查詢-> 動態解析-> 消息轉發 之前做了什么

OC中的方法調用,編譯后的代碼最終都會轉成objc_msgSend(id , SEL, ...)方法進行調用,這個方法第一個參數是一個消息接收者對象,runtime通過這個對象的isa指針找到這個對象的類對象,從類對象中的cache中查找是否存在SEL對應的IMP,若不存在,則會在 method_list中查找,如果還是沒找到,則會到supper_class中查找,仍然沒找到的話,就會調用_objc_msgForward(id, SEL, ...)進行消息轉發。

 

9、IMPSELMethod的區別和使用場景

IMP:是方法的實現,即:一段c函數

SEL:是方法名

Method:是objc_method類型指針,它是一個結構體,如下:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
} 

使用場景:

實現類的swizzle的時候會用到,通過class_getInstanceMethod(class, SEL)來獲取類的方法Method,其中用到了SEL作為方法名

調用method_exchangeImplementations(Method1, Method2)進行方法交換

我們還可以給類動態添加方法,此時我們需要調用class_addMethod(Class, SEL, IMP, types),該方法需要我們傳遞一個方法的實現函數IMP,例如:

static void funcName(id receiver, SEL cmd, 方法參數...) {
   // 方法具體的實現   
}

函數第一個參數:方法接收者,第二個參數:調用的方法名SEL,方法對應的參數,這個順序是固定的。

 

10、loadinitialize方法的區別什么?在繼承關系中他們有什么區別

 load:當類被裝載的時候被調用,只調用一次

  • 調用方式並不是采用runtime的objc_msgSend方式調用的,而是直接采用函數的內存地址直接調用的
  • 多個類的load調用順序,是依賴於compile sources中的文件順序決定的,根據文件從上到下的順序調用
  • 子類和父類同時實現load的方法時,父類的方法先被調用
  • 本類與category的調用順序是,優先調用本類的(注意:category是在最后被裝載的)
  • 多個category,每個load都會被調用(這也是load的調用方式不是采用objc_msgSend的方式調用的),同樣按照compile sources中的順序調用的
  • load是被動調用的,在類裝載時調用的,不需要手動觸發調用

注意:當存在繼承關系的兩個文件時,不管父類文件是否排在子類或其他文件的前面,都是優先調用父類的,然后調用子類的。

例如:compile sources中的文件順序如下:SubB、SubA、A、B,load的調用順序是:B、SubB、A、SubA。

分析:SubB是排在compile sources中的第一個,所以應當第一個被調用,但是SubB繼承自B,所以按照優先調用父類的原則,B先被調用,然后是SubB,A、SubA。

第二種情況:compile sources中的文件順序如下:B、SubA、SubB、A,load調用順序是:B、A、SubA、SubB,這里我給大家畫個圖梳理一下:

initialize:當類或子類第一次收到消息時被調用(即:靜態方法或實例方法第一次被調用,也就是這個類第一次被用到的時候),只調用一次

  • 調用方式是通過runtime的objc_msgSend的方式調用的,此時所有的類都已經裝載完畢
  • 子類和父類同時實現initialize,父類的先被調用,然后調用子類的
  • 本類與category同時實現initialize,category會覆蓋本類的方法,只調用category的initialize一次(這也說明initialize的調用方式采用objc_msgSend的方式調用的)
  • initialize是主動調用的,只有當類第一次被用到的時候才會觸發

參考這篇文章

 

內存管理

1、weak的實現原理?SideTable的結構是什么樣的

 weak:其實是一個hash表結構,其中的key是所指對象的地址,value是weak的指針數組,weak表示的是弱引用,不會對對象引用計數+1,當引用的對象被釋放的時候,其值被自動設置為nil,一般用於解決循環引用的。

weak的實現原理

1、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。

2、添加引用時:objc_initWeak函數會調用 objc_storeWeak() 函數, objc_storeWeak() 的作用是更新指針指向,創建對應的弱引用表。

3、釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取所有weak指針地址的數組,然后遍歷這個數組把其中的數據設為nil,最后把這個entry從weak表中刪除,最后清理對象的記錄。

SideTable的結構如下:

struct SideTable {
// 保證原子操作的自旋鎖
    spinlock_t slock;
    // 引用計數的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

參考這篇文章

 

2、關聯對象的應用?系統如何實現關聯對象的?

應用:

  • 可以在不改變類的源碼的情況下,為類添加實例變量(注意:這里指的實例變量,並不是真正的屬於類的實例變量,而是一個關聯值變量)
  • 結合category使用,為類擴展存儲屬性。

關聯對象實現原理:

關聯對象的值實際上是通過AssociationsManager對象負責管理的,這個對象里有個AssociationsHashMap靜態表,用來存儲對象的關聯值的,關於AssociationsHashMap存儲的數據結構如下:

AssociationsHashMap:

------添加屬性對象的指針地址(key):ObjectAssociationMap(value:所有關聯值對象)

ObjectAssociationMap:

------關聯值的key:關聯值的value

 

具體runtime的方法實現請參考這篇文章

 

3、關聯對象的如何進行內存管理的?關聯對象如何實現weak屬性?

內存管理方面是通過在賦值的時候設置一個policy,根據這個policy的類型對設置的對象進行retain/copy等操作。

當policy為OBJC_ASSOCIATION_ASSIGN的時候,設置的關聯值將是以weak的方式進行內存管理的。

這個題跟上面的問題差不多,可以參考上面的那篇文章。

 

4、Autoreleasepool的原理?所使用的的數據結構是什么?

自動釋放池是一個 AutoreleasePoolPage 組成的一個page是4096字節大小,每個 AutoreleasePoolPage 以雙向鏈表連接起來形成一個自動釋放池

pop 時是傳入邊界對象,然后對page 中的對象發送release 的消息

AutoreleasePool的釋放有如下兩種情況:

  • 一種是Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop。
  • 手動調用AutoreleasePool的釋放方法(drain方法)來銷毀AutoreleasePool或者@autoreleasepool{}執行完釋放

參考這篇文章

 

5、ARC的實現原理?ARC下對retain & release做了哪些優化?

 

參考這篇文章

 

6、ARC下哪些情況會造成內存泄漏?

  • block中的循環引用
  • NSTimer的循環引用
  • addObserver的循環引用
  • delegate的強引用
  • 大次數循環內存爆漲
  • 非OC對象的內存處理(需手動釋放)

參考這篇文章

 

其他

1、Method Swizzle注意事項?

  • 如果是通過method_exchangeImplements()方法實現swizzle的話,需要考慮調用時機,弄不好會出現無效的swizzle
  • 在調用method_exchangeImplements函數之前,我們需要確保傳入的Method,確保有實現IMP

針對method_exchangeImplements函數的副作用,我們可以結合method_setImplementation實現方法swizzle即可。

參考這篇文章

 

2、屬性修飾符atomic的內部實現是怎么樣的?能保證線程安全嗎?

atomic實際上是為成員變量的setter方法自動添加了一個自旋鎖,確保屬性的賦值的原子性。

不能保證線程安全,因為atomic只是對setter方法加鎖,getter並沒有加鎖

參考這篇文章

 

3、iOS 中內省的幾個方法有哪些?內部實現原理是什么?

實現內省的方法包括:

  • isKindOfClass:Class
  • isMemberOfClass:Class
  • respondToSelector:selector
  • conformsToProtocol:protocol

實現原理:以上方法的實現原理都是運用runtime的相關函數實現的。

參考這篇文章,以及oc類的數據結構

 

4、class、objc_getClass、object_getclass 方法有什么區別?

objc_getClass:參數是類名的字符串,返回的就是這個類的類對象;

object_getClass:參數是id類型,它返回的是這個id的isa指針所指向的Class,如果傳參是Class,則返回該Class的metaClass

[obj class]:則分兩種情況:一是當obj為實例對象時,[obj class]中class是實例方法:- (Class)class,返回的obj對象中的isa指針;二是當obj為類對象(包括元類和根類以及根元類)時,調用的是類方法:+ (Class)class,返回的結果為其本身。

 

2020年阿里、字節:一套高效的iOS面試題(二)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM