<li id="fw3su"></li>
  • <li id="fw3su"></li>
  • <div id="fw3su"><tr id="fw3su"></tr></div>
    <dl id="fw3su"></dl>
  • <div id="fw3su"><tr id="fw3su"></tr></div>
  • <sup id="fw3su"></sup>
    <progress id="fw3su"></progress><div id="fw3su"><tr id="fw3su"></tr></div><input id="fw3su"><ins id="fw3su"></ins></input>

    關于一些 iOS 面試問題的解答

    這篇 post 主要是對知乎上 iOS程序員的問題列表 的回答, 也算是對自己已有的知識進行整理.

    如果你對本篇 post 中的回答有所疑問, 可以在下面留言. 如果有問題, 我一定會修改的 :-)

    問題以及回答

    1. 什么是 ARC? (ARC 是為了解決什么問題而誕生的?)

    ARC 是 Automatic Reference Counting 的縮寫, 即自動引用計數. 這是蘋果在 iOS5 中引入的內存管理機制. Objective-C 和 Swift 使用 ARC 追蹤和管理應用的內存使用. 這一機制使得開發者無需鍵入 retainrelease , 這不僅能夠降低程序崩潰和內存泄露的風險, 而且可以減少開發者的工作量, 能夠大幅度提升程序的 流暢性可預測性 . 但是 ARC 不適用于 Core Foundation 框架中, 仍然需要手動管理內存.

    2. 以下 keywords 有什么區別: assign vs weak , __block vs __weak

    assignweak 是用于在聲明屬性時, 為屬性指定內存管理的語義.

    • assign 用于簡單的賦值, 不改變屬性的引用計數, 用于 Objective-C 中的 NSInteger , CGFloat 以及 C 語言中 int , float , double 等數據類型.
    • weak 用于對象類型, 由于 weak 同樣不改變對象的引用計數且不持有對象實例, 當該對象廢棄時, 該弱引用自動失效并且被賦值為 nil , 所以它可以用于避免兩個強引用產生的 循環引用 導致內存無法釋放的問題.

    __block__weak 之間的卻是確實極大的, 不過它們都用于修飾變量.

    • 前者用于指明當前聲明的變量在被 block 捕獲之后, 可以在 block 中改變變量的值. 因為在 block 聲明的同時會截獲該 block 所使用的全部自動變量的值, 而這些值只在 block 中 只具有"使用權"而不具有"修改權" . 而 __block 說明符就為 block 提供了變量的修改權.
    • 后者是 所有權修飾符 , 什么是所有權修飾符? 這里涉及到另一個問題, 因為在 ARC 有效時, id 類型和對象類型同 C 語言中的其他類型不同, 必須附加所有權修飾符. 所有權修飾符一種有 4 種:
      • __strong
      • __weak
      • __unsafe_unretained
      • __autorelease
    • __weakweak 的區別只在于, 前者用于變量的聲明, 而后者用于屬性的聲明.

    3. __block 在 ARC 和非 ARC 下含義一樣嗎?

    __block 在 ARC 下捕獲的變量會被 block retain , 這樣可能導致循環引用, 所以必須要使用弱引用才能解決該問題. 而在非 ARC 下, 可以直接使用 __block 說明符修飾變量, 因為在非 ARC 下, block 不會 retain 捕獲的變量.

    4. 使用 nonatomic 一定是線程安全的嗎?

    nonatomic 的內存管理語義是 非原子 的, 非原子的操作本來就是線程不安全的, 而 atomic 的操作是原子的, 但是 并不意味著它是線程安全的 , 它會增加正確的幾率, 能夠更好的避免線程的錯誤, 但是它仍然是線程不安全的.

    當使用 nonatomic 的時候, 屬性的 setter 和 getter 操作是非原子的, 所以當多個線程同時對某一屬性進行讀和寫的操作, 屬性的最終結果是不能預測的.

    當使用 atomic 時, 雖然對屬性的讀和寫是原子的, 但是仍然可能出現線程錯誤: 當線程 A 進行寫操作, 這時其他線程的讀或寫操作會因為該操作的進行而等待. 當 A 線程的寫操作結束后, B 線程進行寫操作, 然后當 A 線程進行讀操作時, 卻獲得了在 B 線程中的值, 這就破壞了線程安全, 如果有線程 C 在 A 線程讀操作前 release 了該屬性, 那么還會導致程序崩潰. 所以僅僅使用 atomic 并不會使得線程安全, 我們還需要為線程添加 lock 來確保線程的安全.

    atomic 都不是一定線程安全的, nonatomic 就更不必多說了.

    5. 描述一個你遇到過的 retain cycle 例子.

    6. + (void)load;+ (void)initialize; 有什么用處?

    當類對象被引入項目時, runtime 會向每一個類對象發送 load 消息. load 方法還是非常的神奇的, 因為它會在 每一個類甚至分類 被引入時僅調用一次, 調用的順序是父類優先于子類, 子類優先于分類. 而且 load 方法不會被類自動繼承, 每一個類中的 load 方法都不需要像 viewDidLoad 方法一樣調用父類的方法. 由于 load 方法會在類被 import 時調用一次, 而這時往往是改變類的行為的最佳時機. 我在 DKNightVersion 中使用 method swizlling 來修改原有的方法時, 就是在分類 load 中實現的.

    initialize 方法和 load 方法有一些不同, 它雖然也會在整個 runtime 過程中調用一次, 但是它是在 該類的第一個方法執行之前 調用, 也就是說 initialize 的調用是 惰性 的, 它的實現也與我們在平時使用的惰性初始化屬性時基本相同. 我在實際的項目中并沒有遇到過必須使用這個方法的情況, 在該方法中主要做 靜態變量的設置 并用于 確保在實例初始化前某些條件必須滿足 .

    7. 為什么其他語言里叫函數調用, Objective-C 中是給對象發送消息 (談下對 runtime 的理解)

    我們在其他語言中比如說: C, Python, Java, C++, Haskell ... 中提到函數調用或者方法調用(面向對象). 函數調用是在編譯期就已經決定了會調用哪個函數(方法), 編譯器在編譯期就能檢查出函數的執行是否正確.

    然而 Objective-C(ObjC) 是一門動態的語言, 整個 ObjC 語言都是盡可能的將所有的工作推遲到運行時才決定. 它基于 runtime 來工作, runtime 就是 ObjC 的靈魂, 其核心就是消息發送 objc_msgSend .

    What makes Objective-C truly powerful is its runtime.

    所有的消息都會在運行時才會確定, [obj message] 在運行時會被轉化為 objc_msgSend(id self, SEL cmd, ...) 來執行, 它會在運行時從 選擇子表中尋找對應的選擇子 并將選擇子與實現進行綁定. 而如果沒有找到對應的實現, 就會進入類似黑魔法的消息轉發流程. 調用 + (BOOL)resolveInstanceMethod:(SEL)aSelector 方法, 我們可以在這個方法中 為類動態地生成方法 .

    我們幾乎可以使用 runtime 魔改 Objective-C 中的一切: class property object ivar method protocol , 而下面就是它的主要應用:

    • 內省
    • 為分類動態的添加屬性
    • 使用方法調劑修改原有的方法實現
    • ...

    8. 什么是 Method Swizzling?

    method swizzling 實際上就是一種在運行時動態修改原有方法的技術, 它實際上是基于 ObjC runtime 的特性, 而 method swizzling 的核心方法就是 method_exchangeImplementations(SEL origin, SEL swizzle) . 使用這個方法就可以在運行時動態地改變原有的方法實現, 在 DKNigtVersion (為 iOS 應用添加夜間模式) 中能夠看到大量 method swizzling 的使用, 方法的調用時機就是在上面提到的 load 方法中, 不在 initialize 方法中改變方法實現的原因是 initialize 可能會被子類所繼承并重新執行最終導致錯誤 , 而 load 并不會被繼承并重新執行.

    9. UIViewCALayer 有什么關系?

    看到這個問題不禁想到大一在網易面試時的經歷, 當時的兩位面試官就問了我這么一個問題, UIViewCALayer 是什么關系, 為什么要這么設計? 我已經忘記了當時是怎么回答的. 隱約記得當時說每一個 UIView 都會對應一個 CALayer 至于為什么這樣, 當時的我實在是太弱無法回答出來了.

    每一個 UIView 的身后對應一個 Core Animation 框架中的 CALayer .

    Many of the methods you call on UIView simply delegate to the layer

    在 iOS 上 當你處理一個一個有一個的 UIView 時實際上是在操作 CALayer . 盡管有的時候你并不知道 (直接操作 CALayer 并不會在對效率有著顯著的提升).

    UIView 實際上就是對 CALayer 的輕量級的封裝. UIView 繼承自 UIResponder 處理來自用戶的事件; CALayer 繼承自 NSObject 主要用于圖層的渲染和動畫. 這么設計有以下幾個原因:

    • 你可以通過操作 UIView 在一個更高的層級上處理與用戶的交互, 觸摸, 點擊, 拖拽等事件, 這些都是在 UIKit 這個層級上完成的.
    • UIViewNSView(AppKit) 的實現極其不同, 而使用 Core Animation 可以實現底層代碼地重用, 因為在 Mac 和 iOS 平臺上都使用著近乎相同的 Core Animation 代碼, 這樣我們可以對這個層級進行抽象在兩種平臺上產生 UIKitAppKit 用于不同平臺的框架.

    使用 CALayer 的唯一原因大概是便于移植到不同的平臺, 如果僅僅使用 Core Animation 層級, 處理用戶的交互時間需要寫更多的代碼.

    10. 如何高性能的給 UIImageView 加個圓角? (不準說 layer.cornerRadius !)

    一般情況下給 UIImageView 或者說 UIKit 的控件添加圓角都是改變 clipsToBoundslayer.cornerRadius , 這樣大約兩行代碼就可以解決. 但是, 這樣使用這樣的方法會 強制 Core Animation 提前渲染屏幕的離屏繪制 , 而離屏繪制就會為性能帶來負面影響.

    我們也可以使用另一種比較復雜的方式來為圖片添加圓角, 這里就用到了貝塞爾曲線.

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];  
    imageView.center = CGPointMake(200, 300);  
    UIImage *anotherImage = [UIImage imageNamed:@"image"];  
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);  
    [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
    	cornerRadius:50] addClip];
    [anotherImage drawInRect:imageView.bounds];
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();  
    UIGraphicsEndImageContext();  
    [self.view addSubview:imageView];
    
    

    在這里使用了貝塞爾曲線"切割"個這個圖片, 給 UIImageView 添加了的圓角.

    11. 使用 drawRect: 有什么影響?

    這個問題對于我來說確實有些難以回答, 我記得我在我人生的第一個 iOS 項目 SportJoin 中曾經使用這個方法來繪制圖形, 但是具體怎么做的, 我已經忘記了.

    這個方法的主要作用是根據傳入的 rect 來繪制圖像 參見文檔 . 這個方法的默認實現沒有做任何事情, 我們 可以 在這個方法中使用 Core GraphicsUIKit 來繪制視圖的內容.

    這個方法的調用機制也是非常特別. 當你調用 setNeedsDisplay 方法時, UIKit 將會把當前圖層標記為 dirty, 但還是會顯示原來的內容, 直到下一次的視圖渲染周期, 才會重新建立 Core Graphics 上下文, 然后將內存中的數據恢復出來, 使用 CGContextRef 進行繪制.

    12. ASIHttpRequest 或者 SDWebImage 里面給 UIImageView 加載圖片的邏輯是什么樣的?

    我曾經閱讀過 SDWebImage 的源代碼, 就在這里對如何給 UIImageView 加載圖片做一個總結吧, SDWebImage 中為 UIView 提供了一個分類叫做 WebCache, 這個分類中有一個最常用的接口, sd_setImageWithURL:placeholderImage: , 這個分類同時提供了很多類似的方法, 這些方法最終會調用一個同時具有 option progressBlock completionBlock 的方法, 而在這個類最終被調用的方法首先會檢查是否傳入了 placeholderImage 以及對應的參數, 并設置 placeholderImage .

    然后會獲取 SDWebImageManager 中的單例調用一個 downloadImageWithURL:... 的方法來獲取圖片, 而這個 manager 獲取圖片的過程有大體上分為兩部分, 它首先會在 SDWebImageCache 中尋找圖片是否有對應的緩存, 它會以 url 作為數據的索引先在內存中尋找是否有對應的緩存, 如果緩存未命中就會在磁盤中利用 MD5 處理過的 key 來繼續查詢對應的數據, 如果找到了, 就會把磁盤中的緩存備份到內存中.

    然而, 假設我們在內存和磁盤緩存中都沒有命中, 那么 manager 就會調用它持有的一個 SDWebImageDownloader 對象的方法 downloadImageWithURL:... 來下載圖片, 這個方法會在執行的過程中調用另一個方法 addProgressCallback:andCompletedBlock:fotURL:createCallback: 來存儲下載過程中和下載完成的回調, 當回調塊是第一次添加的時候, 方法會實例化一個 NSMutableURLRequestSDWebImageDownloaderOperation , 并將后者加入 downloader 持有的下載隊列開始圖片的異步下載.

    而在圖片下載完成之后, 就會在主線程設置 image, 完成整個圖像的異步下載和配置.

    13. 設計一個簡單的圖片內存緩存器 (包含移除策略)

    待我閱讀完 path 開源的 FastImageCache 的源代碼就來回答.

    14. 講講你用Instrument優化動畫性能的經歷

    15. loadView 的作用是什么?

    This method loads or creates a view and assigns it to the view property.

    loadViewUIViewController 的實例方法, 我們永遠不要直接調用這個方法 [self loadView] . 這在蘋果的 官方文檔 中已經明確的寫出了. loadView 會在獲取視圖控制器的 view 但是卻得到 nil 時被調用.

    loadView 的具體實現會做下面兩件事情中的一件:

    1. 如果你的視圖控制器關聯了一個 storyboard, 那么它就會加載 storyboard 中的視圖.

    2. 如果試圖控制器沒有關聯的 storyboard, 那么就會創建一個空的視圖, 并分配給 view 屬性

    如果你需要覆寫 loadView 方法:

    1. 你需要創建一個根視圖.
    2. 創建并初始化 view 的子視圖, 調用 addSubview: 方法將它們添加到父視圖上.
    3. 如果你使用了自動布局, 提供足夠的約束來保證視圖的位置.
    4. 將根視圖分配給 view 屬性.

    永遠不要在這個方法中調用 [super loadView] .

    16. viewWillLayoutSubviews 的作用是什么?

    viewWillLayoutSubviews 方法會在視圖的 bounds 改變時, 視圖會調整子視圖的位置, 我們可以在視圖控制器中覆寫這個方法在視圖放置子視圖前做出改變, 當屏幕的方向改變時, 這個方法會被調用.

    17. GCD 里面有哪幾種 Queue? 背后的線程模型是什么樣的?

    GCD 中 Queue 的種類還要看我們怎么進行分類, 如果根據同一時間內處理的操作數分類的話, GCD 中的 Queue 分為兩類

    1. Serial Dispatch Queue
    2. Concurrent Dispatch Queue

    一類是串行派發隊列, 它只使用一個線程, 會等待當前執行的操作結束后才會執行下一個操作, 它按照追加的順序進行處理. 另一類是并行派發隊列, 它同時使用多個線程, 如果當前的線程數足夠, 那么就不會等待正在執行的操作, 使用多個線程同時執行多個處理.

    另外的一種分類方式如下:

    1. Main Dispatch Queue
    2. Global Dispatch Queue
    3. Custom Dispatch Queue

    主線程只有一個, 它是一個串行的進程. 所有追加到 Main Dispatch Queue 中的處理都會在 RunLoop 在執行. Global Dispatch Queue 是所有應用程序都能使用的并行派發隊列, 它有 4 個執行優先級 High, Default, Low, Background. 當然我們也可以使用 dispatch_queue_create 創建派發隊列.

    18. Core Data 或者 sqlite 的讀寫是分線程的嗎? 死鎖如何解決?

    Core Data 和 sqlite 這兩個我還真沒深入用過, 我只在小的玩具應用上使用過 Core Data, 但是發現這貨實在是太難用了, 我就果斷放棄了, sqlite 我也用過, 每次輸入 SQL 語句的時候我多想吐槽, 寫一些簡單的還好, 復雜的就直接 Orz 了. 所以我一般會使用 levelDB 對進行數據的持久存儲.
    

    數據庫讀取操作一般都是多線程的, 在對數據進行讀取的時候, 我們要確保當前的狀態不會被修改, 所以加鎖, 防止由于線程競爭而出現的錯誤. 在 Core Data 中使用并行的最重要的規則是: 每一個 NSManagedObjectContext 必須只從創建它的進程中訪問 .

    19. http 的 POST 和 GET 有什么區別?

    根據 HTTP 協議的定義 GET 類型的請求是冪等的, 而 POST 請求是有副作用的, 也就是說 GET 用于獲取一些資源, 而 POST 用于改變一些資源, 這可能會創建新的資源或更新已有的資源.

    POST 請求比 GET 請求更加的安全, 因為你不會把信息添加到 URL 上的查詢字符串上. 所以使用 GET 來收集密碼或者一些敏感信息并不是什么好主意.

    最后, POST 請求比 GET 請求也可以傳輸更多的信息.

    20. 什么是 Binary search tree, 它的時間復雜度是多少?

    二叉搜索樹是一棵以二叉樹來組織的, 它搜索的時間復雜度 $O(h)$ 與樹的高度成正比, 最壞的運行時間是 $\Theta(\lg n)$.
    

    參考資料

    我來評幾句
    登錄后評論

    已發表評論數()

    相關站點

    +訂閱
    熱門文章
    11选五