以100道面試題構建自己的iOS開發體系

曾幾何時,特別喜歡看、收集別人分享的面試真題,直到看到圖中這個學習方法,若有所思。在百度三面被掛掉之後,沉下心來,整理構建自己的開發體系,方便以後查看。

  • 有些還沒有寫完後面再添加
  • 主體脈絡來自:《李明傑底層原理》、《慕課網面試視頻》、《iOS與OSX多線程和內存管理》
  • 希望大家在2021年都能拿到自己滿意的offer

目錄

  • 一、設計原則、設計模式
  • 二、內存管理
  • 三、多線程
  • 四、Block
  • 五、Runtime
  • 六、Runloop
  • 七、KVO
  • 八、KVC
  • 九、Category
  • 十、網絡
  • 十一、UI
  • 十二、其他
  • 十三、OC對象相關

一、設計原則、設計模式

1、六大設計基本原則
定義:一個類只負責一件事
優點:類的複雜度降低、可讀性增強、易維護、變更引起的風險降低
應用:系統提供的UIView和CALayer的關係:UIView負責時間傳遞、事件響應;CALayer負責動畫及展示
定義:對修改關閉、對擴展開放
- 設計的類做好後就不再修改,如果有新的需求,通過新加類的方式來滿足,而不去修改現有的類的代碼

優點:靈活、穩定(不需修改內部代碼,使得被破壞的程度大大下降)
關鍵:抽象化

使用:
- 我們可以把把行爲添加到一個協議中,使用時遵守這個協議即可。
- 添加類目(Category)方式創建
定義:所有引用父類的地方必須能透明地使用其子類的對象。
- 通俗點說就是,父類可以被子類無縫替換,且原有功能不受任何影響

優點:
- 代碼共享,減少創建類的工作量,每個子類都擁有父類的所有屬性和方法
- 提高代碼的可重用性、擴張性,項目的開放性

缺點:程序的可移植性降低,增加了對象間的耦合性
定義:抽象不應該依賴於具體實現,具體實現可以依賴於抽象
核心思想:面向接口編程

優點:代碼結構清晰,維護容易
實例:平時我們使用 protocol 匿名對象模式就是依賴倒置原則的最好體現
定義:客戶端不應該依賴它不需要的接口

- 使用多個專門的協議、而不是一個龐大臃腫的協議。
- 協議中的方法應當儘量少

例:UITableViewDataSource、UITableViewDelegate
優點:解耦、增強可讀性、可擴展性、可維護性
定義:一個對象應該對其他對象有儘可能少的瞭解。
- 也就是說,如果兩個類不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用。

迪米特法則應用:
- 外觀模式(Facade)
- 中介者模式(Mediator)
- 匿名對象

優點:使對象之間的耦合降到最底,從而使得類具有很好的可讀性和可維護性。

特點總結

  • 單一職責原則主要說明:類的職責要單一
  • 里氏替換原則強調:不要破壞繼承體系
  • 依賴倒置原則描述要:面向接口編程
  • 接口隔離原則講解:設計接口的時候要精簡
  • 迪米特法則告訴我們:要降低耦合
  • 開閉原則講述的是:對擴展開放,對修改關閉

  • 設計模式
TODO(待填充);⌛️⌛️⌛️⌛️⌛️

二、內存管理

規則

  • 在iOS中,使用 “引用計數” 來管理OC對象的內存
  • 新創建的OC對象,引用計數是1;
  • 調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1
  • 當引用計數減爲0,OC對象就會銷燬,釋放佔用的內存空間
  • 當調用 alloc、new、copy、mutableCopy 方法返回了一個對象,在不需要這個對象時,要調用release或者aoturelease釋放
2、引用計數怎麼存儲?
- 可以直接存儲在isa指針中
- 如果不夠存儲的話,會存儲在SideTable結構體的refcnts散列表中

struct SideTable {
    spinlock_t stock;
    RefcountMap refcnts; // 存放着對象引用計數的散列表
    weak_table_t weak_table;
}
3、ARC具體爲引用計數做了哪些工作?
- 編譯階段自動添加代碼

ARC是LLVM編譯器和Runtime系統相互協作的一個結果
- 編譯器幫我們實現內存管理相關的代碼
- Runtime在程序運行過程中處理弱引用
4、深拷貝與淺拷貝
概念:
- 深拷貝:內容拷貝,產生新的對象
- 淺拷貝:指針拷貝,沒有產生新的對象,原對象的引用計數+1
- 完全拷貝:深拷貝的一種,能拷貝多層內容(使用歸解檔技術)

執行結果:
- copy:不可變拷貝,產生不可變副本
- mutableCopy:可變拷貝,產生可變副本


準則:不可變對象的copy方法是淺拷貝,其餘都是深拷貝🚩🚩🚩🚩🚩
原因:
- 它是不可變對象,沒有必要拷貝一份出來,指向同一塊地址還節省內存
- 不可變對象調用copy返回他本身,不可變對象copy就相當於是retain

1、對象的拷貝
- 遵守協議(<NSCopying, NSMutableCopying>)
- 實現協議方法

- (id)copyWithZone:(NSZone *)zone {
    Person *person = [Person allocWithZone:zone];
    person.name = self.name;
    return person;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
    Person *person = [Person allocWithZone:zone];
    person.name = self.name;
    return person;
}


2、集合對象的拷貝
- 對於集合類的可變對象來說,深拷貝並非嚴格意義上的深複製,只能算是單層深複製
- 即雖然新開闢了內存地址,但是存放在內存上的值(也就是數組裏的元素仍然之鄉員數組元素值,並沒有另外複製一份),這就叫做單層深複製
- 對於集合類的對象如何實現每一層都深拷貝呢?(1、initWithArray:copyItems、2、歸檔解檔技術)

#import <Foundation/Foundation.h>

@interface Person : NSObject <NSCoding>

@property (nonatomic, copy) NSString *name;

@end


#import "Person.h"

@implementation Person

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self.name = [aDecoder decodeObjectForKey:@"name"];
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.name forKey:@"name"];
}

@end

歸檔和解檔的概念補充:
有時存在這樣的需求,即將程序中使用的多個對象及其屬性值,以及它們的相互關係保存到文件中,或者發送給另外的進程。爲了實現此功能,foundation框架中,可以把相互關聯的多個對象歸檔爲二進制文件,而且還能將對象的關係從二進制文件中還原出來。

5、weak指針實現原理,SideTable的結構是什麼樣?
1、常用知識點:
- 所引用對象的計數器不會+1,並在引用對象被釋放的時候自動被設置爲nil
- 通常用於解決循環引用問題

2、weak指針實現原理
- Runtime維護了一個weak表,用於存儲指向某個對象的所有weak指針。
- weak表其實就是一個哈希表,key:對象的內存地址;value:指向該對象的所有弱引用的指針
- 當對象銷燬的時候,通過對象的地址值,取出對象的弱引用表,將表裏面的弱引用清除


3、爲什麼弱引用不會導致循環引用?
- 沒有增加引用計數


4、SideTable的結構是什麼樣的?
struct SideTable {
    // 保證原子操作的自旋鎖
    spinlock_t slock;
    // 引用計數的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
};


5、weak屬性如何自動置nil的? 具體到如何查找的?
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
6、自動釋放池相關
1、以下代碼輸出什麼?會有什麼問題?

for (int i = 0; i < 1000000; i ++) {
    NSString *string = @"Abc";
    string = [string lowercaseString];
    // string = [string stringByAppendingString:@"xyz"];
    string = [string stringByAppendingFormat:@"xyz"];
    NSLog(@"%d-%@", i, string);
}

問題解析:
- 每執行一次循環,就會有一個string加到當前NSRunloop中的自動釋放池中
- 只有當自動釋放池被release的時候,自動釋放池中的標示了autorelease的這些數據所佔用的內存空間才能被釋放掉
- 當someLargeNumber大到一定程度時,內存空間將被耗盡而沒有被釋放掉,所以就出現了內存溢出的現象。

解決方案:在循環裏面加個自動釋放池
for (int i = 0; i < 1000000; i ++) {
    @autoreleasepool {
        NSString *string = @"Abc";
        string = [string lowercaseString];
        string = [string stringByAppendingFormat:@"xyz"];
        NSLog(@"%d-%@", i, string);
    }
}


2、自動釋放池底層結構:
- AutoreleasPool是通過以AutoreleasePoolPage爲結點的 “雙向鏈表” 來實現的


3、AutoreleasPool運行的三個過程:
- objc_autoreleasePoolPush()
- [objc autorelease] 
- objc_autoreleasePoolPop(void *)

objc_autoreleasePoolPush()
- 調用的AutoreleasePoolPage的push函數
- 一個push操作其實就是創建一個新的Autoreleasepool
- 對應AutoreleasePoolPage的具體實現就是往AutoreleasePoolPage中的next位置插入一個 POOL_SENTINEL
- 並且返回插入的 POOL_SENTINEL 的內存地址。這個地址也就是我們前面提到的 pool token
- 在執行 pop 操作的時候作爲函數的入參

