答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(一)

decemberSola發表於2018-03-21

離職找工作中,刷一刷網上的面試題。原文連結

1. 為什麼說Objective-C是一門動態語言

因為OC可以在執行時可以改變其結構:新的函式可以被引進,已有的函式可以被刪除等。所以,OC是一門動態語言。它具有相當多的動態特性,基本的,也是經常被提到和用到的有動態型別(Dynamic typing),動態繫結(Dynamic binding)和動態載入(Dynamic loading)

  1. 動態型別(Dynamic typing)
    執行時再決定物件的型別。簡單的說,就是id型別。id可以指向任意型別的物件,然後使用的時候再確定物件本來的型別。
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
    someClass *classSpecifiedInstance = (someClass *)obj;
    // Do Something to classSpecifiedInstance which now is an instance of someClass
    //...
}
複製程式碼
  1. 動態繫結(Dynamic binding)
    即是在例項所屬類確定後,將某些屬性和相應的方法繫結到例項上。例如class_addMethod這個方法就可以動態的新增方法。

  2. 動態載入(Dynamic loading)
    讓程式在執行時新增程式碼模組以及其他資源。使用者可以根據需要載入一些可執行程式碼和資源,而不是在啟動時就載入所有元件。比如@2x,@3x資源,就是在執行的時候根據不同的裝置載入不同的資源。

2. 講一下MVC和MVP,MVVM?

它們都是MVC的變種,結構劃分為:

  • view : 檢視
  • model : 業務資料
  • x(c,vm,p):業務邏輯的處理者,作為M、V的橋樑

其中mvp和mvvm中的v是包含了ViewController的。

看圖說話。

1. MVC

答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(一)

2. iOS開發實際應用時的MVC

答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(一)
用MVC開發的時候,View和Controller耦合會很嚴重, 像viewDidLoadviewWillAppear這些view的生命週期都會在controller裡面來管理。再加上controller還要負責代理、資料來源、網路請求等,於是controller就變得越來越龐大,越來越混亂,很不好測試。

3. MVP

答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(一)
跟MVC相比,我們把所有view相關的東西都化作view模組,其餘的邏輯放到一個模組,於是就有了MVP。MVP中的V包含了UIViewController,它負責所有跟UI相關的東西,比如view的生命週期管理,佈局。所以P的責任更加單一,只是通過資料和狀態更新View。由於V和P的分離,我們會寫很多事件傳遞的程式碼來連線V和P。比如:

import UIKit

struct Person { // Model
    let firstName: String
    let lastName: String
}

protocol GreetingView: class {
    func setGreeting(greeting: String)
}

protocol GreetingViewPresenter {
    init(view: GreetingView, person: Person)
    func showGreeting()
}

class GreetingPresenter : GreetingViewPresenter {
    unowned let view: GreetingView
    let person: Person
    required init(view: GreetingView, person: Person) {
        self.view = view
        self.person = person
    }
    func showGreeting() {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.view.setGreeting(greeting)
    }
}

class GreetingViewController : UIViewController, GreetingView {
    var presenter: GreetingViewPresenter!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }

    func didTapButton(button: UIButton) {
        self.presenter.showGreeting()
    }

    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }

    // layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
複製程式碼

MVP帶給我們更好的可測試性的同時又帶來了大量的程式碼。像上面這個例子裡,因為V和P的分離,在需要傳遞事件時,View中的一個方法只呼叫Presenter的一個方法的情況會時常發生

   func didTapButton(button: UIButton) {
        self.presenter.showGreeting()
    }
複製程式碼

4. MVVM.

答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(一)
MVVM它跟 MVP 很像:

  • 把 ViewController 看做 View。
  • View 和 Model 之間沒有緊耦合

MVVM和MVP的區別主要在於資料繫結這一塊。通過響應式程式設計的框架比如ReactiveCocoa來把View和ViewModel繫結在一起,這樣我們就不用寫很多重新整理頁面的程式碼了。

5. VIPER

