1. 程式人生 > >iOS 經典全部面試題(上)

iOS 經典全部面試題(上)

索引

   @implementation Son : Father
   - (id)init
   {
       self = [super init];
       if (self) {
           NSLog(@"%@", NSStringFromClass([self class]));
           NSLog(@"%@", NSStringFromClass([super class]));
       }
       return self;
   }
   @end
  1. 22--55題,請看下篇。

1. 風格糾錯題

enter image description here修改完的程式碼:

修改方法有很多種,現給出一種做示例:

// .h檔案
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的程式碼,這是第一種修改方法,後面會給出第二種修改方法

typedef NS_ENUM(NSInteger, CYLSex) {
   CYLSexMan,
   CYLSexWoman
};

@interface CYLUser : NSObject<NSCopying>

@property (nonatomic, readonly, copy) NSString *name;
@property
(nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) CYLSex sex; - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex; @end

下面對具體修改的地方,分兩部分做下介紹:硬傷部分

 和 優化部分 。因為硬傷部分沒什麼技術含量,為了節省大家時間,放在後面講,大神請直接看優化部分

優化部分

  1. enum 建議使用 NS_ENUM 和 NS_OPTIONS 巨集來定義列舉型別,參見官方的 Adopting Modern Objective-C 一文:
//定義一個列舉
   typedef NS_ENUM(NSInteger, CYLSex) {
       CYLSexMan,
       CYLSexWoman
   };

(僅僅讓性別包含男和女可能並不嚴謹,最嚴謹的做法可以參考 這裡 。)

  1. age 屬性的型別:應避免使用基本型別,建議使用 Foundation 資料型別,對應關係如下:
   int -> NSInteger
   unsigned -> NSUInteger
   float -> CGFloat
   動畫時間 -> NSTimeInterval

