工程師必看的 20 道 iOS 面試題 - 小專欄
在 iOS 開發中,語言的選擇是最初的一步。
Objective-C 是蘋果為 iOS 和 Mac 開發量身定製的語言。它隨著 iPhone 的出現而大火,直到今天國內外大多數的 App 依然是用 Objective-C 在寫。它一度在 TIOBE 排行榜上位列第三名,僅次於 Java 和 C。其市場佔有份額也遠超其他語言。看名字我們可以知道,它與 C 語言有千絲萬縷的聯繫,事實上也確實如此:Objective-C 是 C 語言的超集,它在 C 語言主體上加上了面向對象的特性。這是為了 App 開發的方便,同時也兼顧了語言的整體性能。
2014年來,Swift 橫空出世,功能不斷完善,逐漸成為 Apple 全力主推的官方編程語言。自發布以來,Swift 已經歷經4個版本的迭代。在 TIOBE 編程語言排行榜上的目前位列12位,超過 Ruby 並遠遠甩開其上代語言 Objective-C。從性能上來說,它的速度是 Objective-C 的2.6倍,Python 的8.4倍。更重要的是,Swift 是一門開源的語言,它的質量和進步接受著整個業界的建議、監督、關注。無論從哪個角度講,Swift 都將取代 Objective-C,成為 iOS 開發的主流語言。
現在的面試中,傳統大廠如BAT對 Objective-C 的語言進行較多考察,日常開發也是以 Objective-C為主。而因為 Swift 的高歌猛進,我們日後會看到關於 Swift 的問題越來越多。本文收錄總結了常見的 Swift 和 Objective-C 的面試題,希望對大家有所幫助。
Objective-C Basics1. 請說明並比較以下關鍵詞:strong, weak, assign, copystrong表示指向並擁有該對象。其修飾的對象引用計數會增加1。該對象只要引用計數不為0則不會被銷毀。當然強行將其設為nil可以銷毀它。
weak表示指向但不擁有該對象。其修飾的對象引用計數不會增加。無需手動設置,該對象會自行在內存中銷毀。
assign主要用於修飾基本數據類型,如NSInteger和CGFloat,這些數值主要存在於棧上。
weak 一般用來修飾對象,assign一般用來修飾基本數據類型。原因是assign修飾的對象被釋放後,指針的地址依然存在,造成野指針,在堆上容易造成崩潰。而棧上的內存系統會自動處理,不會造成野指針。
copy與strong類似。不同之處是strong的複製是多個指針指向同一個地址,而copy的複製每次會在內存中拷貝一份對象,指針指向不同地址。copy一般用在修飾有可變對應類型的不可變對象上,如NSString, NSArray, NSDictionary。
Objective-C 中,基本數據類型的默認關鍵字是atomic, readwrite, assign;普通屬性的默認關鍵字是atomic, readwrite, strong。
2. 請說明並比較以下關鍵詞:__weak
,__block
__weak
與weak基本相同。前者用於修飾變數(variable),後者用於修飾屬性(property)。__weak
主要用於防止block中的循環引用。
__block
也用於修飾變數。它是引用修飾,所以其修飾的值是動態變化的,即可以被重新賦值的。__block
用於修飾某些block內部將要修改的外部變數。
__weak
和__block
的使用場景幾乎與block息息相關。而所謂block,就是Objective-C對於閉包的實現。閉包就是沒有名字的函數,或者理解為指向函數的指針。
3. 請說明並比較以下關鍵詞:atomatic, nonatomic
atomic修飾的對象會保證setter和getter的完整性,任何線程對其訪問都可以得到一個完整的初始化後的對象。因為要保證操作完成,所以速度慢。它比nonatomic安全,但也並不是絕對的線程安全,例如多個線程同時調用set和get就會導致獲得的對象值不一樣。絕對的線程安全就要用關鍵詞synchronized。
nonatomic修飾的對象不保證setter和getter的完整性,所以多個線程對它進行訪問,它可能會返回未初始化的對象。正因為如此,它比atomic快,但也是線程不安全的。
4. 什麼是ARC?
ARC全稱是 Automatic Reference Counting,是Objective-C的內存管理機制。簡單地來說,就是代碼中自動加入了retain/release,原先需要手動添加的用來處理內存管理的引用計數的代碼可以自動地由編譯器完成了。
ARC的使用是為了解決對象retain和release匹配的問題。以前手動管理造成內存泄漏或者重複釋放的問題將不復存在。
以前需要手動的通過retain去為對象獲取內存,並用release釋放內存。所以以前的操作稱為MRC (Manual Reference Counting)。
5. 什麼情況下會出現循環引用?循環引用是指2個或以上對象互相強引用,導致所有對象無法釋放的現象。這是內存泄漏的一種情況。舉個例子:
class Father@8810953169 Father: NSObject@property (strong, nonatomic) Son *son;@5535706238class Son@8810953169 Son: NSObject@property (strong, nonatomic) Father *father; @5535706238
上述代碼有兩個類,分別為爸爸和兒子。爸爸對兒子強引用,兒子對爸爸強引用。這樣釋放兒子必須先釋放爸爸,要釋放爸爸必須先釋放兒子。如此一來,兩個對象都無法釋放。
解決方法是將Father中的Son對象屬性從strong改為weak。
內存泄漏可以用Xcode中的Debug Memory Graph去檢查,同時Xcode也會在runtime中自動彙報內存泄漏的問題。
6. 下面代碼中有什麼bug?- (void)viewDidLoad { UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)]; alertLabel.text = @"Wait 4 seconds..."; [self.view addSubview:alertLabel]; NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; [backgroundQueue addOperationWithBlock:^{ [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]]; alertLabel.text = @"Ready to go!」 }];}
Bug在於,在等了4秒之後,alertLabel並不會更新為Ready to Go。
原因是,所有UI的相關操作應該在主線程進行。當我們可以在一個後台線程中等待4秒,但是一定要在主線程中更新alertLabel。
最簡單的修正如下:
- (void)viewDidLoad { UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)]; alertLabel.text = @"Wait 4 seconds..."; [self.view addSubview:alertLabel]; NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; [backgroundQueue addOperationWithBlock:^{ [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ alertLabel.text = @"Ready to go!」 }]; }];}
7. 以scheduledTimerWithTimeInterval的方式觸發的timer,在滑動頁面上的列表時,timer會暫停,為什麼?該如何解決?
原因在於滑動時當前線程的runloop切換了mode用於列表滑動,導致timer暫停。
runloop中的mode主要用來指定事件在runloop中的優先順序,有以下幾種:
· Default(NSDefaultRunLoopMode):默認,一般情況下使用;
· Connection(NSConnectionReplyMode):一般系統用來處理NSConnection相關事件,開發者一般用不到;
· Modal(NSModalPanelRunLoopMode):處理modal panels事件;
· Event Tracking(NSEventTrackingRunLoopMode):用於處理拖拽和用戶交互的模式。
· Common(NSRunloopCommonModes):模式合集。默認包括Default,Modal,Event Tracking三大模式,可以處理幾乎所有事件。
回到題中的情境。滑動列表時,runloop的mode由原來的Default模式切換到了Event Tracking模式,timer原來好好的運行在Default模式中,被關閉後自然就停止工作了。
解決方法其一是將timer加入到NSRunloopCommonModes中。其二是將timer放到另一個線程中,然後開啟另一個線程的runloop,這樣可以保證與主線程互不干擾,而現在主線程正在處理頁面滑動。示例代碼如下:
// 方法1[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];// 方法2dispatch_async(dispatch_get_global_queue(0, 0), ^{ timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true]; [[NSRunLoop currentRunLoop] run];});
Swift Basics8. 類(class)和結構體(struct)有什麼區別?
Swift 中,類是引用類型,結構體是值類型。值類型在傳遞和賦值時將進行複製,而引用類型則只會使用引用對象的一個"指向"。所以他們兩者之間的區別就是兩個類型的區別。舉個簡單的例子,代碼如下
class Temperature { var value: Float = 37.0}class Person { var temp: Temperature? func sick() { temp?.value = 41.0 }}let A = Person()let B = Person()let temp = Temperature()A.temp = tempB.temp = tempA.sick()
上面這段代碼,由於 Temperature 是 class ,為引用類型,故 A 的 temp 和 B 的 temp指向同一個對象。A 的 temp修改了,B 的 temp 也隨之修改。這樣 A 和 B 的 temp 的值都被改成了41.0。如果將 Temperature 改為 struct,為值類型,則 A 的 temp 修改不影響 B 的 temp。內存中,引用類型諸如類是在堆(heap)上,而值類型諸如結構體實在棧(stack)上進行存儲和操作。相比於棧上的操作,堆上的操作更加複雜耗時,所以蘋果官方推薦使用結構體,這樣可以提高 App 運行的效率。
class有這幾個功能struct沒有的:
class可以繼承,這樣子類可以使用父類的特性和方法
類型轉換可以在runtime的時候檢查和解釋一個實例的類型
可以用deinit來釋放資源
一個類可以被多次引用
struct也有這樣幾個優勢:
結構較小,適用於複製操作,相比於一個class的實例被多次引用更加安全。
無須擔心內存memory leak或者多線程衝突問題
9. Swift 是面向對象還是函數式的編程語言?
推薦閱讀:
※第一期:那些有關EA職業評估抄襲的流言以及DIY的事 | EA認證 | 工程師技術移民 | CDR輔導
※Marty Cagan 這6條原則,讓你的工程師發揮雙倍的價值!
※粉碎工程師的技術筆記——細度的標準概念
※考消防工程師證書,你了解這幾個問題嗎?
※矽谷高級工程師自述:我是怎樣遠程工作4年的