答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(一)

最後說一下VIPER這個框架,它不屬於MV(X)架構,它更像是樂高積木一樣搭建你的應用。VIPER對職責劃分了5個模組。

  • View(頁面) - 展示給使用者的介面
  • Interactor(互動器) - 包括資料(Entities)或者網路相關的業務邏輯。比如建立新的 entities 或者從伺服器上獲取資料;要實現這些功能,你可能會用到一些服務和管理(Services and Managers):這些可能會被誤以為成是外部依賴東西,但是它們就是 VIPER 的 Interactor 模組。
  • Presenter(展示器) - 包括 UI(but UIKit independent)相關的業務邏輯,可以呼叫 Interactor 中的方法。
  • Entities(實體) - 純粹的資料物件。不包括資料訪問層,因為這是 Interactor 的職責。
  • Router(路由) - 負責 VIPER 模組之間的轉場

實際上 VIPER 模組可以只是一個頁面(screen),也可以是你應用裡整個的使用者使用流程(the whole user story)- 比如說「驗證」這個功能,它可以只是一個頁面,也可以是連續相關的一組頁面。你的每個「樂高積木」想要有多大,都是你自己來決定的。

這篇文章對幾個模式分析得很好。值得好好讀一讀。文中的幾個例子也是引自這篇文章。

3. 為什麼代理要用weak?代理的delegate和dataSource有什麼區別?block和代理的區別?

  • 避免迴圈引用。

    @interface SubObj : NSObject
    @property(nonatomic,strong) id delegate;
    @end
    
    @interface ViewController : UIViewController
    @property(nonatomic,strong)SubObj * obj;
    @end
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.obj = [[SubObj alloc] init];
        self.obj.delegate = self;
    }
    
    @end
    複製程式碼

    上面的程式碼中就出現了ViewController持有了SubObj,同時因為Suobj的delegate是強引用的,所以持有了ViewController,出現了迴圈引用。

  • delegate主要是事件拋到給代理來做。dataSource主要是資料來源。

  • 一般情況下,簡單功能的回撥用block,系列函式的回撥選擇delegate。

4. 屬性的實質是什麼?包括哪幾個部分?屬性預設的關鍵字都有哪些?@dynamic關鍵字和@synthesize關鍵字是用來做什麼的?

  • 屬性的實質就是變數+get方法+set方法
  • 屬性的關鍵字有:
    • 原子性nonatomic,atomic
    • 讀寫許可權readonly,readwrite
    • 指定讀寫方法getter,setter
    • 持有方式strong,assign,weak,copy,unsafe_unretained
    • 是否可以為空nullable,nonnull,null_resettable,null_unspecified
    • 類屬性class
  • @synthesize 表示由系統自動生成get和set方法,如果自己實現了get或者set方法則會替換掉系統生成的。@dynamic必須自己提供get和set方法。

5. 屬性的預設關鍵字是什麼

基本資料型別預設關鍵字是 atomic,readwrite,assign

其他型別預設關鍵字是 atomic,readwrite,strong

6. NSString為什麼要用copy關鍵字,如果用strong會有什麼問題?(注意:這裡沒有說用strong就一定不行。使用copy和strong是看情況而定的)

防止被修改。比如:

@interface ViewController : UIViewController

@property(nonatomic,strong)NSString * text;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *test =  [NSMutableString stringWithFormat:@"123"];
    self.text = test;
    [test appendString:@"456"];
}

@end
複製程式碼

這個時候列印self.text則變成了被修改後的值123456

7. 如何令自己所寫的物件具有拷貝功能?

實現NSCopying協議就讓物件有了拷貝功能。

