1. 程式人生 > >2018-iOS面試題<一>(更新答案版)

2018-iOS面試題<一>(更新答案版)

來源:簡書 - 不懂技術的愛迪生

連結:https://www.jianshu.com/p/7ba3d0eb4908(點選尾部閱讀原文前往)


宣告:面試是對自我審視的一種過程,面試題和iOS程式設計師本身技術水平沒任何關聯,無論你能否全部答出,都不要對自己產生任何正面或消極的評價!(面試題均來自群成員提供)


面試題預覽:


  1. KVO實現原理?

  2. 說說你理解的埋點?

  3. 訊息轉發機制原理?

  4. 說說你理解weak屬性?

  5. 假如Controller太臃腫,如何優化?

  6. 專案中網路層如何做安全處理?

  7. main()之前的過程有哪些?


1.KVO實現原理?


KVO在Apple中的API文件如下: 


Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …


KVO基本原理:


1.KVO是基於runtime機制實現的


2.當某個類的屬性物件第一次被觀察時,系統就會在執行期動態地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內實現真正的通知機制


3.如果原類為Person,那麼生成的派生類名為NSKVONotifying_Person


4.每個類物件中都有一個isa指標指向當前類,當一個類物件的第一次被觀察,那麼系統會偷偷將isa指標指向動態生成的派生類,從而在給被監控屬性賦值時執行的是派生類的setter方法


5.鍵值觀察通知依賴於NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey:;在一個被觀察屬性發生改變之前, willChangeValueForKey:一定會被呼叫,這就 會記錄舊的值。而當改變發生後,didChangeValueForKey:會被呼叫,繼而 observeValueForKey:ofObject:change:context: 也會被呼叫。


KVO深入原理:


1.Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。當觀察物件A時,KVO機制動態建立一個新的名為: NSKVONotifying_A的新類,該類繼承自物件A的本類,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會負責在呼叫原 setter 方法之前和之後,通知所有觀察物件屬性值的更改情況。


2.NSKVONotifying_A類剖析:在這個過程,被觀察物件的 isa 指標從指向原來的A類,被KVO機制修改為指向系統新建立的子類 NSKVONotifying_A類,來實現當前類屬性值改變的監聽;


3.所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對KVO的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們建立一個新的名為“NSKVONotifying_A”的類(),就會發現系統執行到註冊KVO的那段程式碼時程式就崩潰,因為系統在註冊監聽的時候動態建立了名為NSKVONotifying_A的中間類,並指向這個中間類了。


4.(isa 指標的作用:每個物件都有isa 指標,指向該物件的類,它告訴 Runtime 系統這個物件的類是什麼。所以物件註冊為觀察者時,isa指標指向新子類,那麼這個被觀察的物件就神奇地變成新子類的物件(或例項)了。) 因而在該物件上對 setter 的呼叫就會呼叫已重寫的 setter,從而啟用鍵值通知機制。


5.子類setter方法剖析:KVO的鍵值觀察通知依賴於 NSObject 的兩個方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數值的前後分別呼叫2個方法: 被觀察屬性發生改變之前,willChangeValueForKey:被呼叫,通知系統該keyPath 的屬性值即將變更;當改變發生後, didChangeValueForKey: 被呼叫,通知系統該 keyPath 的屬性值已經變更;之後, observeValueForKey:ofObject:change:context: 也會被呼叫。且重寫觀察屬性的setter 方法這種繼承方式的注入是在執行時而不是編譯時實現的。


640?wx_fmt=png

KVO原理圖


2.說說你理解的埋點?


以下幾篇文章寫的相當不錯,可以適當借鑑下!


  • iOS無埋點資料SDK實踐之路

  • iOS無埋點資料SDK的整體設計與技術實現

  • iOS無埋點SDK 之 RN頁面的資料收集


3.訊息轉發機制原理?


訊息轉發機制基本分為三個步驟:


  1. 動態方法解析

  2. 備用接受者

  3. 完整轉發


640?wx_fmt=png

轉發機制原理


新建一個HelloClass的類,定義兩個方法:


@interfaceHelloClass:NSObject

- (void)hello;

+ (HelloClass *)hi;@end


動態方法解析