push 函數通過調用 autoreleaseFast 函數來執行具體的插入操作
autoreleaseFast 函數在執行具體的插入操作時三種情況不同的處理
- 當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即 next 指向的位置;
- 當前 page 存在且已滿時,創建一個新的 page ,並將對象添加到新創建的 page 中;
- 當前 page 不存在時,即還沒有 page 時,創建第一個 page ,並將對象添加到新創建的 page 中


objc_autoreleasePoolPop(void *) 函數本質
- 就是是調用的 AutoreleasePoolPage 的 pop 函數
- pop 函數的入參就是 push 函數的返回值,也就是 POOL_SENTINEL 的內存地址,即 pool token 。
- 當執行 pop 操作時,內存地址在 pool token 之後的所有 autoreleased 對象都會被 release 。
- 直到 pool token 所在 page 的 next 指向 pool token 爲止。


TODO(待填充);⌛️⌛️⌛️⌛️⌛️
4、autoreleasepool和線程的關係?
7、Copy、Strong、Weak、Assign的區別?
assign
- 用於對基本數據類型進行賦值操作,不更改引用計數
- 也可以用來修飾對象,但是被assign修飾的對象在釋放後,指針的地址還是存在的,指針並沒有被置爲nil,成爲野指針
- 之所以可以修飾基本數據類型,因爲基本數據類型一般分配在棧上,棧的內存會由系統自動處理,不會造成野指針。

weak:
- 修飾Object類型,修飾的對象在釋放後,指針地址會被置爲nil,是一種弱引用
- 在ARC環境下,爲避免循環引用,往往會把delegate屬性用weak修飾
- weak和strong不同的是:當一個對象不再有strong類型的指針指向它的時候,它就會被釋放,即使還有weak型指針指向它,那麼這些weak型指針也將被清除。

strong:
- ARC下的strong等同於MRC下的retain都會把對象引用計數加1

copy:
- 會在內存裏拷貝一份對象,兩個指針指向不同的內存地址。
- 一般用來修飾NSString等有對應可變類型的對象,因爲他們有可能和對應的可變類型(NSMutableString)之間進行賦值操作,爲確保可變對象變化時,對象中的字符串不被修改 ,應該在設置屬性時拷貝一份。
- 而若用strong修飾,如果可變對象變化,對象中的字符串屬性也會跟着變化。


1、block屬性爲什麼需要用copy來修飾?
- 因爲在MRC下,block在創建的時候,它的內存是分配在棧(stack)上的,而不是在堆(heap)上,可能被隨時回收。
- 他本身的作於域是屬於創建時候的作用域,一旦在創建時候的作用域外面調用block將導致程序崩潰。
- 通過copy可以把block拷貝(copy)到堆,保證block的聲明域外使用。
- 在ARC下寫不寫都行,編譯器會自動對block進行copy操作。


2、代理爲什麼使用weak修飾?
- weak指明該對象並不負責保持delegate這個對象,delegate的銷燬由外部控制
- 如果用strong修飾,強引用後外界不能銷燬delegate對象,會導致循環引用


3、爲什麼NSMutableArray一般不用copy修飾?
- (void)setData:(NSMutableArray *)data {
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}
拷貝完成後:可變數組->不可變數組,在外操作時(添加、刪除等)會存在問題


4、說到野指針了,什麼是“殭屍對象”?
#[iOS-野指針與殭屍對象](https://www.cnblogs.com/junhuawang/p/9213093.html)⏰⏰⏰⏰⏰

- 一個OC對象引用計數爲0被釋放後就變成殭屍對象,殭屍對象的內存已經被系統回收
- 雖然可能該對象還存在,數據依然在內存中,但殭屍對象已經是不穩定對象了,不可以再訪問或者使用
- 它的內存是隨時可能被別的對象申請而佔用的
8、- (void)dealloc底層執行了什麼?
- (void)dealloc {
    _objc_rootDealloc(self);
}


void _objc_rootDealloc(id obj) {
    ASSERT(obj);
    obj->rootDealloc();
}


inline void objc_object::rootDealloc() {
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&          // 無優化過isa指針
                 !isa.weakly_referenced  &&  // 無弱引用
                 !isa.has_assoc  &&          // 無關聯對象
                 !isa.has_cxx_dtor  &&       // 無cxx析構函數
                 !isa.has_sidetable_rc)) {   // 不存在引用計數器是否過大無法存儲在isa中(使用 sidetable 來存儲引用計數)
        // 直接釋放
        assert(!sidetable_present());
        free(this);
    } else {
        // 下一步
        object_dispose((id)this);
    }
}


// 如果不能快速釋放,則調用 object_dispose()方法,做下一步的處理
static id _object_dispose(id anObject) {
    if (anObject==nil) return nil;

    objc_destructInstance(anObject);
    
    anObject->initIsa(_objc_getFreedObjectClass ());

    free(anObject);
    return nil;
}


void *objc_destructInstance(id obj) {
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();               // 是否存在析構函數
        bool assoc = obj->hasAssociatedObjects();   // 是否有關聯對象

        // This order is important.
        if (cxx) object_cxxDestruct(obj);           // 銷燬成員變量
        if (assoc) _object_remove_assocations(obj); // 釋放動態綁定的對象
        obj->clearDeallocating();
    }
    return obj;
}


/*
 * clearDeallocating一共做了兩件事
 *
 * 1、將對象弱引用表清空,即將弱引用該對象的指針置爲nil
 * 2、清空引用計數表
 * - 當一個對象的引用計數值過大(超過255)時,引用計數會存儲在一個叫 SideTable 的屬性中
 * - 此時isa的 has_sidetable_rc 值爲1,這就是爲什麼弱引用不會導致循環引用的原因
 */
inline void  objc_object::clearDeallocating() {
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }
    assert(!sidetable_present());
}

三、多線程

9、多線程 - GCD相關
GCD核心概念:「任務」、「隊列」

1、任務:
- 概念:指操作,線程中執行的那段代碼,GCD主要放在block中;
- 執行任務的方式:「同步執行」、「異步執行」;
- 區別:是否等待隊列的任務執行結束,是否具備開啓新縣城的能力;

同步執行(sync)
- 同步添加任務到指定隊列中,在添加的任務執行結束之前,會一直等待,直到隊列裏面的任務完成之後再繼續執行
- 只能在當前線程中執行任務,不具備開啓新線程的能力

異步執行(async)
- 異步添加任務到指定隊列中,不會做任何等待,可以繼續執行任務
- 可以在新的線程中執行任務,具備開啓新縣城的能力
- ⚠️異步執行雖然具有開啓新線程的能力,但不一定開啓新線程。(與任務指定的隊列類型有關)


2、隊列(Dispatch Queue)
- 概念:執行任務的等待隊列,即用來存放任務的隊列
- 結構:特殊的線性表,採用FIFO(先進先出)原則。即每讀取一個任務,則從隊列中釋放一個任務

串行隊列:(Serial Dispatch Queue)
- 每次只有一個任務被執行,任務依次執行(只開啓一個線程,一個任務執行完成後,再執行下一個任務)

併發隊列:(Concurrent Dispatch Queue)
- 可以讓多個任務併發(同時)執行。(可以開啓多個線程,並且同時執行任務)
- ⚠️併發隊列的「併發」功能只有在異步(dispatch_async)方法下才有效


3、GCD使用步驟
- 創建一個隊列(串行隊列/併發隊列)
- 將任務追加到任務的等待隊列中,然後系統就會根據任務類型執行任務(同步執行/異步執行)

4、死鎖條件:
- 使用sync函數往當前串行隊列中添加任務,會卡住當前的串行隊列。


面試題一、打印順序
NSLog(@"執行任務1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"執行任務2");
    dispatch_sync(queue2, ^{
        NSLog(@"執行任務3");
    });
    NSLog(@"執行任務4");
});
NSLog(@"執行任務5");

2021-03-01 16:47:46.122744+0800 ZF_Beta[17625:344152] 執行任務1
2021-03-01 16:47:46.122977+0800 ZF_Beta[17625:344152] 執行任務5
2021-03-01 16:47:46.122984+0800 ZF_Beta[17625:344229] 執行任務2
2021-03-01 16:47:46.123171+0800 ZF_Beta[17625:344229] 執行任務3
2021-03-01 16:47:46.123300+0800 ZF_Beta[17625:344229] 執行任務4



dispatch_queue_t ser = dispatch_queue_create("ser", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(ser, ^{
    NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(ser, ^{
    NSLog(@"4");
});
NSLog(@"5");

2021-02-26 11:25:15.703849+0800 ZF_Beta[6156:123418] 1
2021-02-26 11:25:15.704053+0800 ZF_Beta[6156:123418] 3
2021-02-26 11:25:15.704062+0800 ZF_Beta[6156:123698] 2
2021-02-26 11:25:15.704231+0800 ZF_Beta[6156:123418] 4
2021-02-26 11:25:15.704311+0800 ZF_Beta[6156:123418] 5



- (void)viewDidLoad {
   [self performSelector:@selector(test3) withObject:nil afterDelay:0];
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"---111");
    });
    NSLog(@"---333");
}

