"2022" 新 iOS 開發進階+面試大全(一)

這篇文章可能有點長,所以分了三篇文章記錄。內容來自網上的面試題以及自己面試過程中遇到的問題總結,也會定期更新,不合理的地方歡迎指正。

  • 更多技術題合集:

iOS技術題大全(上)

iOS技術題大全(中)

iOS技術題大全(下)

提升iOS開發技能學習網址:docs.qq.com/doc/DVWlQam9Qd3B1cEF2

為自己的面試,為自己的跳槽,加油吧 iOS開發

一、分類和擴展

  • 分類和擴展有什么區別?

category

1、分類給類添加方法
2、不能通過正常模式給類添加屬性,但是可以通過 runtime 添加
3、如果在分類中通過@property定義屬性,那么只會對屬性的 getter setter 方法進行聲明,不會實現。同時也不會生成帶下劃線的成員變量
4、在運行時才會編譯代碼

extension

1、擴展可以看成是特殊的分類 匿名分類
2、可以給類添加屬性,私有
3、可以給類添加方法,也是私有
4、在編譯時期就會編譯,與 .h 文件中的@interface和.m文件里的@implement一起形成了一個完整的類
5、擴展一般用來隱藏類的信息,所以使用擴展的前提是要有類的源碼!所以針對系統自帶的類,是無法使用擴展的。

為什么分類可以添加方法,而不能添加成員變量???

因為在運行時,類的內部布局早已經確定,如果添加實例變量,會破壞類的內部布局。

  • 分類的結構體里面有哪些成員?

category 是一個指向類結構體的指針,其結構體的定義如下:

typedef struct objc_category *Category;

struct objc_category {
    char *category_name                          OBJC2_UNAVAILABLE; // 分類名
    char *class_name                             OBJC2_UNAVAILABLE; // 分類所屬的類名
    struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE; // 實例方法列表
    struct objc_method_list *class_methods       OBJC2_UNAVAILABLE; // 類方法列表
    struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE; // 分類所實現的協議列表
}

可以 與 objc_class 的結構體進行對比:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父類
    const char *name                        OBJC2_UNAVAILABLE;  // 類名
    long version                            OBJC2_UNAVAILABLE;  // 類的版本信息,默認為0
    long info                               OBJC2_UNAVAILABLE;  // 類信息,供運行期使用的一些位標識
    long instance_size                      OBJC2_UNAVAILABLE;  // 該類的實例變量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 該類的成員變量鏈表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定義的鏈表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法緩存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 協議鏈表
#endif
} OBJC2_UNAVAILABLE;

經過對比,發現少了一個 struct objc_ivar_list *ivars 成員變量列表!!!這也就說明了 分類是不能添加成員變量的。

  • 分類加載和方法調用順序

    1、 加載:先加載原始類的 load() 方法 ,再去加載 分類中的 load() 方法,如果有多個分類,則按照編譯順序加載

    2、調用:先調用分類中的方法,再去調用原始類中的方法,如果要是重名,則會覆蓋原始類中的方法(因為在方法列表中,分類的方法會排在原始類中同名方法的前面)。

二、atomic的實現機制;為什么不能保證絕對的線程安全(最好可以結合場景來說)?

  • atomic

atomic

1、會對屬性的 setter/getter 方法進行加鎖,這僅僅只能保證在 操作 setter/getter 方法是安全的。不能保證其他線程的安全
2、例如 : 線程1調用了某一屬性的setter方法并進行到了一半,線程2調用其getter方法,那么會執行完setter操作后,在執行getter操作,線程2會獲取到線程1 setter后的完整的值.
3、當幾個線程同時調用同一屬性的setter、getter方法時,會get到一個完整的值,但get到的值不可控。

例如 : 線程1 調用getter ,線程2 調用setter,線程3 調用setter,這3個線程并行同時開始,線程1會get到一個值,但是這個值不可控,可能是線程2,線程3 set之前的原始值,可能是線程2 set的值,也可能是線程3 set的值

  • atomic是線程安全的嗎?

不是,很多文章談到atomic和nonatomic的區別時,都說atomic是線程安全,其實這個說法是不準確的.atomic只是對屬性的getter/setter方法進行了加鎖操作,這種安全僅僅是set/get 的讀寫安全,并非真正意義上的線程安全,因為線程安全還有讀寫之外的其他操作