8. 可變集合類 和 不可變集合類的 copy 和 mutablecopy有什麼區別?如果是集合是內容複製的話,集合裡面的元素也是內容複製麼?

  • 簡單的說,copy會生成一個不可變的物件,mutableCopy會生成一個可變物件

    NSArray *array = @[@1,@2];
    NSArray *array2 = [array copy];//不可變
    NSMutableArray *array3 = [array mutableCopy];//可變
    NSArray *array4 = [array3 copy];//不可變
    NSMutableArray *array5 = [array3 mutableCopy];//可變
    複製程式碼
  • 集合裡面的元素並沒有內容拷貝。還是原來的物件。

    NSArray<NSMutableString *> *array = @[[NSMutableString stringWithString:@"123"]];
    NSArray<NSMutableString *> *array2 = [array copy];
    NSMutableString *item = array2[0];
    [item appendString:@"456"];
    NSLog(@"%@",array);
    複製程式碼

    列印結果是

    (
        123456
    )
    複製程式碼

9.為什麼IBOutlet修飾的UIView也適用weak關鍵字?

因為view被新增到superView上面後,就被superView持有了。我們一般在IB裡面的拖的view都是加在了根view或者它的子view上。而根view又被它的controller持有,所以IBOutlet可以用weak。如果,在IB裡面拖出來的view是一個單獨的view沒有被加到任何其他view上,則需要用strong

10. nonatomic和atomic的區別?atomic是絕對的執行緒安全麼?為什麼?如果不是,那應該如何實現?

  • nonatomic表示非原子性,不安全,效率高。atomic表示原子性,效率低。

  • atomic不是絕對的執行緒安全的。

    @property (atomic, assign)    int       intA;
    
    //thread A
    for (int i = 0; i < 10000; i ++) {
        self.intA = self.intA + 1;
        NSLog(@"Thread A: %d\n", self.intA);
    }
    
    //thread B
    for (int i = 0; i < 10000; i ++) {
        self.intA = self.intA + 1;
        NSLog(@"Thread B: %d\n", self.intA);
    }
    複製程式碼

    即使我將intA宣告為atomic,最後的結果也不一定會是20000。原因就是因為self.intA = self.intA + 1;不是原子操作,雖然intA的getter和setter是原子操作,但當我們使用intA的時候,整個語句並不是原子的,這行賦值的程式碼至少包含讀取(load),+1(add),賦值(store)三步操作,當前執行緒store的時候可能其他執行緒已經執行了若干次store了,導致最後的值小於預期值。這種場景我們也可以稱之為多執行緒不安全。

    @property (atomic, strong) NSString*                 stringA;
    
    //thread A
    for (int i = 0; i < 100000; i ++) {
        if (i % 2 == 0) {
            self.stringA = @"a very long string";
        }
        else {
            self.stringA = @"string";
        }
        NSLog(@"Thread A: %@\n", self.stringA);
    }
    
    //thread B
    for (int i = 0; i < 100000; i ++) {
        if (self.stringA.length >= 10) {
            NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
        }
        NSLog(@"Thread B: %@\n", self.stringA);
    }
    複製程式碼

    雖然stringA是atomic的property,而且在取substring的時候做了length判斷,執行緒B還是很容易crash,因為在前一刻讀length的時候self.stringA = @"a very long string";,下一刻取substring的時候執行緒A已經將self.stringA = @"string";,立即出現out of bounds的Exception,crash,多執行緒不安全。

    這段例子引用於這篇文章

11. UICollectionView自定義layout如何實現?

繼承UICollectionViewLayout自己實現prepareLayout,collectionViewContentSize,layoutAttributesForElementsInRect:這三個方法。

12. 用StoryBoard開發介面有什麼弊端?如何避免?

平日裡開發,複用較多的模組用的xib來寫。單獨的模組,如設定介面用的storyboard來開發。

關於StoryBoard的討論可以參看喵神的這篇文章

13. 程式和執行緒的區別?同步非同步的區別?並行和併發的區別?

程式和執行緒的區別

程式(process)

狹義的定義:程式就是一段程式的執行過程。

廣義定義:程式是一個具有一定獨立功能的程式關於某次資料集合的一次執行活動,它是作業系統分配資源的基本單元。