物件在接收到未知的訊息時,首先會呼叫所屬類的類方法+resolveInstanceMethod:(例項方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知訊息新增一個”處理方法”“。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在執行時通過class_addMethod函式動態新增到類裡面就可以了。


void functionForMethod(id self, SEL _cmd)

{

    NSLog(@"Hello!");

}

Class functionForClassMethod(id self, SEL _cmd)

{

    NSLog(@"Hi!");

    return [HelloClass class];

}

#pragma mark - 1、動態方法解析

+ (BOOL)resolveClassMethod:(SEL)sel

{

    NSLog(@"resolveClassMethod");

    NSString *selString = NSStringFromSelector(sel);

    if ([selString isEqualToString:@"hi"])

    {

         Class metaClass = objc_getMetaClass("HelloClass");

         class_addMethod(metaClass, @selector(hi), (IMP)functionForClassMethod, "[email protected]:");

         return YES;

    }

    return [super resolveClassMethod:sel];

}

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

    NSLog(@"resolveInstanceMethod");

    NSString *selString = NSStringFromSelector(sel);

    if ([selString isEqualToString:@"hello"])

    {

         class_addMethod(self, @selector(hello), (IMP)functionForMethod, "[email protected]:");

         return YES;

    }

    return [super resolveInstanceMethod:sel];

}


備用接受者


動態方法解析無法處理訊息,則會走備用接受者。這個備用接受者只能是一個新的物件,不能是self本身,否則就會出現無限迴圈。如果我們沒有指定相應的物件來處理aSelector,則應該呼叫父類的實現來返回結果。


#pragma mark - 2、備用接收者


- (id)forwardingTargetForSelector:(SEL)aSelector

{

    NSLog(@"forwardingTargetForSelector");

    NSString *selectorString = NSStringFromSelector(aSelector);

    // 將訊息交給_helper來處理    if ([selectorString isEqualToString:@"hello"]) {

        return _helper;

    }

    return [super forwardingTargetForSelector:aSelector];

}


在本類中需要實現這個新的接受物件


@interfaceHelloClass()

{

    RuntimeMethodHelper *_helper;

}

@end

@implementationHelloClass- (instancetype)init

{

    self = [super init];

    if (self)

    {

         _helper = [RuntimeMethodHelper new];

    }

    return self;

}


RuntimeMethodHelper 類需要實現這個需要轉發的方法:


#import"RuntimeMethodHelper.h"

@implementationRuntimeMethodHelper- (void)hello

{

    NSLog(@"%@, %p", self, _cmd);

}@end


完整訊息轉發


如果動態方法解析和備用接受者都沒有處理這個訊息,那麼就會走完整訊息轉發:


#pragma mark - 3、完整訊息轉發


- (void)forwardInvocation:(NSInvocation *)anInvocation

{

    NSLog(@"forwardInvocation");

    if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {

        [anInvocation invokeWithTarget:_helper];

    }

}

/*必須重新這個方法,訊息轉發機制使用從這個方法中獲取的資訊來建立NSInvocation物件*/

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

    if (!signature)

    {

        if ([RuntimeMethodHelper instancesRespondToSelector:aSelector])

        {

            signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];

        }

    }

    return signature;

}


4.說說你理解weak屬性?


weak實現原理:


Runtime維護了一個weak表,用於儲存指向某個物件的所有weak指標。weak表其實是一個hash(雜湊)表,Key是所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件的地址)陣列。

1、初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。

2、新增引用時:objc_initWeak函式會呼叫 objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。

3、釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。


追問的問題一:


1.實現weak後,為什麼物件釋放後會自動為nil?

runtime 對註冊的類, 會進行佈局,對於 weak 物件會放入一個 hash 表中。 用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為 0 的時候會 dealloc,假如 weak 指向的物件記憶體地址是 a ,那麼就會以 a 為鍵, 在這個 weak 表中搜索,找到所有以 a 為鍵的 weak 物件,從而設定為 nil 。


追問的問題二:


2.當weak引用指向的物件被釋放時,又是如何去處理weak指標的呢?

1、呼叫objc_release

2、因為物件的引用計數為0,所以執行dealloc

3、在dealloc中,呼叫了_objc_rootDealloc函式

4、在_objc_rootDealloc中,呼叫了object_dispose函式

5、呼叫objc_destructInstance

6、最後呼叫objc_clear_deallocating,詳細過程如下:

a. 從weak表中獲取廢棄物件的地址為鍵值的記錄

b. 將包含在記錄中的所有附有 weak修飾符變數的地址,賦值為   nil

c. 將weak表中該記錄刪除

d. 從引用計數表中刪除廢棄物件的地址為鍵值的記錄


5.假如Controller太臃腫,如何優化?