比如:如果當一個線程正在get或set時,又有另一個線程同時在進行release操作,可能會直接crash

  • nonatomic

nonatomic

系統生成的getter/setter方法沒有加鎖線程不安全,但更快當多個線程同時訪問同一個屬性,會出現無法預料的結果

  • atomic的seter getter內部實現
- (void)setCurrentImage:(UIImage *)currentImage
{
    @synchronized(self) {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];

        }
    }
}

- (UIImage *)currentImage
{
    @synchronized(self) {
        return _currentImage;
    }
}

  • nonatomic的seter getter內部實現
- (void)setCurrentImage:(UIImage *)currentImage
{
    if (_currentImage != currentImage) {
        [_currentImage release];
        _currentImage = [currentImage retain];

    }
}
- (UIImage *)currentImage
{
    return _currentImage;
}

三、被weak修飾的對象在被釋放的時候會發生什么?是如何實現的?知道sideTable么?里面的結構可以畫出來么?

參考:http://www.lxweimin.com/p/b93d61418f17
這個問題在 數據結構&&算法里面做了解答

四、關聯對象有什么應用,系統如何管理關聯對象?其被釋放的時候需要手動將其指針置空么?

我們在 iOS 開發中經常需要使用分類(Category),為已經存在的類添加屬性的需求,但是使用 @property 并不能在分類中正確創建實例變量和存取方法。這時候就會用到關聯對象。

分類中的 @property
@interface DKObject : NSObject

@property (nonatomic, strong) NSString *property;

@end

在使用上述代碼時會做三件事:

  • 生成帶下劃線的實例變量 _property
  • 生成 getter 方法 - property
  • 生成 setter 方法 - setProperty:
@implementation DKObject {
    NSString *_property;
}

- (NSString *)property {
    return _property;
}

- (void)setProperty:(NSString *)property {
    _property = property;
}

@end

這些代碼都是編譯器為我們生成的,雖然你看不到它,但是它確實在這里,我們既然可以在類中使用 @property 生成一個屬性,那么為什么在分類中不可以呢?

我們來做一個小實驗:創建一個 DKObject 的分類 Category,并添加一個屬性 categoryProperty

@interface DKObject (Category)

@property (nonatomic, strong) NSString *categoryProperty;

@end

看起來還是很不錯的,不過 Build 一下這個 Demo,會發現有這么一個警告:

image

在這里的警告告訴我們 categoryProperty 屬性的存取方法需要自己手動去實現,或者使用 @dynamic 在運行時實現這些方法。

換句話說,分類中的 @property 并沒有為我們生成實例變量以及存取方法,而需要我們手動實現。

使用關聯對象

Q:我們為什么要使用關聯對象?

A:因為在分類中 @property 并不會自動生成實例變量以及存取方法,所以一般使用關聯對象為已經存在的類添加『屬性』。

以下是與關聯對象有關的 API,并在分類中實現一個偽屬性:

#import "DKObject+Category.h"
#import <objc/runtime.h>

@implementation DKObject (Category)