同時考慮到 age 的特點,應使用 NSUInteger ,而非 int 。 這樣做的是基於64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》

  1. 如果工程專案非常龐大,需要拆分成不同的模組,可以在類、typedef巨集命名的時候使用字首。
  2. doLogIn方法不應寫在該類中: 

    雖然LogIn的命名不太清晰,但筆者猜測是login的意思, (勘誤:Login是名詞,LogIn 是動詞,都表示登陸的意思。見: Log in vs. login 

    登入操作屬於業務邏輯,觀察類名 UserModel ,以及屬性的命名方式,該類應該是一個 Model 而不是一個“ MVVM 模式下的 ViewModel ”:

無論是 MVC 模式還是 MVVM 模式,業務邏輯都不應當寫在 Model 裡:MVC 應在 C,MVVM 應在 VM。

(如果拋開命名規範,假設該類真的是 MVVM 模式裡的 ViewModel ,那麼 UserModel 這個類可能對應的是使用者註冊頁面,如果有特殊的業務需求,比如: -logIn 對應的應當是註冊並登入的一個 Button ,出現 -logIn 方法也可能是合理的。)

  1. doLogIn 方法命名不規範:添加了多餘的動詞字首。 請牢記:

如果方法表示讓物件執行一個動作,使用動詞打頭來命名,注意不要使用 dodoes 這種多餘的關鍵字,動詞本身的暗示就足夠了。

應為 -logIn (注意: Login 是名詞, LogIn 是動詞,都表示登陸。 見 Log in vs. login 

  1. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中不要用 with 來連線兩個引數: withAge:應當換為age:age: 已經足以清晰說明引數的作用,也不建議用 andAge: :通常情況下,即使有類似 withA:withB:的命名需求,也通常是使用withA:andB: 這種命名,用來表示方法執行了兩個相對獨立的操作(從設計上來說,這時候也可以拆分成兩個獨立的方法),它不應該用作闡明有多個引數,比如下面的:
//錯誤,不要使用"and"來連線引數
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//錯誤,不要使用"and"來闡明有多個引數
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正確,使用"and"來表示兩個相對獨立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
  1. 由於字串值可能會改變,所以要把相關屬性的“記憶體管理語義”宣告為 copy 。(原因在下文有詳細論述:***用@property宣告的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什麼?***)
  2. “性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用於設定“姓名”(Name)和“年齡”(Age)的初始值,那如何對“性別”(Sex)初始化?

Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的引數,secondary 初始化方法是一個或多個,並且提供一個或者更多的預設引數來呼叫 designated 初始化方法的初始化方法。舉例說明:

   // .m檔案
   // http://weibo.com/luohanchenyilong/
   // https://github.com/ChenYilong
   //

   @implementation CYLUser

   - (instancetype)initWithName:(NSString *)name
                            age:(NSUInteger)age
                            sex:(CYLSex)sex {
       if(self = [super init]) {
           _name = [name copy];
           _age = age;
           _sex = sex;
       }
       return self;
   }

   - (instancetype)initWithName:(NSString *)name
                            age:(NSUInteger)age {
       return [self initWithName:name age:age sex:nil];
   }

   @end

上面的程式碼中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。因為僅僅是呼叫類實現的 designated 初始化方法。

因為出題者沒有給出 .m 檔案,所以有兩種猜測:1:本來打算只設計一個 designated 初始化方法,但漏掉了“性別”(sex)屬性。那麼最終的修改程式碼就是上文給出的第一種修改方法。2:不打算初始時初始化“性別”(sex)屬性,打算後期再修改,如果是這種情況,那麼應該把“性別”(sex)屬性設為 readwrite 屬性,最終給出的修改程式碼應該是:

   // .h檔案
   // http://weibo.com/luohanchenyilong/
   // https://github.com/ChenYilong
   // 第二種修改方法(基於第一種修改方法的基礎上)

   typedef NS_ENUM(NSInteger, CYLSex) {
       CYLSexMan,
       CYLSexWoman
   };

   @interface CYLUser : NSObject<NSCopying>

   @property (nonatomic, readonly, copy) NSString *name;
   @property (nonatomic, readonly, assign) NSUInteger age;
   @property (nonatomic, readwrite, assign) CYLSex sex;

   - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
   - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
   + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;

   @end
  • 按照介面設計的慣例,如果設計了“初始化方法” (initializer),也應當搭配一個快捷構造方法。而快捷構造方法的返回值,建議為 instancetype,為保持一致性,init 方法和快捷構造方法的返回型別最好都用 instancetype。
  • 如果基於第一種修改方法:既然該類中已經有一個“初始化方法” (initializer),用於設定“姓名”(Name)、“年齡”(Age)和“性別”(Sex)的初始值: 那麼在設計對應 @property 時就應該儘量使用不可變的物件:其三個屬性都應該設為“只讀”。用初始化方法設定好屬性值之後,就不能再改變了。在本例中,仍需宣告屬性的“記憶體管理語義”。於是可以把屬性的定義改成這樣
       @property (nonatomic, readonly, copy) NSString *name;
       @property (nonatomic, readonly, assign) NSUInteger age;
       @property (nonatomic, readonly, assign) CYLSex sex;
  由於是隻讀屬性,所以編譯器不會為其建立對應的“設定方法”,即便如此,我們還是要寫上這些屬性的語義,以此表明初始化方法在設定這些屬性值時所用的方式。要是不寫明語義的話,該類的呼叫者就不知道初始化方法裡會拷貝這些屬性,他們有可能會在呼叫初始化方法之前自行拷貝屬性值。這種操作多餘而且低效。
  1. initUserModelWithUserName 如果改為 initWithName 會更加簡潔,而且足夠清晰。
  2. UserModel 如果改為 User 會更加簡潔,而且足夠清晰。
  3. UserSex如果改為Sex 會更加簡潔,而且足夠清晰。
  4. 第二個 @property 中 assign 和 nonatomic 調換位置。 推薦按照下面的格式來定義屬性
@property (nonatomic, readwrite, copy) NSString *name;

屬性的引數應該按照下面的順序排列: 原子性,讀寫 和 記憶體管理。 這樣做你的屬性更容易修改正確,並且更好閱讀。這在《禪與Objective-C程式設計藝術 >》裡有介紹。而且習慣上修改某個屬性的修飾符時,一般從屬性名從右向左搜尋需要修動的修飾符。最可能從最右邊開始修改這些屬性的修飾符,根據經驗這些修飾符被修改的可能性從高到底應為:記憶體管理 > 讀寫許可權 >原子操作。

硬傷部分

  1. 在-和(void)之間應該有一個空格
  2. enum 中駝峰命名法和下劃線命名法混用錯誤:列舉型別的命名規則和函式的命名規則相同:命名時使用駝峰命名法,勿使用下劃線命名法。
  3. enum 左括號前加一個空格,或者將左括號換到下一行
  4. enum 右括號後加一個空格
  5. UserModel :NSObject 應為UserModel : NSObject,也就是:右側少了一個空格。
  6. @interface 與 @property 屬性宣告中間應當間隔一行。
  7. 兩個方法定義之間不需要換行,有時為了區分方法的功能也可間隔一行,但示例程式碼中間隔了兩行。
  8. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名與引數之間多了空格。而且 - 與 (id) 之間少了空格。
  9. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名與引數之間多了空格:(NSString*)name 前多了空格。
  10. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中 (NSString*)name,應為 (NSString *)name,少了空格。
  11. doLogIn方法中的 `LogIn` 命名不清晰:筆者猜測是login的意思,應該是粗心手誤造成的。

(勘誤: Login 是名詞, LogIn 是動詞,都表示登陸的意思。見: Log in vs. login 

2. 什麼情況使用 weak 關鍵字,相比 assign 有什麼不同?

什麼情況使用 weak 關鍵字?

  1. 在 ARC 中,在有可能出現迴圈引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性

  2. 自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控制元件屬性一般也使用 weak;當然,也可以使用strong。在下文也有論述:***《IBOutlet連出來的檢視屬性為什麼可以被設定成weak?》***

不同點:

  1. weak 此特質表明該屬性定義了一種“非擁有關係” (nonowning relationship)。為這種屬性設定新值時,設定方法既不保留新值,也不釋放舊值。此特質同assign類似, 然而在屬性所指的物件遭到摧毀時,屬性值也會清空(nil out)。 而 assign 的“設定方法”只會執行鍼對“純量型別” (scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。

  2. assign 可以用非 OC 物件,而 weak 必須用於 OC 物件

3. 怎麼用 copy 關鍵字?

用途:

  1. NSString、NSArray、NSDictionary 等等經常使用copy關鍵字,是因為他們有對應的可變型別:NSMutableString、NSMutableArray、NSMutableDictionary;

block 使用 copy 是從 MRC 遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對於 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。如果不寫 copy ,該類的呼叫者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”,他們有可能會在呼叫之前自行拷貝屬性值。這種操作多餘而低效。你也許會感覺我這種做法有些怪異,不需要寫依然寫。如果你這樣想,其實是你“日用而不知”,你平時開發中是經常在用我說的這種做法的,比如下面的屬性不寫copy也行,但是你會選擇寫還是不寫呢?

@property (nonatomic, copy) NSString *userId;

- (instancetype)initWithUserId:(NSString *)userId {
   self = [super init];
   if (!self) {
       return nil;
   }
   _userId = [userId copy];
   return self;
}

enter image description here

下面做下解釋: copy 此特質所表達的所屬關係與 strong 類似。然而設定方法並不保留新值,而是將其“拷貝” (copy)。 當屬性型別為 NSString 時,經常用此特質來保護其封裝性,因為傳遞給設定方法的新值有可能指向一個 NSMutableString 類的例項。這個類是 NSString 的子類,表示一種可修改其值的字串,此時若是不拷貝字串,那麼設定完屬性之後,字串的值就可能會在物件不知情的情況下遭人更改。所以,這時就要拷貝一份“不可變” (immutable)的字串,確保物件中的字串值不會無意間變動。只要實現屬性所用的物件是“可變的” (mutable),就應該在設定新屬性值時拷貝一份。

用 @property 宣告 NSString、NSArray、NSDictionary 經常使用 copy 關鍵字,是因為他們有對應的可變型別:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進行賦值操作,為確保物件中的字串值不會無意間變動,應該在設定新屬性值時拷貝一份。

該問題在下文中也有論述:用@property宣告的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什麼?如果改用strong關鍵字,可能造成什麼問題?

4. 這個寫法會出什麼問題: @property (copy) NSMutableArray *array;

兩個問題:1、新增,刪除,修改陣列內的元素的時候,程式會因為找不到對應的方法而崩潰.因為 copy 就是複製一個不可變 NSArray 的物件;2、使用了 atomic 屬性會嚴重影響效能 ;

第1條的相關原因在下文中有論述***《用@property宣告的NSString(或NSArray,NSDictionary)經常使用 copy 關鍵字,為什麼?如果改用strong關鍵字,可能造成什麼問題?》*** 以及上文***《怎麼用 copy 關鍵字?》***也有論述。

比如下面的程式碼就會發生崩潰

// .h檔案
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的程式碼就會發生崩潰

@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m檔案
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的程式碼就會發生崩潰

NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];

接下來就會奔潰:

 -[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460

第2條原因,如下:

該屬性使用了同步鎖,會在建立時生成一些額外的程式碼用於幫助編寫多執行緒程式,這會帶來效能問題,通過宣告 nonatomic 可以節省這些雖然很小但是不必要額外開銷。

在預設情況下,由編譯器所合成的方法會通過鎖定機制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質,則不使用同步鎖。請注意,儘管沒有名為“atomic”的特質(如果某屬性不具備 nonatomic 特質,那它就是“原子的”(atomic))。

在iOS開發中,你會發現,幾乎所有屬性都宣告為 nonatomic。

一般情況下並不要求屬性必須是“原子的”,因為這並不能保證“執行緒安全” ( thread safety),若要實現“執行緒安全”的操作,還需採用更為深層的鎖定機制才行。例如,一個執行緒在連續多次讀取某屬性值的過程中有別的執行緒在同時改寫該值,那麼即便將屬性宣告為 atomic,也還是會讀到不同的屬性值。

因此,開發iOS程式時一般都會使用 nonatomic 屬性。但是在開發 Mac OS X 程式時,使用 atomic 屬性通常都不會有效能瓶頸。

5. 如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?

若想令自己所寫的物件具有拷貝功能,則需實現 NSCopying 協議。如果自定義的物件分為可變版本與不可變版本,那麼就要同時實現 NSCopying 與 NSMutableCopying 協議。

具體步驟:

  1. 需宣告該類遵從 NSCopying 協議
  2. 實現 NSCopying 協議。該協議只有一個方法:
- (id)copyWithZone:(NSZone *)zone;

注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。

以第一題的程式碼為例:

   // .h檔案
   // http://weibo.com/luohanchenyilong/
   // https://github.com/ChenYilong
   // 修改完的程式碼

   typedef NS_ENUM(NSInteger, CYLSex) {
       CYLSexMan,
       CYLSexWoman
   };

   @interface CYLUser : NSObject<NSCopying>

   @property (nonatomic, readonly, copy) NSString *name;
   @property (nonatomic, readonly, assign) NSUInteger age;
   @property (nonatomic, readonly, assign) CYLSex sex;

   - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
   + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;

   @end

然後實現協議中規定的方法:

- (id)copyWithZone:(NSZone *)zone {
	CYLUser *copy = [[[self class] allocWithZone:zone] 
		             initWithName:_name
 							      age:_age
						          sex:_sex];
	return copy;
}

但在實際的專案中,不可能這麼簡單,遇到更復雜一點,比如類物件中的資料結構可能並未在初始化方法中設定好,需要另行設定。舉個例子,假如 CYLUser 中含有一個數組,與其他 CYLUser 物件建立或解除朋友關係的那些方法都需要操作這個陣列。那麼在這種情況下,你得把這個包含朋友物件的陣列也一併拷貝過來。下面列出了實現此功能所需的全部程式碼:

// .h檔案
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 以第一題《風格糾錯題》裡的程式碼為例

typedef NS_ENUM(NSInteger, CYLSex) {
    CYLSexMan,
    CYLSexWoman
};

@interface CYLUser : NSObject<NSCopying>

@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (void)addFriend:(CYLUser *)user;
- (void)removeFriend:(CYLUser *)user;

@end

// .m檔案

// .m檔案
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//

@implementation CYLUser {
   NSMutableSet *_friends;
}

- (void)setName:(NSString *)name {
   _name = [name copy];
}

- (instancetype)initWithName:(NSString *)name
                        age:(NSUInteger)age
                        sex:(CYLSex)sex {
   if(self = [super init]) {
       _name = [name copy];
       _age = age;
       _sex = sex;
       _friends = [[NSMutableSet alloc] init];
   }
   return self;
}

- (void)addFriend:(CYLUser *)user {
   [_friends addObject:user];
}

- (void)removeFriend:(CYLUser *