- (void)test3 {
    NSLog(@"---222");
}

---333
---111
---222



- (void)viewDidLoad {
    [self performSelector:@selector(test1) withObject:nil afterDelay:0];
    [self performSelector:@selector(test2) withObject:nil];
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"===3");
    });
    [UIView animateWithDuration:10 animations:^{
        NSLog(@"===4");
    }];
}

- (void)test1 {
    NSLog(@"===1");
}

- (void)test2 {
    NSLog(@"===2");
}

2021-03-04 17:41:03.759310+0800 property[25604:424718] ===2
2021-03-04 17:41:03.759642+0800 property[25604:424718] ===4
2021-03-04 17:41:03.788454+0800 property[25604:424718] ===3
2021-03-04 17:41:03.789335+0800 property[25604:424718] ===1


面試題二、如何打造線程安全的NSMutableArray?
- 線程鎖:使用線程鎖在對數組讀寫時候加鎖
- 派發隊列:
《Effective Objective 2.0》中41條提出的觀點,串行同步:將讀取和寫入都安排在同一個隊列裏,可保證數據同步。


面試題三、如何異步下載多張小圖最後合成一張大圖?
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加載圖片1 */ });
dispatch_group_async(group, queue, ^{ /*加載圖片2 */ });
dispatch_group_async(group, queue, ^{ /*加載圖片3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合併圖片
});


面試題四、什麼是線程安全?
- 多線程操作過程中往往都是多個線程併發執行的,因此同一個資源可能被多個線程同時訪問,造成資源搶奪。
- 線程安全就是多條線程同時訪問一段代碼,不會造成數據混亂的情況


面試題五、如何設置常駐線程?


面試題六、在異步線程發送通知,在主線程接收通知。會不會有什麼問題?


面試題七、GCD線程是如何調度的


面試題八、如何實現多個任務執行完後,再統一處理?
- 同步阻塞
- 柵欄函數
- 線程組

⚠️基於runloop的線程保活、銷燬與通信:https://www.jianshu.com/p/4d5b6fc33519
11、線程和線程之間如何通信?
線程通信的表現:
- 1個線程傳遞數據給另1個線程
- 在1個線程中執行完特定任務後,轉到另1個線程繼續執行任務


線程間通信常用方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;


例:
- (void)viewDidLoad {
    [super viewDidLoad];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 在子線程中調用download方法下載圖片
    [self performSelectorInBackground:@selector(download) withObject:nil];
}

- (void)download {
    // 1.根據URL網絡中下載圖片
    NSURL *urlstr=[NSURL URLWithString:@"fdsf"];

    // 2、把圖片轉換爲二進制的數據, 這一行操作會比較耗時
    NSData *data=[NSData dataWithContentsOfURL:urlstr];

    // 3、把數據轉換成圖片
    UIImage *image=[UIImage imageWithData:data];

    // 4、回到主線程中設置圖片
    [self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:NO];
}

//設置顯示圖片
- (void)settingImage:(UIImage *)image {
    self.iconView.image=image;
}
12、談談atomic的實現機制,爲什麼不能保證絕對線程安全?
實現機制
- 編譯器自動生成getter/setter方法中添加鎖保證線程安全

爲什麼不能保證絕對安全?
- 在getter/setter中加鎖,僅保證存取時線程安全,不會讓你拿到一個崩潰的值
- 無法保證對容器的修改是線程安全的,例:假設屬性是可變容器(@property (atomic) NSMutableArray *array)時
- 重寫getter/setter方法時,只能依靠自己在getter/setter中保證線程安全

- (void)setCurrentImage:(UIImage *)currentImage {
    if (_currentImage != currentImage) {
        [_currentImage release];
        _currentImage = [currentImage retain];
    }
}

- (UIImage *)currentImage {
    return _currentImage;
}

- (void)setCurrentImage:(UIImage *)currentImage {
    @synchronized(self) {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];
        }
    }
}

- (UIImage *)currentImage {
    @synchronized(self) {
        return _currentImage;
    }
}
13、進程和線程的區別
區別:
- 一個線程只能屬於一個進程.
- 一個進程可以有多個線程,但至少有一個線程。
- 線程是操作系統可識別的最小執行和調度單位。

資源分配:
- 資源分配給進程,同一進程的所有線程共享該進程的所有資源
- 同一進程中的多個線程共享代碼段、數據段、擴展段
- 但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量
14、Notification與線程相關
官方文檔:
- 在多線程應用中,Notification在哪個線程中post,就在哪個線程中被轉發,而不一定是在註冊觀察者的那個線程中。
- 換句話說就是在哪個線程發送通知,就在哪個線程接受通知。


如何實現在不同線程中post和轉發一個Notification?

重定向的實現思路:
1、自定義一個通知隊列(用數組類型),讓它去維護那些我們需要重定向的Notification
2、我們仍然是像平常一樣去註冊一個通知的觀察者,當Notification來了時,先看看post這個Notification的線程是不是我們所期望的線程
3、如果不是,則將這個Notification存儲到我們的隊列中,併發送一個信號(signal)到期望的線程中,來告訴這個線程需要處理一個Notification
4、指定的線程在收到信號後,將Notification從隊列中移除,並進行處理

// 查看一下這個api
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
15、dispatch_once底層實現
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
16、線程鎖相關
線程鎖的作用:
- 我們在使用多線程的時候,多個線程可能會訪問同一塊資源,就很容易引發數據錯亂和數據安全等問題
- 這時候就需要我們保證每次只有一個線程訪問這一塊資源


線程鎖類型:
- 互斥鎖
- 自旋鎖
- 信號量
- 遞歸鎖
- atomic


1、互斥鎖
- 標記用來保證在任一時刻,只能有一個線程訪問對象
- NSLock
- @synchronized (self)


2、自旋鎖
- OSSpinLock(YYKit作者有一篇文章寫它不安全,可以自己研究一下)
- os_unfair_lock


3、信號量(Semaphore - dispatch_semaphore_t)
- 多線程環境下使用的一種設施,是可以用來保證兩個或多個關鍵代碼段不被併發調用
- 在進入一個關鍵代碼段之前,線程必須獲取一個信號量;關鍵代碼段完成後,該線程必須釋放信號量
- 其它想進入該關鍵代碼段的線程必須等待直到第一個線程釋放信號量


4、遞歸鎖(NSRecursiveLock)
- 同一個線程可以多次加鎖,不會造成死鎖


5、atomic
- atomic 修飾的對象,系統會保證在其自動生成的 getter/setter 方法中的操作是完整的,不受其他線程的影響

線程不安全:
- 如果有另一個線程同時在調[name release],那可能就會crash,因爲 release 不受 getter/setter 操作的限制
- 這個屬性只能說是讀/寫安全的,但並不是線程安全的,因爲別的線程還能進行讀寫之外的其他操作

四、Block

17、block相關
1、block本質是什麼?
- block是將函數及其執行上下文封裝起來的對象


2、關於block的截獲特性,你是否有了解?block的截獲變量特性是怎樣的?
變量捕獲機制分析:
- 對於“基本數據類型”的“局部變量”截獲其值
- 對於“對象”類型的局部變量“連同所有權修飾符”一起截獲
- 以“指針形式”截獲局部靜態變量(指針傳遞)
- 不截獲全局變量、靜態全局變量(直接訪問)

改外部變量必要條件
- 將auto從棧copy到堆
原因:棧中內存管理是由系統管理,出了作用域就會被回收,堆中才是可以由程序員管理


3、對棧上的block進行copy之後,假如在mrc環境下,內存是否回泄漏?
- copy操作之後,堆上的block沒有額外的成員變量指向它,正如我們alloc對象後,沒有進行release,造成內存泄漏


4、面試題:請思考,這段代碼有問題麼?
{
    __block MCBlock *blockSelf = self;
    _blk = ^int(int num){
        return num * blockSelf.var;
    }
    _blk(3);
}
- 在MRC下,不會產生循環引用
- 在ARC下,會產生循環引用,造成內存泄漏


5、爲什麼block會產生循環引用?
- 如果當前block對當前對象的某一成員變量進行截獲,block會對當前對象有一個強引用
- 而當前block由於當前對象對其有一個強引用,產生了一個自循環引用的一個循環引用的問題


6、Block不允許修改外部變量的值
原因:
- block 本質上是一個對象,block 的花括號區域是對象內部的一個函數,變量進入 花括號,實際就是已經進入了另一個函數區域---改變了作用域。
- 在幾個作用域之間進行切換時,如果不加上這樣的限制,變量的可維護性將大大降低。
- 比如想在block內聲明瞭一個與外部同名的變量,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。

- 所以 Apple 在編譯器層面做了限制,如果在 block 內部試圖修改 auto 變量(無修飾符),那麼直接編譯報錯。
- 可以把編譯器的這種行爲理解爲:對 block 內部捕獲到的 auto 變量設置爲只讀屬性---不允許直接修改。


7、如何實現對外部變量的捕獲?
- 將變量設置爲全局變量。原理:block內外可直接訪問全局變量
- 加 static (放在靜態存儲區/全局初始化區)。原理是block內部對外部auto變量進行指針捕獲
- 最優解:使用__block 關鍵字


8、__block
- 將auto變量封裝爲結構體(對象),在結構體內部新建一個同名的auto變量
- block內截獲該結構體的指針
- 在block中使用自動變量時,使用指針指向的結構體中的自動變量

__block int var = 10;
void(^blk)(void) = ^{
    var = 20;
};
blk();

轉換後的代碼:

struct __Block_byref_var_0 {
    void *__isa;
    __Block_byref_var_0 *__forwarding;
    int __flags;
    int __size;
    int var; // 10 => 20 該結構體持有相當於原來自動變量的成員變量
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_var_0 *var; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};


9、block在修改NSMutableArray需不需要添加__block
- 不需要
- 當變量是一個指針的時候,block裏只是複製了一份這個指針,兩個指針指向同一個地址。
- 所以,在block裏面對指針指向內容做的修改,在block外面也一樣生效。


10、block是如何捕獲局部變量的?
- block捕獲外界變量時,在內部會自動生成同一個屬性來保存


11、UIView動畫中block回調裏self要不要弱引用?
- 不需要,它不會造成循環引用,因爲它是類方法。
- 之所以需要弱引用本身,是因爲怕對象之間產生循環引用,當前控制器不可能強引用一個類,所以循環無法形成。


12、block裏面會不會存在self爲空的情況(weak strong的原理)?

__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
    __strong typeof(weakself) strongself = weakself;
    [strongself.dataSource reloadDataWithCompletion:nil];
}];

- 有時候weakSelf在block裏在執行reloadDataWithCompletion還存在
- 但在執行reloadDataWithCompletion前,可能會被釋放了
- 爲了保證self在block執行過程裏一直存在,對他強引用strongSelf


13、__block與__weak的區別
- _block不管是ARC還是MRC模式下都可以使用,可以修飾對象,還可以修飾基本數據類型
- __weak只能在ARC模式下使用,也只能修飾對象(NSString),不能修飾基本數據類型(int)
- __block對象可以在block中被重新賦值,__weak不可以。 


14、多層block嵌套如何使用weakSelf?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
    __strong typeof(weakself) strongself = weakself;
    __weak typeof(self) weakSelf2 = strongself;
    [strongself.dataSource reloadDataWithCompletion:^(BOOL result) {
        __strong typeof(self) strongSelf2 = weakSelf2;
    }];
}];


15、Masonry對於block內部引用self會不會造成循環引用?
- 不會
- 這個block沒有copy,是在棧上,使用完直接釋放了,

- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
18、 代理、Block利弊
- 與委託代理模式的代碼相比,用block寫出的代碼更爲整潔

代理優點:
- 代理語法清晰,可讀性高,易於維護
- 它減少代碼耦合性,使事件監聽與事件處理分離
- 一個控制器可以實現多個代理,滿足自定義開發需求,靈活性較高

代理缺點:
- 實現代理的過程較繁瑣
- 跨層傳值時加大代碼的耦合性,並且程序的層次結構也變得混亂
- 當多個對象同時傳值時不易區分,導致代理易用性大大降低


block優點:
- 語法簡潔,代碼可讀性和維護性較高
- 配合GCD優秀的解決多線程問題
block缺點:
- Block中得代碼將自動進行一次retain操作,容易造成內存泄漏
- Block內默認引用爲強引用,容易造成循環應用

運行成本:
delegate運行成本低,block的運行成本高
- block出棧需要將使用的數據從棧內存拷貝到堆內存,當然對象的話就是假引用技術,使用完block置nil纔會消除
- delegate只是保存了一個對象的指針,直接回調,沒有額外的消耗。就像c的函數指針,只多了一個查表動作
19、有哪些情況會出現內存泄漏。
- block循環引用
- delegate循環引用問題
- NSTimer循環引用
- 地圖類處理

- 線程保活target:self
20、__weak來解決block中的循環引用,還有別的方法嗎。
- __block
- 將對象傳進入修改

五、Runtime

21、以下方法打印什麼
@implementation Son : Father

- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}

@end

這兩個都打印出來的是:Son.

- [self class] 會先在當前類的方法列表中查找class這個方法
- [super class] 會先到父類中去查找class方法
- 兩者在找不到的時候,都會繼續向祖先類查詢class方法,最終到NSObject類

- NSObject中class的實現
- (Class)class {
    return object_getClass(self);
}
22、Runtime如何通過selector找到對應的IMP地址?IMP和SEL關係是?
- SEL:類方法的指針,相當於一種編號,區別與IMP!
- IMP:函數指針,保存了方法的地址!

關係:SEL是通過表取對應關係的IMP,進行方法的調用!


struct objc_method {
    SEL method_name                                      
    char *method_types                                       
    IMP method_imp                                           
}
23、Runtime的相關術語
SEL、id、Class、Method、IMP、Cache、Property

- 介紹下runtime的內存模型(isa、對象、類、metaclass、結構體的存儲信息等)
- 爲什麼要設計metaclass
- class_copyIvarList & class_copyPropertyList區別
- class_rw_t 和 class_ro_t 的區別
23、交互兩個方法的現實有什麼風險?
- class_replaceMethod
- method_exchangeImplementations
- class_getInstanceMethod

個人經驗總結:
- 當我們寫的類沒有繼承的關係的時候,倆種方法都沒什麼問題
- 當有繼承關係又不確定方法實現沒實現,最好用class_replaceMethod方法


補充:在美圖秀秀面試時,一個面試官問到方法交互,我說就是交換兩個放的IMP指針指向,
他問還有麼?不知道還有什麼,現在想起來,他應該是想問從isa指針到方法查找,再到根據SEL查找IMP過程吧


- 多次hook方法會存在什麼問題?

TODO(待填充);⌛️⌛️⌛️⌛️⌛️
24、對象關聯底層數據結構
通過 runtime 的源碼得知:
- 關聯屬性並沒有添加到 category_t(分類)裏邊
- 運行時也不會合併到元類對象裏邊
- 而是存儲在一個全局的AssociationsManager裏邊

#import <objc/runtime.h>
// 添加
objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
// 獲取
objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>)
// 移除
objc_removeAssociatedObjects(<#id  _Nonnull object#>)


- 關聯對象的應用?系統如何實現關聯對象的
- 關聯對象的如何進行內存管理的?關聯對象如何實現weak屬性
- 關聯的對象,需要在主對象dealloc的時候釋放麼?

被關聯的對象的生命週期內要比對象本身釋放晚很多, 它們會在被 NSObject -dealloc 調用的 object_dispose() 方法中釋放。

25、消息轉發流程,向一個nil對象發送消息會怎樣
轉發過程(一定要回答出從緩存中查找)
- 消息發送
- 動態方法解析
- 消息轉發


1、消息發送過程 objc_msgSend(receiver, selector)
- 向一個對象發送消息時,runtime會根據對象的isa指針找到所屬類
- 在該類的方法列表及父類方法列表中尋找方法(緩存)
- 如果在最頂層父類中依然找不到對應方法,會報 unrecognized selector send to xxx


2、向一個nil對象發送消息會怎樣?
- objc_msgSend會通過判斷self來決定是否發送消息
- 如果self爲nil,那麼selector也會爲空,直接返回,不會出現問題
- 但對於[NSNull null]對象發送消息時,是會crash的,因爲NSNull類只有一個null方法


在崩潰前有三次拯救程序崩潰的機會,就是接下來的消息轉發

3、消息轉發流程
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
26、performSelector:withObject:afterDelay: 內部大概是怎麼實現的,有什麼注意事項麼?
內部實現:
- 創建一個定時器, 時間結束後系統會使用runtime通過方法名稱去方法列表中找到對應的方法實現並調用方法
- Selector本質就是方法名稱

注意事項:
- 調用performSelector:withObject:afterDelay:方法時,先判斷希望調用的方法是否存在respondsToSelector:
- 這個方法是異步方法,必須在主線程調用,在子線程調用永遠不會調用到想調用的方法

六、Runloop

27、RunLoop相關
什麼是RunLoop?
- RunLoop 實際上是一個對象
- 這個對象在循環中用來處理程序運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件)
- 從而保持程序的持續運行
- 在沒有事件處理的時候,會使線程進入睡眠模式,從而節省 CPU 資源,提高程序性能


// 簡單的理解爲如下代碼
int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 執行各種任務,處理各種事件
        // ......
    } while (running);  // 判斷是否需要退出

    return 0;
}
- 講講runloop,項目中有用到麼?
- runloop內部實現邏輯?
- timer與runloop的關係?
- 程序中添加每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?
- runloop是怎麼響應用戶操作的,具體流程是什麼樣的?
- 說說runloop的幾種狀態?
- runloop的mode作用是什麼

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            // 睡眠中等待消息
            int message = sleep_and_wait;
            // 處理消息
            retVal = process_message(message);
        } while (retVal == 0)
        return 0;
    }
}

  • runloop內部實現邏輯


  • 應用範疇
- 定時器(Timer)、PerformSelect
- GCD Async Main Queue
- 事件響應、手勢識別、界面刷新
- 網絡請求
- AutoreleasePool
  • 基本應用
- 保持程序的持續運行
- 處理App中的各種事件(比如觸摸事件、定時器事件等)
- 節省CPU資源,提高程序性能:該做事時做事,該休息時休息
  • runloop和線程的關係
- 每條線程都有唯一的一個與之對應的RunLoop對象
- RunLoop保存在一個全局的Dictionary裏,線程作爲key,RunLoop作爲value
- 線程剛創建時並沒有RunLoop對象,RunLoop會在第一次獲取它時創建
- RunLoop會在線程結束時銷燬
- 主線程的RunLoop已經自動獲取(創建),子線程默認沒有開啓RunLoop

/*
 * 從字典中獲取,如果沒有則直接創建
 */
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
    __CFSpinLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
    
    // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
    __CFSpinUnlock(&loopsLock);
    CFRelease(newLoop);
}
28、 NSTimer相關
1、NSTimer準嗎?如果不準的話原因是什麼?如何解決?
原因:
- NSTimer的觸發時間到的時候,會在RunLoop中被檢測一次;
- 如果在這一次的RunLoop中做了耗時的操作,會處於阻塞狀態
- 時間超過了定時器的間隔時間,觸發時間就會推遲到下一個runloop週期

解決方法:
- 在子線程中創建timer,在子線程中進行定時任務的操作,需要UI操作時切換回主線程進行操作
- 使用CADisplayLink(時鐘???)
- 使用GCD定時器


2、使用NSTimer是如何處理循環引用的?
- 使用類方法
TODO(待填充);⌛️⌛️⌛️⌛️⌛️

拓展、如何利用runloop監控卡頓

七、KVO

29、KVO相關

KVO 的 全稱Key-Value Observing,俗稱“鍵值監聽”,可以用於某個對象屬性值的改變

1、iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼)
- 利用runtimeAPI動態生成一個子類(NSKVONotifying_XXXX),並且讓instance對象的isa指向這個全新的子類
- 當修改instance對象的屬性時,會動用Foundation的_NSSetXXXValueAndNotify函數
- willChangeValueForKey
- 父類原來的setter方法
- didChangeValueForKey
- 內部觸發監聽器(ObserveValueForKeyPath:ofObject:change:context)

