《iOS面試題 - 老生常談》之提示答案

Joelixy發表於2019-01-23

資料結構、演算法和網路結構

這兩個類別的面試題請參考演算法網路

物件導向的基礎題

  • 物件導向的幾個設計原則?

    • SOLID原則(更新:據朋友提醒應為七個)
      • Single Responsibility Principle(單一原則)
      • Open Close Principle(開閉原則)
      • Liskov Substitution Principle(里氏替換原則)
      • Interface Segregation Principle(介面分離原則)
      • Dependency Inversion Principle(依賴倒置原則)
      • Law of Demeter(Leaset Knowledge Principle)迪米特法則(最少知道原則)
      • Composite Reuse Principle(合成複用原則)
  • Hash表的實現?

    • 通過把關鍵碼值(key)對映到表中的一個位置來訪問記錄,Hash實現的關鍵是雜湊函式和衝突解決(鏈地址法和開放定址法)。
  • 什麼是程式和執行緒?有什麼區別?

    • 程式:是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位。
    • 執行緒:作業系統能夠進行運算排程的最小單位
    • 區別:執行緒被包含在程式之中,是程式中的實際運作單位。一條執行緒指的是程式中一個單一順序的控制流,一個程式中可以併發多個執行緒,每條執行緒並行執行不同的任務。
  • 記憶體的幾大區域?各自的職能?

    • 棧區:由編譯器自動分配和釋放,一般存放函式的引數值,區域性變數等。
    • 堆區:由程式設計師分配和釋放,若不釋放,則程式結束由作業系統回收。
    • 全域性區(static):由編譯器管理(分配和釋放),程式結束由系統釋放。全域性變數和靜態變數(相鄰兩塊區,初始化和未初始化)。
    • 文字常量區:由編譯器管理(分配釋放),程式結束由系統釋放;存放常量字元。
    • 程式程式碼區:存放函式的二進位制程式碼。
  • 架構、框架和設計模式的區別?

    • 架構:一種頂層概括性的設計概念,像是藍圖,將不同的需求抽象為具體元件並使元件互相通訊。
    • 框架:軟體框架是提取特定領域軟體的共性部分形成的體系結構,不同領域的軟體專案有不同的框架型別。
    • 設計模式:一套被反覆使用、多數人知曉、經過分類編目的總結,程式碼設計的一個成果,可以用在不同軟體框架中一種實際問題的解決方案。(在軟體開發中總結出的一種適合解決某種問題的方法)
  • MVC、MVVM和MVP架構的不同?

    • MVC
      • 【優點】簡單易上手、沒有多層呼叫
      • 【缺點】耦合性強;不利於單元測試,Controller擔負了太多的事件處理,處理使用者操作,管理View的宣告週期,API介面呼叫,處理錯誤,處理回撥,監聽通知,處理檢視方向等,容易造成臃腫,維護不方便
    • MVP
      • 【優點】利於單元測試、明確責任分配
      • 【缺點】要寫一些額外的程式碼(如繫結),若業務邏輯複雜,也會造成Presenter的臃腫
    • MVVM
      • 【優點】責任劃分明確;邏輯清晰;可測性高;雙向繫結,配合第三方開源庫使用方便
      • 【缺點】(1)資料繫結使得Bug很難被定位;(2)對於過大的專案,資料繫結需要花費更多的記憶體;依賴於開源庫實現雙向繫結,學習起來比較複雜;

iOS基礎面試題

UI

  • UIView和CALayer的區別?
    • UIView 和 CALayer 都是 UI 操作的物件。兩者都是 NSObject 的子類,發生在 UIView 上的操作本質上也發生在對應的 CALayer 上。
    • UIView 是 CALayer 用於互動的抽象。UIView 是 UIResponder 的子類( UIResponder 是 NSObject 的子類),提供了很多 CALayer 所沒有的互動上的介面,主要負責處理使用者觸發的種種操作。
    • CALayer 在影象和動畫渲染上效能更好。這是因為 UIView 有冗餘的互動介面,而且相比 CALayer 還有層級之分。CALayer 在無需處理互動時進行渲染可以節省大量時間。
    • CALayer的動畫要通過邏輯樹、動畫樹和顯示樹來實現
  • loadView是幹嘛用的?
    • loadView用來自定義view,只要實現了這個方法,其他通過xib或storyboard建立的view都不會被載入 。
  • layoutIfNeeded、layoutSubviews和setNeedsLayout的區別?
    • layoutIfNeeded:方法呼叫後,在主執行緒對當前檢視及其所有子檢視立即強制更新佈局。
    • layoutSubviews:方法只能重寫,我們不能主動呼叫,在螢幕旋轉、滑動或觸控介面、子檢視修改時被系統自動呼叫,用來調整自定義檢視的佈局。
    • setNeedsLayout:方法與layoutIfNeeded相似,不同的是方法被呼叫後不會立即強制更新佈局,而是在下一個佈局週期進行更新。
  • iOS的響應鏈?什麼情況會影響響應鏈?
    • 事件UIResponder:繼承該類的物件才能響應
    • 事件處理:touchesBegain、touchesMoved、touchesEnded、touchesCancelled;
    • 事件響應過程:
      1. UIApplication(及delegate)接收事件傳遞給 keyWindow
      2. keyWindow遍歷subViews的hitTest:withEvent:方法和pointInside方法找到點選區域內的檢視並處理事件
      3. UIView的子檢視也會遍歷其hitTest:withEvent:方法,以此類推,直到找到點選區域內最上層的檢視,將檢視逐步返回給UIApplication
      4. 最後找到的檢視成為第一響應者,若無法響應,呼叫其nextResponder方法,一直找到響應鏈中能處理該事件的物件(據朋友提醒:UIControl子類不適用)。
      5. 最後到Application依然沒有能處理該事件的物件的話,就廢棄該事件;
      • 關鍵是hitTest:withEvent:方法和pointInside方法
      • ⚠️ 以下幾種情況會忽略,hidden為YES,alpha小於等於0.01,userInteractionEnabled為NO,
    • UIControl的子類和UIGestureRecognizer優先順序較高,會打斷響應鏈;
  • 說幾種給UIImageView新增圓角的方式?
    • cornerRadius(iOS9之後不會導致離屏渲染)
    • CoreGraphic繪製
    • UIBezierPath(本質同上)
  • iOS有哪些實現動畫的方式?
    • UIView Animation:系統提供的基於CALayer Animation的封裝,可以實現平移、縮放、旋轉等功能。
    • CALayer Animation:底層CALayer配合CAAnimation的子類,可以實現更復雜的動畫
    • UIViewPropertyAnimator:iOS10後引入的用於處理互動的動畫,可以實現UIView Animation的所有效果。
  • 使用drawRect有什麼影響?
    • 處理touch事件時會呼叫setNeedsDisplay進行強制重繪,帶來額外的CPU和記憶體開銷。