簡單來講程式的概念主要有兩點:第一,程式是一個實體。每一個程式都有它自己的地址空間,一般情況下,包括文字區域(text region)、資料區域(data region)和堆疊(stack region)。文字區域儲存處理器執行的程式碼;資料區域儲存變數和程式執行期間使用的動態分配的記憶體;堆疊區域儲存著活動過程中呼叫的指令和本地變數。第二,程式是一個“執行中的程式”。程式是一個沒有生命的實體,只有處理器賦予程式生命時,它才能成為一個活動的實體,我們稱其為程式。

程式狀態:程式有三個狀態,就緒,執行和阻塞。就緒狀態其實就是獲取了除cpu外的所有資源,只要處理器分配資源馬上就可以執行。執行態就是獲取了處理器分配的資源,程式開始執行,阻塞態,當程式條件不夠時,需要等待條件滿足時候才能執行,如等待I/O操作的時候,此刻的狀態就叫阻塞態。

說說程式,程式是指令和資料的有序集合,其本身沒有任何運動的含義,是一個靜態的概念,而程式則是在處理機上的一次執行過程,它是一個動態的概念。程式是包含程式的,程式的執行離不開程式,程式中的文字區域就是程式碼區,也就是程式。

執行緒(thread)

通常在一個程式中可以包含若干個執行緒,當然一個程式中至少有一個執行緒,不然沒有存在的意義。執行緒可以利用程式所擁有的資源,在引入執行緒的作業系統中,通常都是把程式作為分配資源的基本單位,而把執行緒作為獨立執行和獨立排程的基本單位,由於執行緒比程式更小,基本上不擁有系統資源,故對它的排程所付出的開銷就會小得多,能更高效的提高系統多個程式間併發執行的程度。

引用於這篇文章

同步和非同步的區別

同步是序列的順序執行,非同步是並行的同時執行

並行和併發的區別

併發和並行的區別就是一個處理器同時處理多個任務和多個處理器或者是多核的處理器同時處理多個不同的任務。

前者是邏輯上的同時發生(simultaneous),而後者是物理上的同時發生.

併發性(concurrency),又稱共行性,是指能處理多個同時性活動的能力,併發事件之間不一定要同一時刻發生。

並行(parallelism)是指同時發生的兩個併發事件,具有併發的含義,而併發則不一定並行

14. 執行緒間通訊?

執行緒間通訊指的是:1、一個執行緒傳遞資料給另一個執行緒,2、在一個執行緒中執行完特定任務後,轉到另一個執行緒繼續執行任務。

在iOS中可以用這些方法來進行執行緒間通訊:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

dispatch_async(otherQueue, ^{
    // dosth
});

複製程式碼