2、如何手動觸發KVO
- 手動調用willChangeValueForKey
- 修改成員變量值
- 手動調用didChangeValueForKey

3、直接修改成員變量會觸發KVO麼
- 不會觸發KVO(原因看KVO的本質)

4、object_getClass(self.person) 和 [self.person class];分別打印什麼?爲什麼?
- object_getClass(self.person); -> NSKVONotifying_MJPerson
- [self.person class];          -> MJPerson

- 原因:NSKVONotifying_MJPerson重寫底層實現,目的:隱藏動態創建的類,不讓用戶感知
- (Class)class {
    return [MJPerson class];
}


// 僞代碼 Function框架
void _NSSetIntValueForKey(){
    [self willChangeValueForKey:@"age"];
    [self setAge:age];
    [self didChangeValueForKey:@"age"];
}

// 通知監聽器
- (void)didChangeValueForKey:(NSString *)key {
   [obser observeValueForKeyPath:key ofObject:self change:nil content:nil];
}

其他:
根據地址打印方法:p (IMP)0X1065....
類對象:  object_getClass(self.person);
原類對象:object_getClass(object_getClass(self.person));
  • 使用kvo什麼時候移除監聽(dealloc不能移除的情況)?

八、KVC

30、KVC相關

KVC的全稱是Key-Value Coding,俗稱“鍵值編碼”,可以通過一個key來訪問某個屬性