1.將網路請求抽象到單獨的類中


方便在基類中處理公共邏輯;


方便在基類中處理快取邏輯,以及其它一些公共邏輯;


方便做物件的持久化。


2.將介面的封裝抽象到專門的類中


構造專門的 UIView 的子類,來負責這些控制元件的拼裝。這是最徹底和優雅的方式,不過稍微麻煩一些的是,你需要把這些控制元件的事件回撥先接管,再都一一暴露回 Controller。


3.構造 ViewModel


借鑑MVVM。具體做法就是將 ViewController 給 View 傳遞資料這個過程,抽象成構造 ViewModel 的過程。


4.專門構造儲存類


專門來處理本地資料的存取。


5.整合常量


6.專案中網路層如何做安全處理?


 1、儘量使用https


https可以過濾掉大部分的安全問題。https在證書申請,伺服器配置,效能優化,客戶端配置上都需要投入精力,所以缺乏安全意識的開發人員容易跳過https,或者拖到以後遇到問題再優化。https除了效能優化麻煩一些以外其他都比想象中的簡單,如果沒精力優化效能,至少在註冊登入模組需要啟用https,這部分業務對效能要求比較低。


2、不要傳輸明文密碼


不知道現在還有多少app後臺是明文儲存密碼的。無論客戶端,server還是網路傳輸都要避免明文密碼,要使用hash值。客戶端不要做任何密碼相關的儲存,hash值也不行。儲存token進行下一次的認證,而且token需要設定有效期,使用refresh


token去申請新的token。


3、Post並不比Get安全


事實上,Post和Get一樣不安全,都是明文。引數放在QueryString或者Body沒任何安全上的差別。在Http的環境下,使用Post或者Get都需要做加密和簽名處理。


4、不要使用301跳轉


301跳轉很容易被Http劫持攻擊。移動端http使用301比桌面端更危險,使用者看不到瀏覽器地址,無法察覺到被重定向到了其他地址。如果一定要使用,確保跳轉發生在https的環境下,而且https做了證書繫結校驗。


5、http請求都帶上MAC


所有客戶端發出的請求,無論是查詢還是寫操作,都帶上MAC(Message Authentication


Code)。MAC不但能保證請求沒有被篡改(Integrity),還能保證請求確實來自你的合法客戶端(Signing)。當然前提是你客戶端的key沒有被洩漏,如何保證客戶端key的安全是另一個話題。MAC值的計算可以簡單的處理為hash(request


params+key)。帶上MAC之後,伺服器就可以過濾掉絕大部分的非法請求。MAC雖然帶有簽名的功能,和RSA證書的電子簽名方式卻不一樣,原因是MAC簽名和簽名驗證使用的是同一個key,而RSA是使用私鑰簽名,公鑰驗證,MAC的簽名並不具備法律效應。


6、http請求使用臨時金鑰


高延遲的網路環境下,不經優化https的體驗確實會明顯不如http。在不具備https條件或對網路效能要求較高且缺乏https優化經驗的場景下,http的流量也應該使用AES進行加密。AES的金鑰可以由客戶端來臨時生成,不過這個臨時的AES


key需要使用伺服器的公鑰進行加密,確保只有自己的伺服器才能解開這個請求的資訊,當然伺服器的response也需要使用同樣的AES


key進行加密。由於http的應用場景都是由客戶端發起,伺服器響應,所以這種由客戶端單方生成金鑰的方式可以一定程度上便捷的保證通訊安全。


7、AES使用CBC模式


不要使用ECB模式,記得設定初始化向量,每個block加密之前要和上個block的祕文進行運算。


7.main()之前的過程有哪些?


1、main之前的載入過程


1)dyld 開始將程式二進位制檔案初始化


2)交由ImageLoader 讀取 image,其中包含了我們的類,方法等各種符號(Class、Protocol 、Selector、 IMP)


3)由於runtime 向dyld 綁定了回撥,當image載入到記憶體後,dyld會通知runtime進行處理


4)runtime 接手後呼叫map_images做解析和處理


5)接下來load_images 中呼叫call_load_methods方法,遍歷所有載入進來的Class,按繼承層次依次呼叫Class的+load和其他Category的+load方法


6)至此 所有的資訊都被載入到記憶體中


7)最後dyld呼叫真正的main函式


注意:dyld會快取上一次把資訊載入記憶體的快取,所以第二次比第一次啟動快一點



640?wx_fmt=png