15. GCD的一些常用的函式?(group,barrier,訊號量,執行緒同步)

  • dispatch_group:做完一組操作後再執行後續的程式碼

    它有兩種用法: 一種是dispatch_group_async

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"group one start");
    dispatch_group_async(group, queue, ^{
        //do something
    });
    
    dispatch_group_async(group, queue, ^{
        //do something
    });
    
    dispatch_group_notify(group, queue, ^{
        //do something
    });
    
    複製程式碼

    第二種是dispatch_group_enterdispatch_group_leave:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        //do something
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        //do something
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, queue, ^{
        //do something
    });
    
    複製程式碼
  • barrier

    • 通過dispatch_barrier_async新增的block會等到之前新增所有的block執行完畢再執行
    • 在dispatch_barrier_async之後新增的block會等到dispatch_barrier_async新增的block執行完畢再執行
    - (void)barrier {
        dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            // dosth1;
        });
        dispatch_async(queue, ^{
            // dosth2;
        });
        dispatch_barrier_async(queue, ^{
            // doBarrier;
        });
        dispatch_async(queue, ^{
            // dosth4;
        });
        dispatch_async(queue, ^{
            // dosth5;
        });
    }
    
    複製程式碼
  • dispatch_semaphore

    當我們多個執行緒要訪問同一個資源的時候,往往會設定一個訊號量,當訊號量大於0的時候,新的執行緒可以去操作這個資源,操作時訊號量-1,操作完後訊號量+1,當訊號量等於0的時候,必須等待,所以通過控制訊號量,我們可以控制能夠同時進行的併發數。

    訊號量有以下3個函式

    dispatch_semaphore_create //建立一個訊號量
    dispatch_semaphore_signal //訊號量+1
    dispatch_semaphore_wait //等待,直到訊號量大於0時,即可操作,同時將訊號量-1
    
    複製程式碼
    -(void)dispatchSignal{
        //crate的value表示,最多幾個資源可訪問
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);   
        dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
         
        //任務1
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 1");
            sleep(1);
            NSLog(@"complete task 1");
            dispatch_semaphore_signal(semaphore);       
        });
        //任務2
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 2");
            sleep(1);
            NSLog(@"complete task 2");
            dispatch_semaphore_signal(semaphore);       
        });
        //任務3
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 3");
            sleep(1);
            NSLog(@"complete task 3");
            dispatch_semaphore_signal(semaphore);       
        });   
    }
    複製程式碼

    執行結果為

    run task 1
    run task 2
    complete task 1
    complete task 2
    run task 3
    complete task 3
    複製程式碼

    由於設定的訊號值為2,先執行兩個執行緒,等執行完一個,才會繼續執行下一個,保證同一時間執行的執行緒數不超過2。

    如果我們把訊號量設定成1dispatch_semaphore_create(1),那麼執行結果就會變成順序執行

    run task 1
    complete task 1
    run task 2
    complete task 2
    run task 3
    complete task 3
    複製程式碼

16. 如何使用佇列來避免資源搶奪?

用鎖,或者把資源的操作放到單一執行緒中。

17. 資料持久化的幾個方案(fmdb用沒用過)

  • NSUserDefault
  • NSKeyedArchiver
  • CoreData
  • SQLite
  • fmdb
  • realm

18. 說一下AppDelegate的幾個方法?從後臺到前臺呼叫了哪些方法?第一次啟動呼叫了哪些方法?從前臺到後臺呼叫了哪些方法?

// 當應用程式啟動時(不包括已在後臺的情況下轉到前臺),呼叫此回撥。launchOptions是啟動引數,假如使用者通過點選push通知啟動的應用,這個引數裡會儲存一些push通知的資訊
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions NS_AVAILABLE_IOS(3_0);
– (void)applicationDidBecomeActive:(UIApplication *)application;

//應用即將從前臺狀態轉入後臺
- (void)applicationWillResignActive:(UIApplication *)application;
– (void)applicationDidEnterBackground:(UIApplication *)application NS_AVAILABLE_IOS(4_0);

//從後臺到前臺呼叫了:
– (void)applicationWillEnterForeground:(UIApplication *)application NS_AVAILABLE_IOS(4_0);
– (void)applicationDidBecomeActive:(UIApplication *)application;

複製程式碼

19. NSCache優於NSDictionary的幾點?

  • NSCache 採用LRU規則,會對超出限制的資料進行自動清除
  • NSCache 在系統記憶體很低時,會自動釋放一些物件
  • NSCache 是執行緒安全的,在多執行緒操作中,不需要對 Cache 加鎖
  • NSCache 的 Key 只是做強引用,不需要實現 NSCopying 協議

20. 知不知道Designated Initializer?使用它的時候有什麼需要注意的問題?

  • 類似於Swift中的初始化方法。便捷初始化方法必須呼叫指定初始化方法。
  • 需要注意的是當子類實現了新的指定初始化方法後,需要在子類的指定初始化方法裡面用super呼叫父類的指定初始化方法,並且子類其他的初始化方法需要呼叫到該指定初始化方法。

21. 實現description方法能取到什麼效果?

NSLog(@"%@")[NSString stringWithFormat:@"%@"]會轉換成description返回的字串。

22. objc使用什麼機制管理物件記憶體?

使用引用計數機制管理物件記憶體。

相關文章