1、通過KVC修改屬性會出發KVO麼?
- 能觸發KVO()
- KVC在修改屬性時,會調用willChangeValueForKey:和didChangeValueForKey:方法;


2、KVC的賦值和取值過程是怎樣的?原理是什麼?
- 見下圖

3、使用場景
- 單層字典模型轉化:[self.model setValuesForKeysWithDictionary:dict];

- 通過KVC修改未暴露的屬性:
UILabel *placeholderLabel=[self.userTextField valueForKeyPath:@"placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];

- 使用valueForKeyPath可以獲取數組中的最小值、最大值、平均值、求和
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];

- 數組內部去重
[dataArray valueForKeyPath:@"@distinctUnionOfObjects.self"]


- 數組合並(去重合並:distinctUnionOfArrays.self、直接合並:unionOfArrays.self)
NSArray *temp1 = @[@3, @2, @2, @1];
NSArray *temp2 = @[@3, @4, @5];

NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@distinctUnionOfArrays.self"]);
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@unionOfArrays.self"]);

輸出兩個數組:( 5, 1, 2, 3, 4 ), ( 3, 2, 2, 1, 3, 4, 5 )。


- 大小寫轉換(uppercaseString)及 打印字符串長度同樣適用(length)
NSArray *array = @[@"name", @"w", @"aa", @"jimsa"];
NSLog(@"%@", [array valueForKeyPath:@"uppercaseString"]);
打印:
(NAME,W,AA,JIMSA)
- 首先會按照setKey、_setKey的順序查找方法,找到方法,直接調用方法並賦值;
- 未找到方法,則調用+ (BOOL)accessInstanceVariablesDirectly;
- 若accessInstanceVariablesDirectly方法返回YES,則按照_key、_isKey、key、isKey的順序查找成員變量,找到直接賦值,找不到則拋出異常;
- 若accessInstanceVariablesDirectly方法返回NO,則直接拋出異常;
- 首先會按照getKey、key、isKey、_key的順序查找方法,找到直接調用取值
- 若未找到,則查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,則直接拋出異常;
- 若返回的YES,則按照_key、_isKey、key、isKey的順序查找成員變量,找到則取值;
- 找不到則拋出異常;

九、Category

31、Category相關
1、Category的使用場合是什麼?
- 將一個類拆成很多模塊(其實就是解耦,將相關的功能放到一起)


2、說說Category的實現原理
- 通過runtime動態將分類的方法合併到類對象、元類對象中
- Category編譯之後的底層結構是 struct_category_t , 裏面存儲着分類的對象方法、類方法、屬性、協議信息
- 在程序運行的時候,runtime會將 Category 的數據,合併到類信息中(類對象、元類對象)


3、category和extension區別
- Extension在編譯時,就把信息合併到類信息中
- Category是在運行時,纔會將分類信息合併到類信息中
- 分類聲明的屬性,只會生成getter/setter方法聲明,不會自動生成成員變量和getter/setter方法實現,而擴展會
- 分類不可用爲類添加實例變量,而擴展可以

分類的侷限性:
- 無法爲類添加實例變量,但可通過關聯對象進行實現
- 分類的方法如果和類重名,會覆蓋原來方法的實現
- 多個分類的方法重名,會調用最後編譯的那個分類的實現


4、爲什麼category不能添加屬性?使用Runtime就可以了?
- 分類沒有自己的isa指針
- 類最開始生成了很多基本屬性,比如IvarList,MethodList
- 分類只會將自己的method attach到主類,並不會影響到主類的IvarList
- 實例變量沒有setter和getter方法。也沒有自己的isa指針

- 關聯對象都由AssociationsManager管理
- AssociationsManager裏面是由一個靜態AssociationsHashMap來存儲所有的關聯對象的。
- 相當於把所有對象的關聯對象都存在一個全局map裏面。而map的的key是這個對象的指針地址
- 而這個map的value又是另外一個AssAssociationsHashMap,裏面保存了關聯對象的kv對


5、Category中有load方法麼?load方法什麼時候調用的?load方法能繼承麼?
- 有
- +load方法會在runtime加載類、分類時調用;
- 每個類、分類的+load,在程序運行過程中只調用一次

- 調用順序
- 先調用類的+load,(按照編譯先後順序,先編譯,先調用),調用子類的+load之前會調用父類的+load
- 再調用分類的+load按照編譯先後順序調用(先編譯,先調用)


6、test方法和load方法的本質區別?(+load方法爲什麼不會被覆蓋)
- test方法是通過消息機制調用 objc_msgSend([MJPerson class], @selector(test))
- +load方法調用,直接找到內存中的地址,進行方法調用


7、load調用順序
- +load方法會在runtime加載類、分類時調用
- 每個類、分類的+load,在程序運行過程中只調用一次

調用順序
- 先調用類的+load方法,之後按照編譯先後順序調用(先編譯,先調用,調用子類的+load之前會先調用父類的+load)
- 再調用分類的+load,之後按照編譯先後順序調用(先編譯,先調用)


