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


NSNotification相關

相關參考

1、實現原理(結構設計、通知如何存儲的、name&observer&SEL之間的關系等)

 參考這篇文章

2、通知的發送是同步的,還是異步的?

同步的

3、NSNotificationCenter接收消息和發送消息是在一個線程里嗎?如何異步發送消息?

通知的接收和發送是在一個線程里

實際上發送通知都是同步的,不存在異步操作。而所謂的異步發送,也就是延遲發送,在合適的實際發送。

實現異步發送:

  • 讓通知的執行方法異步執行即可
  • 通過NSNotificationQueue,將通知添加到隊列當中,立即將控制權返回給調用者,在合適的時機發送通知,從而不會阻塞當前的調用

參考這篇文章

 

4、NSNotificationQueue是異步還是同步發送?在哪個線程響應?

NSPostingStyle的值為:

  • NSPostWhenIdle和NSPostASAP:異步發送
  • NSPostNow:同步發送

響應線程:

默認情況是在主線程中響應的,倘若在調用enqueueNotification將通知添加到隊列中時,是在子線程中完成的,那么,響應也會在這個子線程中。

 

5、NSNotificationQueuerunloop的關系

NSNotificationQueue將通知添加到隊列中時,其中postringStyle參數就是定義通知調用和runloop狀態之間關系。

該參數的三個可選參數:

  • NSPostWhenIdle:runloop空閑的時候回調通知方法
  • NSPostASAP:runloop在執行timer事件或sources事件完成的時候回調通知方法
  • NSPostNow:runloop立即回調通知方法

參考這篇文章

 

6、如何保證通知接收的線程在主線程?

有以下兩種方案

  • 使用addObserverForName: object: queue: usingBlock方法注冊通知,指定在mainqueue上響應block
  • 通過在主線程的runloop中添加machPort,設置這個port的delegate,通過這個Port其他線程可以跟主線程通信,在這個port的代理回調中執行的代碼肯定在主線程中運行,所以,在這里調用NSNotificationCenter發送通知即可,參考這篇文章

 

7、頁面銷毀時不移除通知會崩潰嗎?

  • iOS9.0之前,會crash,原因:通知中心對觀察者的引用是unsafe_unretained,導致當觀察者釋放的時候,觀察者的指針值並不為nil,出現野指針。
  • iOS9.0之后,不會crash,原因:通知中心對觀察者的引用是weak。

 

8、多次添加同一個通知會是什么結果?多次移除通知呢?

多次添加同一個通知,會導致發送一次這個通知的時候,響應多次通知回調。

多次移除通知不會產生crash。

 

9、下面的方式能接收到通知嗎?為什么?

// 發送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

不能

需要了解通知中心存儲通知觀察者的結構了,具體如下:

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation        *wildcard;    /* 鏈表結構,保存既沒有name也沒有object的通知 */
  GSIMapTable        nameless;    /* 存儲沒有name但是有object的通知    */
  GSIMapTable        named;        /* 存儲帶有name的通知,不管有沒有object    */
    ...
} NCTable;

// Observation 存儲觀察者和響應結構體,基本的存儲單元
typedef    struct    Obs {
  id        observer;    /* 觀察者,接收通知的對象    */
  SEL        selector;    /* 響應方法        */
  struct Obs    *next;        /* Next item in linked list.    */
  ...
} Observation;

nameless與named的具體數據結構如下:

如上圖所示,當添加通知監聽的時候,我們傳入了name和object,所以,觀察者的存儲鏈表是這樣的:

named表:key(name):value->key(object):value(Observation)

因此在發送通知的時候,如果只傳入name而並沒有傳入object,是找不到Observation的,也就不能執行觀察者回調

 

Runloop & KVO

runloop

1、app如何接收到觸摸事件的?

  1. 首先,手機中處理觸摸事件的是硬件系統進程 ,當硬件系統進程識別到觸摸事件后,會將這個事件進行封裝,並通過machPort,將封裝的事件發送給當前活躍的APP進程。
  2. 由於APP的主線程中runloop注冊了這個machPort端口,就是用於接收處理這個事件的,所以這里APP收到這個消息后,開始尋找響應鏈。
  3. 尋找到響應鏈后,開始分發事件,它會優先發送給手勢集合,來過濾這個事件,一旦手勢集合中其中一個手勢識別了這個事件,那么這個事件將不會發送給響應鏈對象。
  4. 手勢沒有識別到這個事件,事件將會發送給響應鏈對象UIResponser。

參考這篇文章

 

2、為什么只有主線程的runloop是開啟的?

app啟動前會調用main函數,具體如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

mian函數中調用UIApplicationMain,這里會創建一個主線程,用於UI處理,為了讓程序可以一直運行,所以在主線程中開啟一個runloop,讓主線程常駐。

 

3、為什么只在主線程刷新UI?