OC基礎

  • iOS的記憶體管理機制?
    • 引用計數,從MRC(Manuel Reference Count)到ARC(Automatic Reference Count)。(最好能說明物件的生命週期)
  • @property後的相關修飾詞有哪些?
    • 原子性:
      1. nonatomic:非原子性,編譯器不會對其進行加鎖(同步鎖的開銷較大)
      2. atomic:原子性(預設),編譯器合成的方法會通過鎖定機制確保原子性(非執行緒安全)
    • 讀寫許可權:
      1. readwrite:可讀寫(預設),若不用@dynamic修飾編譯器會自動為其生成setter和getter方法,否則編譯器不生成由使用者自己實現
      2. readonly:只讀,若不用dynamic修飾僅生成getter方法,否則編譯器不生成getter方法 有使用者來實現
    • 記憶體管理語義:
      1. strong:修飾引用型別(表示持有當前例項),保留新值釋放舊值,若修飾IMMutable不可變型別建議用copy
      2. copy:指修飾不可變集合型別、AttributeString、Block(ARC下strong和copy一樣,把棧中的Block拷貝到堆中,copy表示不需要copy操作了)(設定方法不保留新值而是將其copy,不可變型別的可變型別子類)
      3. weak:修飾引用型別(非持有關係),不保留新值,不釋放舊值(屬性所指物件被摧毀時,屬性值也會清空置nil)
      4. assign:修飾基本資料型別(也可以修飾引用型別,但可能會產生野指標),執行簡單賦值操作
      5. unsafe_unretained:修飾引用型別(也可以修飾基本型別),所指物件被摧毀時,屬性值不會被清空(unsafe)
    • 方法名:
      1. getter=指定方法名
      2. setter(不常用)
  • dynamic和synthesis的區別?
    • dynamic:告訴編譯器不要幫我自動合成setter和getter方法,自己來實現,若沒有實現,當方法被呼叫時會導致訊息轉發。
    • synthesis:指定例項變數的名字,子類過載父類屬性也需要synthesis(重新set和get、使用dynamic,在Protocol定義屬性、在category定義屬性,預設不會自動合成)。
  • array為何用copy修飾?mutableArray為何用strong修飾?
    • array:若用strong修飾,在其被賦值可變的子類後,內容可能會在不知不覺中修改,用copy防止被修改。
    • mutableArray若用copy修飾會返回一個NSArray型別,若呼叫可變型別的新增、刪除、修改方法時會因為找不到對應的方法而crash。
  • 深拷貝和淺拷貝(注意NSString型別)?
    • NSString:strong和copy修飾的屬性是同等的,指向同一個記憶體地址,mutableCopy才是記憶體拷貝;
    • NSMutableString:strong和copy不同,strong指向同一個記憶體地址,copy則會進行記憶體拷貝,mutableCopy也會進行記憶體拷貝;
    • NSArray:對於字元型別和自定義物件結果是不同的,strong和copy都指向相同記憶體地址,mutableCopy也僅僅是指標拷貝;但是mutableCopy可以把Array變成MutableArray
    • PS:copy產生一個不可變型別,mutableCopy產生一個可變型別;對於字元型別mutableCopy會拷貝字元,copy對於NSArray是不拷貝的,對於NSMutableArray 是拷貝的;對於物件型別,僅僅是指標拷貝;(建議手動試試)
  • Block的幾種型別?
    • _NSConcreteGlobalBlock:全域性Block也是預設的block型別,一種優化操作,私有和公開,保持私有可防止外部迴圈引用,儲存在資料區,當捕獲外部變數時會被copy到堆上
    • _NSConcreteStackBlock:棧Block,儲存在棧區,待其作用域結束,由系統自動回收
    • _NSConcreteMallocBlock:堆Block,計數器為0 的時候銷燬
  • isEqual和“==”的區別?
    • ==:比較兩個指標本身,而不是其所指向的物件。
    • isEqual:當且僅當指標值也就是記憶體地址相等;若重寫該方法則要保證isEqual相等則hash相等,hash相等isEqual不一定相等;若指標值相等則相等,值不相等就判斷物件所屬的類,不屬於同一個類則不相等,同屬一個類時再判斷每個屬性是否相等。
  • id和NSObject的區別?
    • id:指向物件的指標,編譯時不做型別檢查,執行時向其傳送訊息才會對其檢查。
    • NSObject:NSObject類及其子類,編譯時做型別檢查,向其傳送的訊息無法處理時就會執行訊息轉發。
    • id<NSObject>:編譯器不對其做型別檢查,物件所屬的類預設實現名為NSObject的Protocol,既能響應方法又不對其做型別檢查.
  • 通知、代理、KVO和Block的不同(結合應用場景回答)?
    • 通知: 適用於毫無關聯的頁面之間或者系統訊息的傳遞,屬於一對多的資訊傳遞關係。例如接收系統音量、系統狀態、鍵盤等,應用模式的設定和改變,都比較適合用通知去傳遞資訊。
    • 代理: 一對一的資訊傳遞方式,適用於相互關聯的頁面之間的資訊傳遞,例如push和present出來的頁面和原頁面之間的資訊傳遞。
    • block: 一對一的資訊傳遞方式,效率會比代理要高(畢竟是直接取IMP指標的操作方式)。適用的場景和代理差不多,都是相互關聯頁面之間的頁面傳值。
    • KVO:屬性監聽,監聽物件的某一屬性值的變化狀況,當需要監聽物件屬性改變的時候使用。例如在UIScrollView中,監聽contentOffset,既可以用KVO,也可以用代理。但是其他一些情況,比如說UIWebView的載入進度,AVPlayer的播放進度,就只能用KVO來監聽了,否則獲取不到對應的屬性值。
  • 什麼是迴圈引用?__weak、__strong和__block的區別?
    • 迴圈引用:兩個物件互相持有對方,一個釋放需要另外一個先釋放,delegate的weak宣告就是為了防止迴圈引用。(一般指雙向強引用,單向強引用不需要考慮,如:UIView動畫Block)
    • __weak:Block會捕獲在Block中訪問Block作用域外的例項,這樣會有記憶體洩漏的風險,用__weak修飾表示在Block不會導致該例項引用計數器加1,也可以在Block執行結束後強制將Block置nil,這樣Block捕獲的例項也會跟著釋放,如果捕獲的僅是基本資料型別,Block只會對其值進行拷貝一份,此時值再怎麼變化也不會影響Block內部的操作。
    • __strong:在Block中使用__weak修飾的例項很容易被釋放,所以需要加鎖判斷是否釋放,未釋放則對其進行強引用持有,保證向該例項傳送訊息的時候不會導致崩潰
    • __block:Block預設不允許修改外部變數的值,以int型別為例,若在Block中訪問變數就把該變數進行Copy一份儲存到Block函式內,然後變數在Block外部無論怎麼改變都不會影響Block中使用的變數的值,若在Block改變外部變數的值,變數必須要用__block修飾,Block是把該變數的棧記憶體地址拷貝到堆中,所以可以直接把改變的新值寫入記憶體。(把棧中變數的指標地址拷貝到堆中)
  • 記憶體洩漏、野指標和殭屍物件的區別?
    • 記憶體洩露:在堆中申請的不再使用的記憶體沒有釋放,程式結束釋放(Analyzer,Leaks)
    • 野指標:指向的記憶體已經被釋放,或被系統標記為可回收(用Malloc Scribble除錯)。
    • 殭屍物件:已經被釋放的物件,指標指向的記憶體塊認為你無權訪問或它無法執行該訊息。(EXC_Bad_Access,開啟NSZombieEnabled檢測)
  • nil、Nil、NULL、NSNull的區別?
    • nil:空例項物件(給物件賦空值)
    • Nil:空類物件(Class class = Nil)
    • NULL:指向C型別的空指標
    • NSNull:類,用於空物件的佔位符(用於替代集合中的空物件,還有判斷物件是否為空物件)
  • static和const的區別?
    • const:宣告全域性只讀變數,(若前面沒有static修飾,在另外一個檔案中宣告同名的常量會報錯)
    • static:修飾變數的作用域(本檔案內),被修飾的變數只會分配一份記憶體,在上一次修改的基礎上進行修改。
    • 一般兩者配合使用,如:static const NSTimeInterval kAnimationDuration = 1.0;不會建立外部符合,編譯時預處理指令會把變數替換成常值。
  • iOS中有哪些設計模式?
    • 【單例】保證應用程式的生命週期內僅有一個該類的實力物件,易於外界訪問。如:UIApplication、NSBundle、NSNotificationCenter、NSFileManager、NSUserDefault、NSURLCache等;
    • 【觀察者】定義了一種一對多的依賴關係,可以讓多個觀察者同時監聽一個主題物件,當主題物件狀態或值發生改變,會通知所有的觀察者;KVO當物件屬性變化時,通知觀察此屬性的物件。案例代表:通知和KVO
    • 【類簇】(隱藏抽象基類背後的實現細節)如:UIButton、NSNumber、NSData、NSArray、NSDictionary、NSSting。用isMemberOfClass和isKindOfClass來判斷。
    • 【命令模式】(執行時可以呼叫任意類的方法),代表:NSInvocation,封裝一個請求或行為作為物件,包含選擇器、方法名等。
    • 【委託模式】“我想知道列表中被選中的內容在第幾行”?可以,接受我的委託就可以知道;只是接受我的委託就要幫我完成這幾件事情,有必須要完成的,有不必要完成的,至於你怎麼完成我就不關心了。
    • 【裝飾器模式】:裝飾器模式在不修改原來程式碼的情況下動態的給物件增加新的行為和職責,它通過一個物件包裝被裝飾物件的方法來修改類的行為,這種方法可以做為子類化的一種替代方法。 案例代表:Category和Delegation
  • 靜態庫和動態庫的區別?
    • 靜態庫:.a(.h配合)和.framework(資原始檔.bundle)檔案,編譯好的二進位制程式碼,使用時link,編譯時會拷貝一份到target程式中,容易增加target體積。
    • 動態庫:.tbd和.dylib,程式執行時動態載入到記憶體,可共享使用,動態載入有效能損失且有依賴性(系統直接提供給的framework都是動態庫!)
  • iOS中內省的幾個方法?
    • 內省是指物件在執行時將其自身細節洩露為物件的能力。 這些細節包括物件在繼承樹中的位置,它是否符合特定的協議,以及它是否響應某個特定的訊息。以及它是否響應某一訊息。
    1. class和superclass方法
    2. isKindOfClass:和isMemberOfClass:
    3. respondsToSelector:
    4. conformsToProtocol:
    5. isEqual:

OC進階

  • Foundation和CoreFoundation的轉換?

    • __bridge:負責傳遞指標,在OC和CF之間轉換,不轉移管理權,若把OC橋接給CF,則OC釋放後CF也無法使用。
    • __bridge_retained:將OC換成CF物件,並轉移物件所有權,同時剝奪ARC的管理權,之後需要使用CFRelease釋放。
    • __bridge_transfer:將CF轉換成OC,並轉移物件所有權,由ARC接管,所以不需要使用CFRelease釋放。
  • array和set的區別?查詢速度和遍歷速度誰更快?

    • array:分配的是一片連續的儲存單元,是有序的,查詢時需要遍歷整個陣列查詢,查詢速度不如Hash。
    • set:不是連續的儲存單元,且資料是無序的,通過Hash對映儲存的位置,直接對資料hash即可判斷對應的位置是否存在,查詢速度較快。
    • 遍歷速度:array的資料結構是一片連續的記憶體單元,讀取速度較快,set是不連續的非線性的,讀取速度較慢;
  • 什麼是行內函數?為什麼需要它?

    • 用inline修飾的函式,不一定是行內函數,而行內函數一定是inline修飾的。
    • 普通函式:編譯會生成call指令(入棧、出棧),呼叫時將call指令地址入棧,並將子程式的起始地址送入,執行完畢後再返回原來函式執行的地址,所以會有一定的時間開銷。
    • #define巨集定義和inline修飾的函式程式碼被放入符號表中,使用時直接替換,沒有呼叫的開銷;#define巨集定義的函式:使用時從符號表替換,有格式要求,不會對引數作有效性檢查且返回值不能強轉;inline修飾的行內函數:不需要預編譯,使用時直接替換且作型別檢查,是一段直接在函式中展開的程式碼(巨集是直接文字替換)
    • PS:inline定義的行內函數程式碼被放入符號表中,在使用時直接替換(把程式碼嵌入呼叫程式碼),不像普通函式需要在函式表中查詢,省去壓棧和出棧,沒有了呼叫的開銷,提高效率;
    • PS:inline修飾的行內函數只是向編譯器申請,最後不一定按照行內函數的方式執行,可能申請會失敗,因為函式內不允許使用迴圈或開關語句,太大會被編譯器還原成普通函式;inline關鍵詞一定要與函式定義一起使用,宣告的話不算行內函數;
  • 圖片顯示的過程?

    1. 假設我們使用 +imageWithContentsOfFile: 方法從磁碟中載入一張圖片,這個時候的圖片並沒有解壓縮;
    2. 然後將生成的 UIImage 賦值給 UIImageView ;
    3. 接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化;
    4. 在主執行緒的下一個 run loop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操作,而受圖片是否位元組對齊等因素的影響,這個 copy 操作可能會涉及以下部分或全部步驟:
      • 分配記憶體緩衝區用於管理檔案 IO 和解壓縮操作;
      • 將檔案資料從磁碟讀到記憶體中;
      • 將壓縮的圖片資料解碼成未壓縮的點陣圖形式,這是一個非常耗時的 CPU 操作;
      • 最後 Core Animation 使用未壓縮的點陣圖資料渲染 UIImageView 的圖層。(必須要了解點陣圖相關的知識點)
  • dispatch_once如何只保證只執行一次?

    • 多執行緒中,若有一個執行緒在訪問其初始化操作,另外一個執行緒進來後會延遲空待,有記憶體屏障來保證程式指令的順序執行
    void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
        volatile long *vval = val;
        if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {
            func(ctxt); // block真正執行
            dispatch_atomic_barrier();
            *val = ~0l;
        } 
        else 
        {
            do
            {
                 _dispatch_hardware_pause();
            } while (*vval != ~0l);
            dispatch_atomic_barrier();
        }
    }
    複製程式碼
  • NSThread、NSRunLoop和NSAutoreleasePool三者之間的關係?

    • 根據官方文件中對 NSRunLoop 的描述,我們可以知道每一個執行緒,包括主執行緒,都會擁有一個專屬的 NSRunLoop 物件,並且會在有需要的時候自動建立。
    • 根據官方文件中對NSAutoreleasePool的描述,我們可知,在主執行緒的 NSRunLoop 物件(在系統級別的其他執行緒中應該也是如此,比如通過 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 獲取到的執行緒)的每個 event loop 開始前,系統會自動建立一個 autoreleasepool ,並在 event loop 結束時 drain 。
    • 需要手動新增autoreleasepool的情況:
      1. 如果你編寫的迴圈中建立了大量的臨時物件;
      2. 如果你建立了一個輔助執行緒
  • 分類可擴充套件的區別?(可從記憶體佈局、載入順序、分類方法和原類方法的執行順序來回答)

    • extension:必須在.m中新增,編譯時作為類的一部分參與記憶體佈局,生命週期與類一樣,新增私有實現,隱藏類的私有資訊。
    • category:獨立的.h和.m檔案,獨立編譯動態載入dyld,為已有類擴充套件功能。
    • 分類侷限:無法新增例項變數,可以新增屬性但不會為其生成例項變數(在執行時,物件的記憶體佈局已確定,若新增例項變數就會破壞類的記憶體佈局,這對編譯型語言來說是災難性的)
    • 分類的載入:把例項方法、屬性和協議新增到類上,若有協議和類方法則新增到元類, 執行時分類的方法列表被動態新增到類的方法列表中,且在類的原有方法前面;
    • 方法順序:【load方法】先呼叫父類的load方法再呼叫分類的,【重寫的方法】由於分類方法在原類方法的前面,所以優先呼叫分類中的方法。
  • OC物件釋放的流程?runloop的迴圈週期會檢查引用計數,釋放流程:release->dealloc->dispose

    • release:引用計數器減一,直到為0時開始釋放
    • dealloc:物件銷燬的入口
    • dispose:銷燬物件和釋放記憶體
      • objc_destructInstance:呼叫C++的清理方法和移除關聯引用
        • clearDeallocating:把weak置nil,銷燬當前物件的表結構,通過以下兩個方法執行(二選一)
          • sidetable_clearDeallocating:清理有指標isa的物件
          • clearDeallocating_slow:清理非指標isa的物件
      • free:釋放記憶體
  • CDDisplayLink和NSTimer的區別?

    • CADisplayLink:以和螢幕重新整理率相同的頻率將內容畫到螢幕上的定時器,需手動新增runloop
    • NSTimer:可自動新增到當前執行緒的runloop,預設defualt模式,也可以選擇新增的loopMode;
  • 用runtime實現方法交換有什麼風險?

    • 風險1:若不在load中交換是非原子性的,在initial方法中不安全
    • 風險2:重寫父類方法時,大部分都是需要呼叫super方法的,swizzling交換方法,若不呼叫,可能會出現一些問題,如:命名衝突、改變引數、呼叫順序、難以預測、難以除錯。