8、不同Category中存在同一個方法,會執行哪個方法?如果是兩個都執行,執行順序是什麼樣的?
- 根據Build Phases->Compile Sources中添加的文件順序,後面的會覆蓋前面的


9、load、initialize方法的區別是什麼?它們在category中的調用順序?以及出現繼承時他們之間的調用過程?
區別:
調用方式不同
- load是根據函數地址直接調用
- initialize是榮光objc_msgSend調用

調用時刻
- load是runtime加載 類/分類 的時候調用(只會調用1次)
- initialize是類第一次接收消息時調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次)

調用順序
- load:先調用類的load。先編譯的類,優先調用load(調用子類的load之前,會先調用父類的load)
- 再調用分類的load(先編譯的分類,優先調用load)
- initialize:先初始化父類,  再初始化子類(可能最終調用的是父類的initialize方法)


10、⚠️:
- category的方法沒有“完全替換掉”原來類已經有的方法,也就是說如果category和原來類都有methodA,那麼category附加完成之後,類的方法列表裏會有兩個methodA
- category的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的後面
- 這也就是我們平常所說的category的方法會“覆蓋”掉原來類的同名方法,這是因爲運行時在查找方法的時候是順着方法列表的順序查找的,它只要一找到對應名字的方法,就會罷休_,殊不知後面可能還有一樣名字的方法。


11、爲什麼不能動態添加成員變量?
- 方法和屬性並不“屬於”類實例,而成員變量“屬於”類實例
- “類實例”概念,指的是一塊內存區域,包含了isa指針和所有的成員變量。
- 假如允許動態修改類成員變量佈局,已經創建出的類實例就不符合類定義了,變成了無效對象。但方法定義是在objc_class中管理的,不管如何增刪類方法,都不影響類實例的內存佈局,已經創建出的類實例仍然可正常使用

十、網絡

32、TCP、UDP各自的優缺點及區別
TCP優點:( 可靠,穩定)
- 在傳遞數據之前,會有三次握手來建立連接,
- 在數據傳遞時,有確認、窗口、重傳、擁塞控制機制,
- 在數據傳完後,還會斷開連接用來節約系統資源

TCP缺點:(慢,效率低,佔用系統資源高)
- TCP在傳遞數據之前,要先建連接,這會消耗時間
- 在數據傳遞時(確認機制、重傳機制、擁塞控制機制)等都會消耗大量的時間
- 因爲TCP有確認機制、三次握手機制,這些也導致TCP容易被人利用,實現DOS、DDOS、CC等攻擊

UDP的優點:(快)
- UDP沒有TCP的握手、確認、窗口、重傳、擁塞控制等機制
- UDP是一個無狀態的傳輸協議,所以它在傳遞數據時非常快

UDP的缺點:(不可靠,不穩定)
- 因爲UDP沒有TCP那些可靠的機制,在數據傳遞時,如果網絡質量不好,就會很容易丟包

小結TCP與UDP的區別:
- TCP面向連接(如打電話要先撥號建立連接); UDP是無連接的,即發送數據
- TCP提供可靠的服務。通過TCP連接傳送的數據,無差錯,不丟失,不重複,且按序到達;UDP盡最大努力交付,即不保證可靠交付
- TCP面向字節流,實際上是TCP把數據看成一連串無結構的字節流; UDP是面向報文的
- 每一條TCP連接只能是點到點的; UDP支持一對一,一對多,多對一和多對多的交互通信
33、Scoket連接和HTTP連接的區別
- HTTP協議是基於TCP連接的,是應用層協議,主要解決如何包裝數據。Socket是對TCP/IP協議的封裝,Socket本身並不是協議,而是一個調用接口(API),通過Socket,我們才能使用TCP/IP協議。
- HTTP連接:短連接,客戶端向服務器發送一次請求,服務器響應後連接斷開,節省資源。服務器不能主動給客戶端響應,iPhone主要使用類NSURLConnection
- Socket連接:長連接,客戶端跟服務器端直接使用Socket進行連接,沒有規定連接後斷開,因此客戶端和服務器段保持連接通道,雙方可以主動發送數據
34、HTTP協議的特點,關於HTTP請求GET和POST的區別
特點:
- HTTP超文本傳輸協議,是短連接,是客戶端主動發送請求,服務器做出響應,服務器響應之後,鏈接斷開
- HTTP是一個屬於應用層面向對象的協議,HTTP有兩類報文:請求報文和響應報文
- HTTP請求報文:一個HTTP請求報文由請求行、請求頭部、空行和請求數據4部分組成
- HTTP響應報文:由三部分組成:狀態行、消息報頭、響應正文

GET請求
- 參數在地址後拼接,不安全(因爲所有參數都拼接在地址後面)
- 不適合傳輸大量數據(長度有限制,爲1024個字節)

POST請求
- 參數在請求數據區放着,相對GET請求更安全
- 數據大小理論上沒有限制
- 提交的數據放置在HTTP包的包體中
35、斷點續傳怎麼實現的?
- 斷點續傳主要依賴於 HTTP 頭部定義的 Range 來完成
- 有了 Range,應用可以通過 HTTP 請求獲取失敗的資源,從而來恢復下載該資源
- 當然並不是所有的服務器都支持 Range,但大多數服務器是可以的。Range 是以字節計算的,請求的時候不必給出結尾字節數,因爲請求方並不一定知道資源的大小

36、網絡層相關面試

- 網絡七層協議


- Charles原理


- HTTP和HTTPS的區別?Https爲什麼更加安全?


- HTTPS的連接建立流程


- 解釋一下三次握手和四次揮手


- TCP分片 和 IP分片


- Cookie和Session
[網絡相關之Cookie和Session](https://www.jianshu.com/p/5f250c621e81?utm_campaign=hugo)
37、DNS是什麼?DNS解析過程

域名系統(Domain Name System,DNS)
因特網上的主機,可以使用多種方式標識:

1、區別:
- 主機名:方便人們記憶和接受,但長度不一、沒有規律的字符串,路由器並不方便處理
- IP地址:路由器方便處理,不便於人們記憶

爲了折衷這兩種方式,需要一種能進行主機名到IP地址轉換的目錄服務,就是 域名系統(Domain Name System,DNS)


2、作用:
- 將用戶提供的主機名解析爲IP地址


3、DNS解析過程(以www.163.com爲例:)
- 打開瀏覽器,輸入一個域名(www.163.com)。客戶端會發出一個DNS請求到本地DNS服務器(本地DNS服務器一般都是你的網絡接入服務器商提供,比如中國電信,中國移動)
- 本地DNS服務器會首先查詢它的緩存記錄,如果緩存中有此條記錄,直接返回結果。如果沒有,向DNS根服務器進行查詢。
- 根DNS服務器沒有記錄具體的域名和IP地址的對應關係,而是給出域服務器的地址,告訴他可以到域服務器上去繼續查詢
- 本地DNS服務器繼續向域服務器發出請求,在這個例子中,請求的對象是.com域服務器。
- .com域服務器收到請求之後,也不會直接返回域名和IP地址的對應關係,而是告訴本地DNS服務器,你的域名的解析服務器的地址。
- 最後,本地DNS服務器向域名的解析服務器發出請求,這時就能收到一個域名和IP地址對應關係,
- 本地DNS服務器不僅要把IP地址返回給用戶電腦,還要把這個對應關係保存在緩存中,以備下次別的用戶查詢時,可以直接返回結果,加快網絡訪問。

過程:本地服務器->根服務器->域服務器->域名解析服務器
- 整合成流程圖
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
37、TCP建立連接的三次握手中,第二次握手發送的包會包含的標記,最正確的描述是?
38、json轉model實現
39、如何設計一個網絡庫
40、AFNetworking如何是https的,如何適配ipv6的?

十一、UI

41、Storyboard/Xib和純代碼UI相比,有哪些優缺點?
storyboard/xib優點
- 簡單直接。直接通過拖拽和點選即可完成配置。
- 跳轉關係清楚

缺點:
- 協作衝突(多人提交代碼)
- 很難做到頁面繼承和重用
- 不便於進行模塊化管理
- 影響性能(多圖層渲染)
42、自動佈局AutoLayout原理,性能如何
戴明-iOS開發高手課 - 03
43、說明比較方法:layoutifNeeded、layoutSubviews、setNeedsLayout
44、如果頁面 A 跳轉到 頁面 B,A 的 viewDidDisappear 方法和 B 的 viewDidAppear 方法哪個先調用?
- A -->viewWillDisappear
- B-->viewWillAppear
- A-->viewDidDisappear
- B-->viewDidAppear
45、離屏渲染,隱式動畫和顯式動畫相關
⚠️經常看到,圓角會觸發離屏渲染。但其實這個說法是不準確的,因爲圓角觸發離屏渲染也是有條件的!


1、離屏渲染觸發條件:
- 背景色、邊框、背景色+邊框,再加上圓角+裁剪,因爲 contents = nil 沒有需要裁剪處理的內容,所以不會造成離屏渲染。
- 一旦爲contents設置了內容,無論是圖片、繪製內容、有圖像信息的子視圖等,再加上圓角+裁剪,就會觸發離屏渲染。


2、在一個表內有很多cell,每個cell上有很多個視圖,如何解決卡頓問題?
3、切圓角一定會觸發離屏渲染嗎?


4、iOS 9及之後的系統版本,蘋果進行了一些優化
- 只設置contents或者UIImageView的image,並加上圓角+裁剪,是不會產生離屏渲染的。
- 但如果加上了背景色、邊框或其他有圖像內容的圖層,還是會產生離屏渲染。
- 使用類似於UIButton的視圖的時候需要注意
46、frame和bouns的區別。什麼時候frame和bouns的高寬不相等
旋轉後怎麼樣
47、事件響應過程(響應鏈)
1、事件的傳遞 (尋找最合適的view的過程)
- 當一個事件發生後,事件會從父控件傳給子控件 (UIApplication->UIWindow->UIView->initial view)


2、事件的響應
- 首先看initial view能否處理這個事件,如果不能則會將事件傳遞給其上級視圖(inital view的superView)
- 如果上級視圖仍然無法處理則會繼續往上傳遞;一直傳遞到視圖控制器view controller,首先判斷視圖控制器的根視圖view是否能處理此事件
- 如果不能則接着判斷該視圖控制器能否處理此事件,如果還是不能則繼續向上傳遞
- 一直到window,如果window還是不能處理此事件則繼續交給application處理,如果最後application還是不能處理此事件則將其丟棄


3、⚠️注意
- 事件的傳遞是從上到下(父控件到子控件)
- 事件的響應是從下到上(順着響應者鏈條向上傳遞:子控件到父控件)


4、重要方法:
4.1、hitTest:withEvent:
- 只要事件一傳遞給一個控件,這個控件就會調用他自己的hitTest:withEvent:方法
- 尋找並返回最合適的view(能夠響應事件的那個最合適的view)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判斷自己能否接收觸摸事件
    if (self.userInteractionEnabled == NO
        || self.hidden == YES
        || self.alpha <= 0.01) {
        return nil;
    }
    // 2.判斷觸摸點在不在自己範圍內
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    // 3.從後往前遍歷自己的子控件,看是否有子控件更適合響應此事件
    for(NSInteger i = self.subviews.count; i >= 0; i --) {
        UIView *childView = self.subviews[i];
        CGPoint childPoint = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    // 沒有找到比自己更合適的view
    return self;
}


4.2、pointInside:withEvent:
- 判斷點在不在當前view上(方法調用者的座標系上)
- 如果返回YES,代表點在方法調用者的座標系上;
- 返回NO代表點不在方法調用者的座標系上,那麼方法調用者也就不能處理事件。


5、穿透
- 假設有一個黃色控件和白色控件,白色空間覆蓋在黃色控件上
- 點擊白色view想要黃色view來響應該事件,就是所謂的穿透

方法一、
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint yellowPoint = [self convertPoint:point toView:_yellowView];
    if ([_yellowView pointInside:yellowPoint withEvent:event]) {
        return _yellowView;
    }
    return [super hitTest:point withEvent:event];
}