UIKit並不是一個  線程安全的類,UI操作涉及到渲染訪問各種View對象的屬性,如果異步操作下會存在讀寫問題,而為其加鎖則會耗費大量資源並拖慢運行速度。另一方面因為整個程序的起點 UIApplication是在主線程進行初始化,所有的用戶事件都是在主線程上進行傳遞(如點擊、拖動),所以view只能在主線程上才能對事件進行響應。而在渲染方面由於圖像的渲染需要以60幀的刷新率在屏幕上  同時更新,在非主線程異步化的情況下無法確定這個處理過程能夠實現同步更新。

參考這篇文章

 

4、PerformSelectorrunloop的關系。

當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 並添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。

當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。

 

參考這篇文章

 

5、如何使線程保活?

  • 在NSThread執行的方法中添加while(true){},這樣是模擬runloop的運行原理,結合GCD的信號量,在{}中處理任務。參考這篇文章
  • 采用runloop的方式。參考這篇文章

 

KVO

1、實現原理。

 在給對象A的屬性name添加KVO觀察者的時候,runtime會動態創建一個類B,這個類B繼承自類A,並且重寫了父類的屬性name的setter方法,在重寫的方法中,在給name成員變量賦值的前后,分別通知調用觀察者回調。

參考這篇文章

 

2、如何手動關閉kvo?

  •  重寫被觀察對象的automaticallyNotifiesObserversForKey方法,返回NO
  • 重寫automaticallyNotifiesObserversOf<key>,返回NO

注意:關閉kvo后,需要手動在賦值前后添加willChangeValueForKey和didChangeValueForKey,才可以收到觀察通知。

參考這篇文章

 

3、通過KVC修改屬性會觸發KVO么?

 

4、哪些情況下使用kvo會崩潰,怎么防護崩潰?

  •  removeObserver一個未注冊的keyPath,導致錯誤:Cannot remove an observer A for the key path "str",because it is not registered as an observer.

解決辦法:根據實際情況,增加一個添加keyPath的標記,在dealloc中根據這個標記,刪除觀察者。

  • 添加的觀察者已經銷毀,但是並未移除這個觀察者,當下次這個觀察的keyPath發生變化時,kvo中的觀察者的引用變成了野指針,導致crash。

解決辦法:在觀察者即將銷毀的時候,先移除這個觀察者。

其實還可以將觀察者observer委托給另一個類去完成,這個類弱引用被觀察者,當這個類銷毀的時候,移除觀察者對象,參考KVOController

 

5、kvo的優缺點?

缺點補充:

  • 只能通過重寫 -observeValueForKeyPath:ofObject:change:context:方法來獲得通知。
  • 不同通過指定selector的方式獲取通知。
  • 不能通過block的方式獲取通知。

參考這篇文章

 

Block

1、block的內部實現,結構體是什么樣的?

 block的結構體如下:

struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 {
    unsigned long int reserved;         // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        // optional helper functions
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
        // required ABI.2010.3.16
        const char *signature;                         // IFF (1<<30)
    } *descriptor;
    // imported variables
};

isa:由此可知,block也是一個對象類型,具體類型包括_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock。

flags:block 的負載信息(引用計數和類型信息),按位存儲,也可以獲取block版本兼容的相關信息。以下是flags按bit位取與的所有可能值:

enum {
    // Set to true on blocks that have captures (and thus are not true
    // global blocks) but are known not to escape for various other
    // reasons. For backward compatibility with old runtimes, whenever
    // BLOCK_IS_NOESCAPE is set, BLOCK_IS_GLOBAL is set too. Copying a
    // non-escaping block returns the original block and releasing such a
    // block is a no-op, which is exactly how global blocks are handled.
    BLOCK_IS_NOESCAPE      =  (1 << 23),

    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), // helpers have C++ code
    BLOCK_IS_GLOBAL =         (1 << 28),
    BLOCK_HAS_STRET =         (1 << 29), // IFF BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE =     (1 << 30),
};
switch (flags & (3<<29)) {
  case (0<<29):      10.6.ABI, no signature field available
  case (1<<29):      10.6.ABI, no signature field available
  case (2<<29): ABI.2010.3.16, regular calling convention, presence of signature field
  case (3<<29): ABI.2010.3.16, stret calling convention, presence of signature field,
}

由此可知:當flags & (3<<29) is BLOCK_HAS_COPY_DISPOSE的時候,才會有copy_helper和dispose_helper函數指針。

invoke:是block具體實現函數指針地址,可以通過此地址直接調用block。

Block_descriptor_1:block的描述文內容,它包括如下:

size:block所占的內存大小

copy_helper:copy函數指針(不同版本不一定存在)

dispose_helper:dispose函數指針(不同版本不一定存在)

signature:block的實現函數的簽名(不同版本不一定存在),可以通過此指針獲取block的參數內容描述、返回值內容描述等

獲取block的方法簽名,可以參考這篇文章

 

2、block是類嗎,有哪些類型?

從block的結構體中可知,block同樣也有一個isa指針,所以block也是一個類,它的類型包括:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

 

3、一個int變量被 __block 修飾與否的區別?block的變量截獲?

沒有被__block修飾的int,block體中對這個變量的引用是值拷貝,在block中是不能被修改的。

通過__block修飾的int,block體中對這個變量的引用是指針拷貝,它會生成一個結構體,復制這個變量的指針引用,從而達到可以修改變量的作用。