runtime原始碼相關

  • 知道AutoreleasePoolPage嗎?它是怎麼工作的?

    • AutoreleasePool由AuthReleasePoolPage實現,對應AutoreleasePoolPage 的具體實現就是往AutoreleasePoolPage中的next位置插入一個POOL_SENTINEL,並且返回插入的POOL_SENTINEL的記憶體地址。這個地址也就是我們前面提到的 pool token,在執行pop操作的時候作為函式的入參。
    • 通過呼叫 autoreleaseFast函式來執行具體的插入操作;autoreleaseFast函式在執行一個具體的插入操作時,分別對三種情況進行了不同的處理:
      1. 當前 page存在且沒有滿時,直接將物件新增到當前page中,即next指向的位置;
      2. 當前page存在且已滿時,建立一個新的page,並將物件新增到新建立的 page中,然後關聯child page。
      3. 當前page不存在時,即還沒有page時,建立第一個page,並將物件新增到新建立的page中。
  • KVO的底層實現?(看過RAC原始碼的應該知道,RAC監聽方法也是基於此原理,只是稍微有些不同)

    1. 當一個object有觀察者時,動態建立這個object的類的子類
    2. 對每個被觀察的property,重寫其setter方法
    3. 在重寫的setter方法中呼叫willChangeValueForKey:和didChangeValueForKey通知觀察者
    4. 當一個property沒有觀察者時,重寫方法不會被刪除,直到移除所有的觀察者才會刪除且刪除動態建立的子類。
    // 1)監聽前的準備
    [human addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { /* 監聽後的處理 */ }
    // 2)關閉系統觀察者的自動通知
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }
    // 3)呼叫這兩個方法
    [self willChangeValueForKey:@"name"];
    /* 在此對值進行修改 */
    [self didChangeValueForKey:@"name”];
    複製程式碼
  • 被weak修飾的物件是如何被置nil的?知道SideTable嗎?

    • Runtime對註冊的類會進行記憶體佈局,有個SideTable結構體是負責管理類的引用計數表和weak表,weak修飾的物件地址作為key存放到全域性的weak引用表中,value是所有指向這個weak指標的地址集合,呼叫release會導致引用計數器減一,當引用計數器為0時呼叫dealloc,在執行dealloc時將所有指向該物件的weak指標的值設為nil,避免懸空指標。
  • 什麼是關聯物件?可以用來幹嘛?系統如何管理管理物件?支援KVO嗎?

    • 關聯物件:在執行時動態為指定物件關聯一個有生命週期的變數(通過objc_setAssociatedObject和objc_getAssociatedObject),用於實現category中屬性儲存資料的能力和傳值。(支援KVO,讓關聯的物件作為觀察者)
    • 管理關聯物件:系統通過管理一個全域性雜湊表,通過物件指標地址和傳遞的固定引數地址來獲取關聯物件。根據setter傳入的引數策略,來管理物件的生命週期。通過一個全域性的hash表管理物件的關聯,通過物件指標地址獲取物件關聯表,再根據自定義key查詢對應的值(外表:key(物件指標)-value(hash表),內表:key(自定義name)-value(管理的值))
  • isa、物件、類物件、元類和父類之間的關係?

    • 類:物件是類的一個例項,類也是另一個類的例項,這個類就是元類 (metaclass)。元類儲存了類的類方法。當一個類方法被呼叫時,元類會首先查詢它本身是否有該類方法的實現,如果沒有,則該元類會向它的父類查詢該方法,一直找到繼承鏈的根部,找不到就轉發。
    • 元類:元類 (metaclass) 也是一個例項,那麼元類的 isa 指標又指向哪裡呢?為了設計上的完整,所有的元類的 isa 指標都會指向一個根元類 (root metaclass)。根元類 (root metaclass) 本身的 isa 指標指向自己,這樣就行成了一個閉環。上面提到,一個物件能夠接收的訊息列表是儲存在它所對應的類中的。在實際程式設計中,我們幾乎不會遇到向元類發訊息的情況,那它的 isa 指標在實際上很少用到。不過這麼設計保證了物件導向的乾淨,即所有事物都是物件,都有 isa 指標。
    • 繼承:我們再來看看繼承關係,由於類方法的定義是儲存在元類 (metaclass) 中,而方法呼叫的規則是,如果該類沒有一個方法的實現,則向它的父類繼續查詢。所以,為了保證父類的類方法可以在子類中可以被呼叫,所以子類的元類會繼承父類的元類,換而言之,類物件和元類物件有著同樣的繼承關係。

    《iOS面試題 - 老生常談》之提示答案

  • 知道建立類的方法objc_allocateClassPair?方法裡面具體做了什麼事情?

    // objc-class-old.mm
    Class objc_allocateClassPair(Class supercls, const char *name, 
                             size_t extraBytes)
    {
        Class cls, meta;
        if (objc_getClass(name)) return nil;
        // fixme reserve class name against simultaneous allocation
        if (supercls  &&  (supercls->info & CLS_CONSTRUCTING)) {
            // Can't make subclass of an in-construction class 
            return nil;
        }
        // Allocate new classes. 
        if (supercls) { //  若父類存在,父類的記憶體空間+額外的空間 = 新類的記憶體大小
            cls = _calloc_class(supercls->ISA()->alignedInstanceSize() + extraBytes);
            meta = _calloc_class(supercls->ISA()->ISA()->alignedInstanceSize() + extraBytes);
        } else {        //  若父類不存在,基類的記憶體空間+額外的空間 = 新類的記憶體大小(objc_class是objc_object的子類)
            cls = _calloc_class(sizeof(objc_class) + extraBytes);
            meta = _calloc_class(sizeof(objc_class) + extraBytes);
        }
        // 初始化
        objc_initializeClassPair(supercls, name, cls, meta);
        return cls;
    }
    
    // objc-runtime-new.mm
    Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
    {
        Class cls, meta;
        rwlock_writer_t lock(runtimeLock);
        // Fail if the class name is in use.
        // Fail if the superclass isn't kosher.
        if (getClass(name)  ||  !verifySuperclass(superclass, true/*rootOK*/)) {
            return nil;
        }
        // Allocate new classes.
        cls  = alloc_class_for_subclass(superclass, extraBytes);
        meta = alloc_class_for_subclass(superclass, extraBytes);
        // fixme mangle the name if it looks swift-y?
        objc_initializeClassPair_internal(superclass, name, cls, meta);
        return cls;
    }
複製程式碼
  • class_ro_t 和 class_rw_t 的區別?
    • class_ro_t是readonly,class_rw_t是readwrite;
    • 在編譯之後,class_ro_t的baseMethodList就已經確定。當映象載入的時候,methodizeClass方法會將baseMethodList新增到class_rw_t的methods列表中,之後會遍歷category_list,並將category的方法也新增到methods列表中。這裡的category指的是分類,基於此,category能擴充一個類的方法。這是開發時經常需要使用到。
    • class_ro_t在記憶體中是不可變的。在執行期間,動態給類新增方法,實質上是更新class_rw_t的methods列表。
  • 除了objc_msgSend,還知不知道別的訊息傳送函式?
    • objc_msgSend_stret:當CPU的暫存器能夠容納下訊息返回型別時,該函式才能處理此訊息,若超過CPU暫存器則由另一個函式執行派發,原函式會通過分配在棧上的變數來處理訊息所返回的結構體。
    • objc_msgSendSuper_stret:向父類例項傳送一個返回結構體的訊息
    • objc_msgSendSuper:向超類傳送訊息([super msg])
    • objc_msgSend_fpret:訊息返回浮點數時,在某些結構的CPU中呼叫函式時需要對“浮點數暫存器”做特殊處理,不同的架構下浮點數表示範圍不一樣(暫存器是CPU的一部分,用於儲存指令、資料和地址)
  • 什麼是方法交換?怎麼用的?
    • SEL方法地址和IMP函式指標是通過DispatchTable表來對映,可以通過runtime動態修改SEL,所以可以實現方法的交換(使用經驗根據專案來談即可)。

資料持久化

  • plist:XML檔案,讀寫都是整個覆蓋,需讀取整個檔案,適用於較少的資料儲存,一般用於儲存App設定相關資訊。
  • NSUserDefault:通過UserDefault對plist檔案進行讀寫操作,作用和應用場景同上。
  • NSKeyedArchiver:被序列化的物件必須支援NSCoding協議,可以指定任意資料儲存位置和檔名。整個檔案複寫,讀寫大資料效能低。
  • CoreData:是官方推出的大規模資料持久化的方案,它的基本邏輯類似於 SQL 資料庫,每個表為 Entity,然後我們可以新增、讀取、修改、刪除物件例項。它可以像 SQL 一樣提供模糊搜尋、過濾搜尋、表關聯等各種複雜操作。儘管功能強大,它的缺點是學習曲線高,操作複雜。
  • SQLite(FMDB、Realm)

多執行緒

  • 序列佇列和併發佇列的區別?同步和非同步的區別?

    • 序列佇列(Serial Queue):指佇列中同一時間只能執行一個任務,當前任務執行完後才能執行下一個任務,在序列佇列中只有一個執行緒。
    • 併發佇列(Concurrent Queue):允許多個任務在同一個時間同時進行,在併發佇列中有多個執行緒。序列佇列的任務一定是按開始的順序結束,而併發佇列的任務並不一定會按照開始的順序而結束。
    • 同步(Sync):會把當前的任務加入到佇列中,除非等到任務執行完成,執行緒才會返回繼續執行,也就是說同步會阻塞執行緒。
    • 非同步(Async):也會把當前的任務加入到佇列中,但它會立刻返回,無需等任務執行完成,也就是說非同步不會阻塞執行緒。
    • PS:無論是序列還是併發佇列都可以執行執行同步或非同步操作。注意在序列佇列上執行同步操作容易造成死鎖,在併發佇列上則不用擔心。非同步操作無論是在序列佇列還是併發佇列上都可能出現執行緒安全的問題。
  • GCD和NSOperation的區別?

    • NSOperation VS GCD
    1. 語法:物件導向(重量級) - 面向C(輕量級)
    2. 相比GCD的優點:
      1. 取消某個操作(未啟動的任務)、
      2. 指定操作間的依賴關係(根據需求指定依賴關係)
      3. 通過鍵值觀察機制監控NSOperation物件的屬性(KVO)
      4. 指定操作的優先順序
      5. 重用NSOperation物件
      6. 提供可選的完成block

執行緒安全

  • 如何保證執行緒安全?

    • 原子操作(Atomic Operation):是指不會被執行緒排程機制打斷的操作;這種操作一旦開始,就一直執行到結束,中間不會有任何的上下文切換(context switch)。
    • 記憶體屏障(Memory Barrier):確保記憶體操作以正確的順序發生,不阻塞執行緒(鎖的底層都會使用記憶體屏障,會減少編譯器執行優化的次數,謹慎使用)。
      • 互斥鎖(pthread_mutex):原理與訊號量類似,但其並非使用忙等,而是阻塞執行緒和休眠,需切換上下文。
      • 遞迴鎖(NSRecursiveLock):本質是封裝了互斥鎖的PTHREAD_MUTEX_RECURSIVE型別的鎖,允許同一個執行緒在未釋放其擁有的鎖時反覆對該鎖進行加鎖操作。
      • 自旋鎖(OSSpinLock):通過全域性變數,來判斷當前鎖是否可用,不可用就忙等。
      • @synchronized(self):系統提供的面向OC的API,通過把物件hash當做鎖來用。
      • NSLock:本質是封裝了互斥鎖的PTHREAD_MUTEX_ERRORCHECK型別的鎖,它會損失一定效能換來錯誤提示,因為物件導向的設計,其效能稍慢。
      • 條件變數(NSConditionLock):底層通過(condition variable)pthread_cond_t來實現的,類似訊號量具有阻塞執行緒與訊號機制,當某個等待的資料就緒後喚醒執行緒,比如常見的生產者-消費者模式。
      • 訊號量(dispatch_semaphore)
        // 傳入值需>=0,若傳0則阻塞執行緒並等待timeout
        dispatch_semaphore_create(1):
        // lock 資源已被鎖,會使得signal值-1
        dispatch_semaphore_wait(signal, overTime):
        // unlock,會使得signal值+1
        dispatch_semaphore_signal(signal):
        複製程式碼
  • 什麼是死鎖?如何避免死鎖?

    • 死鎖:兩個或兩個以上的執行緒,互相等待彼此停止以獲得某種資源,但是沒有一方會提前退出的情況。
    • 避免在序列佇列中執行同步任務;避免Operation相互依賴;
  • 什麼是優先倒置?

    • 低優先順序任務會先於高優先順序任務執行,假設:任務優先順序A>B>C,A等待訪問被C正在使用的臨界資源,同時B也要訪問該資源,B的優先順序高於C,同時,C優先順序任務被B次高優先順序的任務所搶先,從而無法及時地釋放該臨界資源。這種情況下,B次高優先順序任務獲得執行權,而高優先順序任務A只能被阻塞。 可以設定相同的任務優先順序避免優先倒置。

專案經驗相關題

  • 什麼時候重構?怎麼重構的?(包括但不限於以下幾點,僅供參考)
    • 當前架構支撐不了業務、當前設計模式弊端太多、專案急著上線沒有來得及優化等等。
    1. 優化業務邏輯;刪除沒用的程式碼(包括第三方庫、變數、方法等,註釋除外);
    2. 保持類和方法的單一性原則,把不屬於本類功能的部門移植到一個新類中
    3. 根據實際使用的需求、優化基類中的屬性(像BaseObject等)
    4. 修改一點就測試一點,保證每一步的重構不影響原有功能
    5. 規範(命名、語法等)
    6. 編譯優化
    • PS: 重構是一個長期的過程,每一次緊迫迭代的新功能都可能需要優化,特別是在多人開發的時候,如果來不及在專案釋出前Code Review,在重構時Code Review就顯得很有必要。
  • AppDelegate如何瘦身?
    • 主要是在保證功能和效率以及效能的前提下,把第三方的初始化程式碼拆解到別的檔案中處理
    1. category
      • 優點:簡單、直接,不需要任何第三方來協助;
      • 缺點:新增屬性不太方便,需要藉助關聯物件來實現;
    2. FRDModuleManager
      • 優點:簡單、易維護,耦合性低;
      • 缺點:模組增多需分配不少記憶體,物件都是長期持有的(若有依賴關係,優先順序不明確)
  • 如何解決卡頓?
    • 卡頓主要是因為主執行緒執行了一些耗時操作導致
    • 耗時計算:儘可能放到子執行緒中執行
    • 圖片解壓縮:在子執行緒強制解壓縮
    • 離屏渲染:避免導致離屏渲染(注意繪製方法)
    • 優化業務流程:減少中間層,業務邏輯
    • 合理分配執行緒:UI操作和資料來源放到主執行緒,保證主執行緒儘可能處理少的非UI操作,同時控制App子執行緒數量。
    • 預載入和延時載入:平衡CPU和GPU使用率;優先載入可視內容,提升介面繪製速度。
  • 如何排查Crash?
    • 斷點:一般的Crash
    • zombie object:殭屍物件
    • dSYM:iOS編譯後儲存16進位制函式地址對映資訊的檔案 (根據Crash的記憶體地址,定位對應的Crash程式碼的範圍)
  • 如何檢測記憶體洩漏?有沒有遇到記憶體警告?怎麼解決的?
    • dealloc方法:手動檢測的笨方法
    • 第三方庫PLeakSniffer和MLeaksFinder:自動檢測
  • 有何優化App啟動速度?(main前和main後)
    • 設定環境變數:DYLD_PRINT_STATISTICS或DYLD_PRINT_STATISTICS_DETAILS為1(更詳細),Xcode-> Edit Scheme-> Run-> Arguments,新增以上二選一 環境變數
        main之前:
            Total pre-main time:  94.33 milliseconds (100.0%) // main函式前總共需要94.33ms
            dylib loading time:  61.87 milliseconds (65.5%)    // 動態庫載入
            rebase/binding time:   3.09 milliseconds (3.2%)    // 指標重定位
            ObjC setup time:  10.78 milliseconds (11.4%)    // Objc類初始化
            initializer time:  18.50 milliseconds (19.6%)    // 各種初始化
            slowest intializers :    // 在各種初始化中,最耗時的如下:
                libSystem.B.dylib :   3.59 milliseconds (3.8%)
                libBacktraceRecording.dylib :   3.65 milliseconds (3.8%)
                GTFreeWifi :   7.09 milliseconds (7.5%)
    複製程式碼
    • main()函式之前耗時的影響因素

      • 動態庫載入越多,啟動越慢。
      • ObjC類越多,啟動越慢
      • C的constructor函式越多,啟動越慢
      • C++靜態物件越多,啟動越慢
      • ObjC的+load越多,啟動越慢
    • main()函式之後耗時的影響因素

      • 執行applicationWillFinishLaunching的耗時
      • rootViewController及其childViewController的載入、view及其subviews的載入
    • 具體優化內容:

      1. 移除不需要用到的動態庫
      2. 移除不需要用到的類
      3. 合併功能類似的類和擴充套件(Category)
      4. 壓縮資源圖片
      5. 優化applicationWillFinishLaunching
      6. 優化rootViewController載入
      7. 挖掘最後一點效能優化
    • PS:應該在400ms內完成main()函式之前的載入,整體過程耗時不能超過20秒,否則系統會kill掉程式,App啟動失敗

開源庫

這部分主要跟簡歷中提到的相關庫有關,建議對簡歷中提到的開源庫,一定要有所準備。

SDWebImage

SDWebImage幾乎是每個iOS開發者都用過的開源庫,也是在簡歷中曝光度比較高的開源庫之一,同時也幾乎是面試都會問到的,所以要準備充分再去。

  • 從呼叫到顯示的過程?
    1. 根據請求的檔案URL路徑判斷當前是否存在還沒結束的操作,若有的話就取消並移除與URL對映的操作。
    2. 設定placeholder佔點陣圖,不管URL是否為空都會設定,且在當前佇列同步執行。
    3. 判斷URL是否為空,為空則執行completedBlock回撥,並結束。
    4. URL非空,若是首次則開始相關類的初始化,如:載入進度及其相關的Block、SDWebImageManager(SDWebImageCache管理快取、SDWebImageDownloader管理),若非首次還是使用最初初始化的例項,因為SDWebImageManager是以單例的形式存在。
    5. 開始進入SDWebImageManager的範圍,URL容錯處理、建立負責載入圖片和取消載入圖片的類,判斷當前的URL是否在黑名單裡,若存在則執行回撥,返回當前的操作類,不再處理下載失敗過的URL,並結束。
    6. 若URL不在黑名單裡,則開始在記憶體快取中查詢,找到後執行doneBlock回撥,在該回撥方法中執行判斷當前操作是否已取消,未取消則回撥並非同步設定圖片,在回撥的Block中,每次都會檢查當前操作是否已取消,若取消則不處理,並結束。
    7. 記憶體快取沒找到,則在磁碟快取中查詢,一般是在序列佇列非同步執行,根據URL路徑找到儲存的資料並取出,然後縮放解壓縮返回,執行回撥設定圖片,並結束。
    8. 磁碟沒找到則下載圖片,下載完成後快取,設定圖片;SDWebImage對圖片作了哪些優化:子執行緒強制解壓縮,從硬碟獲取的圖片和下載的圖片都進行解壓縮操作,提高渲染效率,節省主執行緒的工作量。

ReactiveCocoa

該庫比較複雜,可問的問題也非常多,以下僅供參考,建議自己找答案(自己理解後才能從容面對)

  • 冷熱訊號的區別?
  • RAC如何監聽方法?
  • bind方法做了什麼?
  • RAC中的RACObserver和KVO有什麼區別?
  • RAC的map和flattenMap的區別?

工具

難免會遇到一些有關常用工具的問題,提供幾個僅供參考

  • Git、SVN?
    • 問題可深可淺,淺:基本用法或相關命令,深:Git的工作原理
  • CocoaPods
    • pod update和pod install的區別
  • CI(持續整合、持續部署)
    • 後期更新

相關文章