方法二、
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint yellowPoint =[_yellowView convertPoint:point fromView:self];
    if ([_yellowView pointInside:yellowPoint withEvent:event]){
        return NO;
    } else {
        return [super pointInside:point withEvent:event];
    }
}
48、drawRect
49、手勢識別的過程
這裏主要說的是關於runloop的概念點

- 當_UIApplicationHandleEventQueue()識別了一個手勢時,其首先會調用Cancel,將當前的 touchesBegin/Move/End 系列回調打斷
- 隨後系統將對應的 UIGestureRecognizer 標記爲待處理
- 蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件
- 這個 Observer 的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記爲待處理的 GestureRecognizer,並執行GestureRecognizer 的回調。
- 當有 UIGestureRecognizer 的變化(創建/銷燬/狀態改變)時,這個回調都會進行相應處理
50、IBOutlet連出來的視圖屬性爲什麼可以被設置成weak?
UITableView相關優化
- 每個UITableViewCell都有一個UIImageView需要加載,如果沒有緩存,有五個Cell請求一個url,同時請求肯定不實際,如何處理?( UITableViewCell 異步加載圖片緩存機制優化)
- 微信UITableView滑動的時候,動圖是不動的,爲什麼?(Runloop的mode)
兩個半透明控件重貼部分顏色加深問題

十二、其他

51、數組和鏈表的區別
- 數組在內存上給出了連續的空間
- 鏈表,內存地址上可以是不連續的,每個鏈表的節點包括原來的內存和下一個節點的信息(單向的一個,雙向鏈表的話,會有兩個)

數組: 
- 優點: 使用方便,查詢效率比鏈表高,內存爲一連續的區域 
- 缺點: 大小固定,不適合動態存儲,不方便動態添加

 鏈表: 
- 優點: 可動態添加刪除,大小可變   
- 缺點: 只能通過順次指針訪問,查詢效率低
53、談談你對編譯、鏈接的理解
54、leak工具使用
55、應用程序啓動過程,啓動優化
- 應用啓動時是用怎樣加載所有依賴的Mach-O文件的?
- 請列舉你所知道main()函數之前耗時的因素都有哪些

App啓動分爲兩種:
- 冷啓動(Cold Launch):從零開始啓動app
- 熱啓動(Warm Launch):app已在內存中,在後臺存活,再次點擊圖標啓動app


啓動時間的優化,主要是針對冷啓動進行優化
1、通過添加環境變量可以打印app的啓動時間分析(詳情請見下圖)
- DYLD_PRINT_STATISTICS
- DYLD_PRINT_STATISTICS_DETAILS(比上一個詳細)
- 一般400毫秒以內正常

打印結果:
Total pre-main time: 238.05 milliseconds (100.0%)              // main函數調用之前(pre-main)總耗時
         dylib loading time: 249.65 milliseconds (104.8%)      // 動態庫耗時 
        rebase/binding time: 126687488.8 seconds (18128259.6%) 
            ObjC setup time:  10.67 milliseconds (4.4%)        // OC結構體準備耗時 
           initializer time:  52.83 milliseconds (22.1%)       // 初始化耗時 
           slowest intializers :                               // 比較慢的加載 
             libSystem.B.dylib :   6.63 milliseconds (2.7%)
   libBacktraceRecording.dylib :   6.61 milliseconds (2.7%)
    libMainThreadChecker.dylib :  31.82 milliseconds (13.3%)


2、冷啓動可以概括爲3大階段
- dyld
- runtime
- main


3、dyld(dynamic link editor),Apple的動態連接器,可以裝載Mach-O(可執行文件、動態庫等)
- 裝載app的可執行文件,同時遞歸加載所有依賴的動態庫
- 當dyld把可執行文件、動態庫都裝載完成後,會通知runtime進行下一步處理


4、runtime所做的事情
- 調用map_images函數中調用call_load_methods,調用所有Class和Category的+load方法
- 進行各種objc結構的初始化(註冊objc類、初始化類對象等等)
- 調用C++靜態初始化器和__attribure__((constructor))修飾的函數(JSONKit中存在具體應用)
- 到此爲止,可執行文件和動態庫中所有的符號(Class, Protocol, Selector, IMP...)都已按格式成功加載到內存中,被runtime所管理


5、總結
- app的啓動由dylb主導,將可執行文件加載到內存,順便加載所有依賴的動態庫
- 並由runtime負責加載成objc定義的結構
- 所有初始化工作結束後,dyld就會調用main函數
- 接下來就是ApplicationMain函數,AppDelegate的application:didFinishLaunchingWithOptions:方法


6、按照不同的階段優化
dyld
- 減少動態庫、合併一些動態庫(定期清理不必要的動態庫)
- 減少objc類、分類的數量、減少selector數量(定期清理不必要的類、分類)
- 減少C++虛構函數
- Swift儘量使用struct

runtime
- 使用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++靜態構造器、Objc的+load方法

main
- 在不影響用戶體驗的前提下,儘可能將一些操作延遲,不要全部都放在finishLaunching方法中
- 按需加載
56、包體積優化
安裝包瘦身(ipa):資源文件、可執行文件