關於block的變量截獲:

block會將block體內引用外部變量的變量進行拷貝,將其拷貝到block的數據結構中,從而可以在block體內訪問或修改外部變量。

外部變量未被__block修飾時,block數據結構中捕獲的是外部變量的值,通過__block修飾時,則捕獲的是對外部變量的指針引用。

注意:block內部訪問全局變量時,全局變量不會被捕獲到block數據結構中。

舉個栗子:

未被__block修飾的情況

int param = 1;
int a = param; // 沒用__block修飾的時候,block內部捕獲的外部變量
[self updateInt:a];
NSLog(@"----:%@", @(param));// 這里輸出:1

// 沒用__block修飾的時候,block內部實現如下
- (void)updateInt:(int)a{
    a = 2;// 此時對外部變量修改是無效的
}

被__block修飾的情況

int param = 1;
int *a = &param; // 用__block修飾的時候,block內部捕獲的外部變量,是外部變量的指針
[self updateInt:a];
NSLog(@"----:%@", @(param));// 這里輸出:2


// 用__block修飾的時候,block內部實現如下
- (void)updateInt:(int *)a{
    *a = 2;// 此時對外部變量修改是有效的
}

 參考這篇文章

 

4、block在修改NSMutableArray,需不需要添加__block?

  • 如果修改的是NSMutableArray的存儲內容的話,是不需要添加__block修飾的。
  • 如果修改的是NSMutableArray對象的本身,那必須添加__block修飾。

參考block的變量捕獲。

 

5、block怎么進行內存管理的?

 block按照內存分布,分三種類型:全局內存中的block、棧內存中的block、堆內存中的block。

在MRC和ARC下block的分布情況不一樣

MRC下:

當block內部引用全局變量或者不引用任何外部變量時,該block是在全局內存中的。

當block內部引用了外部的非全局變量的時候,該block是在棧內存中的。

當棧中的block進行copy操作時,會將block拷貝到堆內存中。

通過__block修飾的變量,不會對其應用計數+1,不會造成循環引用。

ARC下:

當block內部引用全局變量或者不引用任何外部變量時,該block是在全局內存中的。

當block內部引用了外部的非全局變量的時候,該block是在堆內存中的。

也就是說,ARC下只存在全局block和堆block。

通過__block修飾的變量,在block內部依然會對其引用計數+1,可能會造成循環引用。

通過__weak修飾的變量,在block內部不會對其引用計數+1,不會造成循環引用。

參考這篇文章

 

6、block可以用strong修飾嗎?

在MRC環境中,是不可以的,strong修飾符會對修飾的變量進行retain操作,這樣並不會將棧中的block拷貝到堆內存中,而執行的block是在堆內存中,所以用strong修飾的block會導致在執行的時候因為錯誤的內存地址,導致閃退。

在ARC環境中,是可以的,因為在ARC環境中的block只能在堆內存或全局內存中,因此不涉及到從棧拷貝到堆中的操作。

 

7、解決循環引用時為什么要用__strong、__weak修飾?

__weak修飾的變量,不會出現引用計數+1,也就不會造成block強持有外部變量,這樣也就不會出現循環引用的問題了。

但是,我們的block內部執行的代碼中,有可能是一個異步操作,或者延遲操作,此時引用的外部變量可能會變成nil,導致意想不到的問題,而我們在block內部通過__strong修飾這個變量時,block會在執行過程中強持有這個變量,此時這個變量也就不會出現nil的情況,當block執行完成后,這個變量也就會隨之釋放了。

 

8、block發生copy時機?

一般情況在ARC環境中,編譯器將創建在棧中的block會自動拷貝到堆內存中,而block作為方法或函數的參數傳遞時,編譯器不會做copy操作。

  • block作為方法或函數的返回值時,編譯器會自動完成copy操作。
  • 當block賦值給通過strong或copy修飾的id或block類型的成員變量時。
  • 當 block 作為參數被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時。

 

9、Block訪問對象類型的auto變量時,在ARC和MRC下有什么區別?

首先我們知道,在ARC下,棧區創建的block會自動copy到堆區;而MRC下,就不會自動拷貝了,需要我們手動調用copy函數。

我們再說說block的copy操作,當block從棧區copy到堆區的過程中,也會對block內部訪問的外部變量進行處理,它會調用Block_object_assign函數對變量進行處理,根據外部變量是strong還會weak對block內部捕獲的變量進行引用計數+1或-1,從而達到強引用或弱引用的作用。

因此

在ARC下,由於block被自動copy到了堆區,從而對外部的對象進行強引用,如果這個對象同樣強引用這個block,就會形成循環引用。

在MRC下,由於訪問的外部變量是auto修飾的,所以這個block屬於棧區的,如果不對block手動進行copy操作,在運行完block的定義代碼段后,block就會被釋放,而由於沒有進行copy操作,所以這個變量也不會經過Block_object_assign處理,也就不會對變量強引用。

簡單說就是:

ARC下會對這個對象強引用,MRC下不會。

參考這篇文章

 


免責聲明!

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



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