- (NSString *)categoryProperty {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setCategoryProperty:(NSString *)categoryProperty {
    objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

這里的 _cmd 代指當前方法的選擇子,也就是 @selector(categoryProperty)

我們使用了兩個方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 來模擬『屬性』的存取方法,而使用關聯對象模擬實例變量。

在這里有必要解釋兩個問題:

  • 為什么向方法中傳入 @selector(categoryProperty)?
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC 是干什么的?

關于第一個問題,我們需要看一下這兩個方法的原型:

id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

@selector(categoryProperty) 也就是參數中的key,其實可以使用靜態指針 static void *類型的參數來代替,不過在這里,筆者強烈推薦使用 @selector(categoryProperty) 作為 key 傳入。因為這種方法省略了聲明參數的代碼,并且能很好地保證 key 的唯一性

OBJC_ASSOCIATION_RETAIN_NONATOMIC 又是什么呢?如果我們使用 Command 加左鍵查看它的定義:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

從這里的注釋我們能看到很多東西,也就是說不同的 objc_AssociationPolicy 對應了不通的屬性修飾符:

objc_AssociationPolicy modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN atomic, strong
OBJC_ASSOCIATION_COPY atomic, copy

而我們在代碼中實現的屬性 categoryProperty 就相當于使用了 nonatomic 和 strong 修飾符。

在obj dealloc時候會調用object_dispose,檢查有無關聯對象,有的話_object_remove_assocations刪除

五、KVO的底層實現?如何取消系統默認的KVO并手動觸發(給KVO的觸發設定條件:改變的值符合某個條件時再觸發KVO)?

實現原理:

image
  • 當某個類的對象第一次被觀察時,系統就會在運行期動態地創建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。
  • 派生類在被重寫的 setter 方法中實現真正的通知機制,就如前面手動實現鍵值觀察那樣。這么做是基于設置屬性會調用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設置方式來變更屬性值,如果僅是直接修改屬性對應的成員變量,是無法實現 KVO 的。
  • 同時派生類還重寫了 class 方法以“欺騙”外部調用者它就是起初的那個類。然后系統將這個對象的 isa 指針指向這個新誕生的派生類,因此這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。

KVO與Notification之間的區別:

  • notification是需要一個發送notification的對象,一般是notificationCenter,來通知觀察者。

  • KVO是直接通知到觀察對象,并且邏輯非常清晰,實現步驟簡單。

六、Autoreleasepool所使用的數據結構是什么?AutoreleasePoolPage結構體了解么?


每創建一個池子,會在首部創建一個 哨兵 對象,作為標記

最外層池子的頂端會有一個next指針。當鏈表容量滿了,就會在鏈表的頂端,并指向下一張表。

Autorelease對象什么時候釋放?

這個問題拿來做面試題,問過很多人,沒有幾個能答對的。很多答案都是“當前作用域大括號結束時釋放”,顯然木有正確理解Autorelease機制。

在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop

例子:

__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];    NSString *str = [NSString stringWithFormat:@"sunnyxx"];    // str是一個autorelease對象,設置一個weak的引用來觀察它
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];    
    NSLog(@"%@", reference); 
    // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];    
    NSLog(@"%@", reference); 
    // Console: (null)
}

當然,我們也可以手動干預Autorelease對象的釋放時機:

- (void)viewDidLoad
{
    [super viewDidLoad];
    @autoreleasepool {        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    }    NSLog(@"%@", str); 
// Console: (null)
}

Autorelease原理

AutoreleasePoolPage

ARC下,我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨后編譯器將其改寫成下面的樣子:

void *context = objc_autoreleasePoolPush();
// {}中的代碼objc_autoreleasePoolPop(context);

而這兩個函數都是對AutoreleasePoolPage的簡單封裝,所以自動釋放機制的核心就在于這個類。

AutoreleasePoolPage是一個C++實現的類

image
  • AutoreleasePool并沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應結構中的parent指針和child指針)。
  • AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)。
  • AutoreleasePoolPage每個對象會開辟4096字節內存(也就是虛擬內存一頁的大小),除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址
  • 上面的id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
  • 一個AutoreleasePoolPage的空間被占滿時,會新建一個AutoreleasePoolPage對象,連接鏈表,后來的autorelease對象在新的page加入。

所以,若當前線程中只有一個AutoreleasePoolPage對象,并記錄了很多autorelease對象地址時內存如下圖:

image

圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page對象,與這一頁鏈表連接完成后,新page的next指針被初始化在棧底(begin的位置),然后繼續向棧頂添加新對象。

所以,向一個對象發送- autorelease消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置

釋放時刻

每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象,值為0(也就是個nil),那么這一個page就變成了下面的樣子:

image

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作為入參,于是:

1.根據傳入的哨兵對象地址找到哨兵對象所處的page

2.在當前page中,將晚于哨兵對象插入的所有autorelease對象都發送一次- release消息,并向回移動next指針到正確位置

3.補充2:從最新加入的對象一直向前清理,可以向前跨越若干個page,直到哨兵所在的page(在一個page中,是從高地址向低地址清理)

剛才的objc_autoreleasePoolPop執行后,最終變成了下面的樣子:

image
嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常簡單了,pop的時候總會釋放到上次push的位置為止,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣,每次一層,互不影響。

七、class_ro_t 和 class_rw_t 的區別?

Class的結構
image
class_rw_t

class_rw_t里面的methods、properties、protocols是二維數組,是可讀可寫的,包含了類的初始內容、分類的內容

image
class_ro_t

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一維數組,是只讀的,包含了類的初始內容

image

收錄文章來源

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容