資源文件(圖片、音頻、視頻等)
- 採取無損壓縮(使用工具)
- 去除沒有用到的資源(https://github.com/tinymind/LSUnusedResources)


可執行文件瘦身:
- 編譯器優化(Xcode相關配置)
- 利用AppCode(https://www.jetbrains.com/objc/)檢測未使用的代碼:菜單欄 -> Code -> Inspect Code
- 生成LinkMap,可以查看可執行文件的具體組成
- 可藉助第三方工具解析LinkMap文件:http://github.com/huanxsd/LinkMap
57、項目的優化、性能優化
啓動速度:
- 啓動過程中做的事情越少越好(儘可能將多個接口合併)
- 不在UI線程上作耗時的操作(數據的處理在子線程進行,處理完通知主線程刷新節目)
- 在合適的時機開始後臺任務(例如在用戶指引節目就可以開始準備加載的數據)
- 儘量減小包的大小
- 輔助工具(友盟,聽雲,Flurry)

頁面瀏覽速度
- json的處理(iOS 自帶的NSJSONSerialization,Jsonkit,SBJson)
- 數據的分頁(後端數據多的話,就要分頁返回,例如網易新聞,或者 微博記錄)
- 數據壓縮(大數據也可以壓縮返回,減少流量,加快反應速度)
- 內容緩存(例如網易新聞的最新新聞列表都是要緩存到本地,從本地加載,可以緩存到內存,或者數據庫,根據情況而定)
- 延時加載tab(比如app有5個tab,可以先加載第一個要顯示的tab,其他的在顯示時候加載,按需加載
- 算法的優化(核心算法的優化,例如有些app 有個 聯繫人姓名用漢語拼音的首字母排序)

操作流暢度優化
- Tableview 優化(tableview cell的加載優化)
- ViewController加載優化(不同view之間的跳轉,可以提前準備好數據)
58、說說你自己吧
- 你在項目中技術亮點、難點
- 你的發展方向(職業規劃)
- 你的優點、你的缺點
59、說說組件化,你是如何組件化解耦的
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
60、靜態庫、動態庫相關
1、什麼是庫?
- 共享代碼,實現代碼的複用,一般分爲靜態庫和動態庫。


2、靜態庫和動態庫的區別
靜態庫(.a和.framework 樣式):
- 鏈接時完整的拷貝到可執行文件,多次使用多次拷貝,造成冗餘,使包變的更大
- 但是代碼裝載速度快,執行速度略比動態庫快

動態庫:(.dylib和.framework)
- 鏈接時不復制,程序運行時由系統加在到內存中,供系統調用,系統加在一次,多次使用,共用節省內存。


3、爲什麼framework既是靜態又是動態?
- 系統的framework是動態的,自己創建的是靜態的。


4、.a 和 .framework 的區別是什麼?
- .a 是單純的二進制文件,需要 .h文件配合,不能直接使用
- .framework是二進制文件+資源文件,可以直接使用。 .framework = .a + .h + sorrceFile(資源文件)

十三、OC對象相關

61、對 OC 中 Class 的源碼理解?其中 cache 的理解?說說NSCache緩存策略
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

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
62、protocol中能否添加屬性
- OC語言的協議裏面是支持聲明屬性的
- 但在協議中聲明屬性其實和在其中定義方法一樣,只是聲明瞭getter和setter方法,並沒有具體實現
63、OC內聯函數 inline
作用:
- 替代宏

inline函數與宏有區別
- 解決函數調用效率的問題
- 函數之間調用,是內存地址之間的調用,當函數調用完畢之後還會返回原來函數執行的地址。
- 函數調用有時間開銷,內聯函數就是爲了解決這一問題


inline相比於宏的優點
- 避免了宏的缺點:需要預編譯.因爲inline內聯函數也是函數,不需要預編譯.
- 編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。消除了它的隱患和侷限性。
- 可以使用所在類的保護成員及私有成員


inline相比於函數的優點
- inline函數避免了普通函數的,在彙編時必須調用call的缺點:取消了函數的參數壓棧,減少了調用的開銷,提高效率.所以執行速度確比一般函數的執行速度要快.
- 集成了宏的優點,使用時直接用代碼替換(像宏一樣)
64、id和NSObject ,instancetype的區別?
- id和instancetype都可以做方法的返回值。

- id類型的返回值在編譯期不能判斷對象的真實類型,即非關聯返回類型
- instancetype類型的返回值在編譯期可以判斷對象的真實類型,即關聯返回類型。

- id可以用來定義變量, 可以作爲返回值, 可以作爲形參
- instancetype只能用於作爲返回值。

非關聯返回類型、關聯返回類型
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
65、方法簽名有什麼作用?
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
66、nil、Nil、NULL、NSNull的區別?
- nil:指向一個對象的空指針    
- Nil:指向一個類的空指針,   
- NULL:指向其他類型(如:基本類型、C類型)的空指針, 用於對非對象指針賦空值.
- NSNull:在集合對象中,表示空值的對象.

NSNull在Objective-C中是一個類 .NSNull有 + (NSNull *)null; 單例方法.多用於集合(NSArray,NSDictionary)中值爲空的對象.

NSArray *array = [NSArray arrayWithObjects: [[NSObject alloc] init], [NSNull null], @"aaa", nil, [[NSObject alloc] init], [[NSObject alloc] init], nil];
NSLog(@"%ld", array.count);// 輸出 3,NSArray以nil結尾
67、NSDictionary底層實現原理
- 在OC中NSDictionary是使用hash表來實現key和value的映射和存儲的。

hash表存儲過程簡單介紹:
- 根據key值計算出它的hash值h;
- 假設箱子的個數是n,那麼鍵值對應該放在第(h%n)個箱子中。
- 如果該箱子中已經有了鍵值對,就是用開放尋址法或者拉鍊法解決衝突。使用拉鍊法解決哈希衝突時,每個箱子其實是一個鏈表,屬於同一個箱子的所有鍵值對都會排列在鏈表中。
68、父類的property是如何查找的?
- 子類中的propert_list、method_list、ivar_list並不包含父類
- 子類對象的_IMPL包含父類的

從以上幾點回答
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
69、+load與 +initialize
共同點:
- 方法只會執行一次
- 在類使用之前,就自動調用了這兩個方法

區別:
- 執行時機不同()

- load方法:如果類自身沒有定義,並不會調用其父類的load方法;
- initialize方法:如果類自身沒有定義,就會調用其父類的initialize方法;

執行的前提條件:
- load 只要類所在文件被引用,就會執行; 
- 如果類沒有引用進項目,就不會有load的執行; 
- initialize 需要類或者其子類的第一個方法被調用,纔會執行,而且是在第一個方法執行之前,先執行; 
- 即使類文件被引用進項目,但是沒有使用,那麼initialize就不會調用執行;
70、iOS如何實現多繼承,代碼書寫一下
- 使用協議組合
- NSProxy

TODO(待填充);⌛️⌛️⌛️⌛️⌛️
71、類與結構體的區別
- 結構體只能封裝數據,而類還可以封裝行爲
- 賦值:結構體是拷貝,對象之間是地址
- 結構體變量分配在棧空間(如果是一個局部變量的情況下),而對象分配在堆空間
72、crash崩潰怎麼解,崩潰到底層代碼
NSSetUncaughtExceptionHandler可以統計閃退
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
73、屬性、成員變量、set、get方法相關
- 屬性可以與set方法和get方法 三者同時存在嗎,如果不行,請說明原因?
換句話說就是:iOS中同時重寫屬性的set與get方法時,爲什麼訪問不了下劃線屬性?

原因:
- 屬性的setter方法和getter方法是不能同時進行重寫,
- 因爲,一旦你同時重寫了這兩個方法,那麼系統就不會幫你生成這個成員變量了

解決方式:
@synthesize authType = _authType;
- 意思是,將屬性的setter,getter方法,作用於這個變量。
74、isa和superclass相關
1、對象的isa指針指向哪裏?superclass指針呢?(⚠️圖-總結圖)
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基類的meta-class

- class的superclass指向父類的class(如果沒有父類,superclass指針爲nil)
- meta-class的superclass指向父類的meta-class
- ⚠️基類的meta-class的superclass指向基類的class



2、方法調用查找(⚠️⚠️⚠️圖-instance調用對象的軌跡;圖-類方法調用軌跡)
- 對象方法的調用:通過instance的isa找到class,最後找到對象方法的實現進行調用
- 類方法的調用:當調用類方法時,通過class的isa找到meta-class,最後找到類方法的實現進行調用

3、class對象的superclass指針
Student : Person : NSObject

當Student的instance對象要調用Personal的對象方法時:
- 先通過isa找到Student的class,然後通過superclass找到Person的class,最後找到對象方法的實現進行調用

4、meta-class對象的superclass指針
當Student的class要調用Person的類方法時
- 先通過isa找到Student的meta-class,然後通過superclass找到Person的meta-class,最後找到類方法的實現進行調用
75、OC的類信息存放在哪裏?
- 對象方法、屬性、成員變量、協議信息,存放在class對象中
- 類方法,存放在meta-class對象中
- 成員變量的具體值,存放在instance對象中
76、class、meta-class的結構
struct objc_class : objc_object {
    Class ISA;
    Class superclass;
    cache_t cache;             // 方法緩存
    class_data_bits_t bits;    // 用於獲取具體的類信息
}

& FAST_DATA_MASK

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;        // 
    method_array_t methods;      // 方法列表
    property_array_t properties; // 屬性列表
    protocol_array_t protocols;  // 協議列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; // 類名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成員變量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}
77、共用體和結構體的區別

部分參考文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章