[轉]《招聘一個靠譜的 iOS》—參考答案(上)

索引

  1. 風格糾錯題html

    1. 優化部分
    2. 硬傷部分
  2. 什麼狀況使用 weak 關鍵字,相比 assign 有什麼不一樣?
  3. 怎麼用 copy 關鍵字?
  4. 這個寫法會出什麼問題: @property (copy) NSMutableArray *array;
  5. 如何讓本身的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?
  6. @property 的本質是什麼?ivar、getter、setter 是如何生成並添加到這個類中的
  7. @protocol 和 category 中如何使用 @property
  8. runtime 如何實現 weak 屬性
  9. @property中有哪些屬性關鍵字?/ @property 後面能夠有哪些修飾符?
  10. weak屬性須要在dealloc中置nil麼?
  11. @synthesize和@dynamic分別有什麼做用?
  12. ARC下,不顯式指定任何屬性關鍵字時,默認的關鍵字都有哪些?
  13. 用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?若是改用strong關鍵字,可能形成什麼問題?
    1. 對非集合類對象的copy操做
    2. 集合類對象的copy與mutableCopy
  14. @synthesize合成實例變量的規則是什麼?假如property名爲foo,存在一個名爲_foo的實例變量,那麼還會自動合成新變量麼?
  15. 在有了自動合成屬性實例變量以後,@synthesize還有哪些使用場景?
  16. objc中向一個nil對象發送消息將會發生什麼?
  17. objc中向一個對象發送消息[obj foo]和objc_msgSend()函數之間有什麼關係?
  18. 何時會報unrecognized selector的異常?
  19. 一個objc對象如何進行內存佈局?(考慮有父類的狀況)
  20. 一個objc對象的isa的指針指向什麼?有什麼做用?
  21. 下面的代碼輸出什麼?ios

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

1. 風格糾錯題

enter image description here修改完的代碼:git

修改方法有不少種,現給出一種作示例:github

// .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

下面對具體修改的地方,分兩部分作下介紹:硬傷部分優化部分 。由於硬傷部分沒什麼技術含量,爲了節省你們時間,放在後面講,大神請直接看優化部分web

優化部分

  1. enum 建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:編程

    //定義一個枚舉
    typedef NS_ENUM(NSInteger, CYLSex) {
        CYLSexMan,
        CYLSexWoman
    };

    (僅僅讓性別包含男和女可能並不嚴謹,最嚴謹的作法能夠參考 這裏 。)api

  2. age 屬性的類型:應避免使用基本類型,建議使用 Foundation 數據類型,對應關係以下:數組

    int -> NSInteger
      unsigned -> NSUInteger
      float -> CGFloat
      動畫時間 -> NSTimeInterval

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

  3. 若是工程項目很是龐大,須要拆分紅不一樣的模塊,能夠在類、typedef宏命名的時候使用前綴。
  4. 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 方法也多是合理的。)

  5. doLogIn 方法命名不規範:添加了多餘的動詞前綴。 請牢記:

    若是方法表示讓對象執行一個動做,使用動詞打頭來命名,注意不要使用 dodoes 這種多餘的關鍵字,動詞自己的暗示就足夠了。

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

  6. -(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;
  7. 因爲字符串值可能會改變,因此要把相關屬性的「內存管理語義」聲明爲 copy 。(緣由在下文有詳細論述:用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?)
  8. 「性別」(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

    .h 中暴露 designated 初始化方法,是爲了方便子類化 (想了解更多,請戳--》 《禪與 Objective-C 編程藝術 (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》。)

  9. 按照接口設計的慣例,若是設計了「初始化方法」 (initializer),也應當搭配一個快捷構造方法。而快捷構造方法的返回值,建議爲 instancetype,爲保持一致性,init 方法和快捷構造方法的返回類型最好都用 instancetype。
  10. 若是基於第一種修改方法:既然該類中已經有一個「初始化方法」 (initializer),用於設置「姓名」(Name)、「年齡」(Age)和「性別」(Sex)的初始值: 那麼在設計對應 @property 時就應該儘可能使用不可變的對象:其三個屬性都應該設爲「只讀」。用初始化方法設置好屬性值以後,就不能再改變了。在本例中,仍需聲明屬性的「內存管理語義」。因而能夠把屬性的定義改爲這樣

    @property (nonatomic, readonly, copy) NSString *name;
          @property (nonatomic, readonly, assign) NSUInteger age;
          @property (nonatomic, readonly, assign) CYLSex sex;
    因爲是隻讀屬性,因此編譯器不會爲其建立對應的「設置方法」,即使如此,咱們仍是要寫上這些屬性的語義,以此代表初始化方法在設置這些屬性值時所用的方式。要是不寫明語義的話,該類的調用者就不知道初始化方法裏會拷貝這些屬性,他們有可能會在調用初始化方法以前自行拷貝屬性值。這種操做多餘並且低效。
  11. initUserModelWithUserName 若是改成 initWithName 會更加簡潔,並且足夠清晰。
  12. UserModel 若是改成 User 會更加簡潔,並且足夠清晰。
  13. UserSex若是改成Sex 會更加簡潔,並且足夠清晰。
  14. 第二個 @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. assigin 能夠用非 OC 對象,而 weak 必須用於 OC 對象

3. 怎麼用 copy 關鍵字?

用途:

  1. NSString、NSArray、NSDictionary 等等常用copy關鍵字,是由於他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
  2. block 也常用 copy 關鍵字,具體緣由見官方文檔:Objects Use Properties to Keep Track of Blocks

    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;

兩個問題:一、添加,刪除,修改數組內的元素的時候,程序會由於找不到對應的方法而崩潰.由於 copy 就是複製一個不可變 NSArray 的對象;二、使用了 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 *)user { [_friends removeObject:user]; } - (id)copyWithZone:(NSZone *)zone { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [_friends mutableCopy]; return copy; } - (id)deepCopy { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; } @end 

以上作法能知足基本的需求,可是也有缺陷:

若是你所寫的對象須要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。

【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的 NSString(或NSArray,NSDictionary)常用 copy 關鍵字,爲何?若是改用 strong 關鍵字,可能形成什麼問題?

在例子中,存放朋友對象的 set 是用 「copyWithZone:」 方法來拷貝的,這種淺拷貝方式不會逐個複製 set 中的元素。若須要深拷貝的話,則可像下面這樣,編寫一個專供深拷貝所用的方法:

- (id)deepCopy {
    CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; } 

至於如何重寫帶 copy 關鍵字的 setter這個問題,

若是拋開本例來回答的話,以下:

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

不過也有爭議,有人說「蘋果若是像下面這樣幹,是否是效率會高一些?」

- (void)setName:(NSString *)name { if (_name != name) { //[_name release];//MRC _name = [name copy]; } }

這樣真得高效嗎?不見得!這種寫法「看上去很美、很合理」,但在實際開發中,它更像下圖裏的作法:

enter image description here

克強總理這樣評價你的代碼風格:

enter image description here

我和總理的意見基本一致:

老百姓 copy 一下,咋就這麼難?

你可能會說:

之因此在這裏作if判斷 這個操做:是由於一個 if 可能避免一個耗時的copy,仍是很划算的。 (在剛剛講的:《如何讓本身的類用 copy 修飾符?》裏的那種複雜的copy,咱們能夠稱之爲 「耗時的copy」,可是對 NSString 的 copy 還稱不上。)

可是你有沒有考慮過代價:

你每次調用 setX: 都會作 if 判斷,這會讓 setX: 變慢,若是你在 setX:寫了一串複雜的 if+elseif+elseif+... 判斷,將會更慢。

要回答「哪一個效率會高一些?」這個問題,不能脫離實際開發,就算 copy 操做十分耗時,if 判斷也不見得必定會更快,除非你把一個「 @property他當前的值 」賦給了他本身,代碼看起來就像:

[a setX:x1];
[a setX:x1]; //你肯定你要這麼幹?與其在setter中判斷,爲何不把代碼寫好?

或者

[a setX:[a x]]; //隊友咆哮道:你在幹嗎?!!

不要在 setter 裏進行像 if(_obj != newObj) 這樣的判斷。(該觀點參考連接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure 

什麼狀況會在 copy setter 裏作 if 判斷? 例如,車速可能就有最高速的限制,車速也不可能出現負值,若是車子的最高速爲300,則 setter 的方法就要改寫成這樣:

-(void)setSpeed:(int)_speed{ if(_speed < 0) speed = 0; if(_speed > 300) speed = 300; _speed = speed; }

回到這個題目,若是單單就上文的代碼而言,咱們不須要也不能重寫 name 的 setter :因爲是 name 是隻讀屬性,因此編譯器不會爲其建立對應的「設置方法」,用初始化方法設置好屬性值以後,就不能再改變了。( 在本例中,之因此還要聲明屬性的「內存管理語義」--copy,是由於:若是不寫 copy,該類的調用者就不知道初始化方法裏會拷貝這些屬性,他們有可能會在調用初始化方法以前自行拷貝屬性值。這種操做多餘而低效)。

那如何確保 name 被 copy?在初始化方法(initializer)中作:

    - (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; } 

6. @property 的本質是什麼?ivar、getter、setter 是如何生成並添加到這個類中的

@property 的本質是什麼?

@property = ivar + getter + setter;

下面解釋下:

「屬性」 (property)有兩大概念:ivar(實例變量)、存取方法(access method = getter + setter)。

「屬性」 (property)做爲 Objective-C 的一項特性,主要的做用就在於封裝對象中的數據。 Objective-C 對象一般會把其所須要的數據保存爲各類實例變量。實例變量通常經過「存取方法」(access method)來訪問。其中,「獲取方法」 (getter)用於讀取變量值,而「設置方法」 (setter)用於寫入變量值。這個概念已經定型,而且經由「屬性」這一特性而成爲 Objective-C 2.0 的一部分。 而在正規的 Objective-C 編碼風格中,存取方法有着嚴格的命名規範。 正由於有了這種嚴格的命名規範,因此 Objective-C 這門語言才能根據名稱自動建立出存取方法。其實也能夠把屬性當作一種關鍵字,其表示:

編譯器會自動寫出一套存取方法,用以訪問給定類型中具備給定名稱的變量。 因此你也能夠這麼說:

@property = getter + setter;

例以下面這個類:

@interface Person : NSObject @property NSString *firstName; @property NSString *lastName; @end

上述代碼寫出來的類與下面這種寫法等效:

@interface Person : NSObject - (NSString *)firstName; - (void)setFirstName:(NSString *)firstName; - (NSString *)lastName; - (void)setLastName:(NSString *)lastName; @end

ivar、getter、setter 是如何生成並添加到這個類中的?

「自動合成」( autosynthesis)

完成屬性定義後,編譯器會自動編寫訪問這些屬性所需的方法,此過程叫作「自動合成」(autosynthesis)。須要強調的是,這個過程由編譯 器在編譯期執行,因此編輯器裏看不到這些「合成方法」(synthesized method)的源代碼。除了生成方法代碼 getter、setter 以外,編譯器還要自動向類中添加適當類型的實例變量,而且在屬性名前面加下劃線,以此做爲實例變量的名字。在前例中,會生成兩個實例變量,其名稱分別爲 _firstName 與 _lastName。也能夠在類的實現代碼裏經過 @synthesize 語法來指定實例變量的名字.

@implementation Person @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end

我爲了搞清屬性是怎麼實現的,曾經反編譯過相關的代碼,他大體生成了五個東西

  1. OBJC_IVAR_$類名$屬性名稱 :該屬性的「偏移量」 (offset),這個偏移量是「硬編碼」 (hardcode),表示該變量距離存放對象的內存區域的起始地址有多遠。
  2. setter 與 getter 方法對應的實現函數
  3. ivar_list :成員變量列表
  4. method_list :方法列表
  5. prop_list :屬性列表

也就是說咱們每次在增長一個屬性,系統都會在 ivar_list 中添加一個成員變量的描述,在 method_list 中增長 setter 與 getter 方法的描述,在屬性列表中增長一個屬性的描述,而後計算該屬性在對象中的偏移量,而後給出 setter 與 getter 方法對應的實現,在 setter 方法中從偏移量的位置開始賦值,在 getter 方法中從偏移量開始取值,爲了可以讀取正確字節數,系統對象偏移量的指針類型進行了類型強轉.

7. @protocol 和 category 中如何使用 @property

  1. 在 protocol 中使用 property 只會生成 setter 和 getter 方法聲明,咱們使用屬性的目的,是但願遵照我協議的對象能實現該屬性
  2. category 使用 @property 也是隻會生成 setter 和 getter 方法的聲明,若是咱們真的須要給 category 增長屬性的實現,須要藉助於運行時的兩個函數:

    1. objc_setAssociatedObject
    2. objc_getAssociatedObject

8. runtime 如何實現 weak 屬性

要實現 weak 屬性,首先要搞清楚 weak 屬性的特色:

weak 此特質代表該屬性定義了一種「非擁有關係」 (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同 assign 相似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。

那麼 runtime 如何實現 weak 變量的自動置nil?

runtime 對註冊的類, 會進行佈局,對於 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。

(注:在下文的《使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?》裏給出的「對象的內存銷燬時間表」也提到__weak引用的解除時間。)

咱們能夠設計一個函數(僞代碼)來表示上述機制:

objc_storeWeak(&a, b)函數:

objc_storeWeak函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak修飾的屬性變量(a)的內存地址(&a)做爲value,註冊到 weak 表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak表中刪除,

你能夠把objc_storeWeak(&a, b)理解爲:objc_storeWeak(value, key),而且當key變nil,將value置nil。

在b非nil時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。

而若是a是由 assign 修飾的,則: 在 b 非 nil 時,a 和 b 指向同一個內存地址,在 b 變 nil 時,a 仍是指向該內存地址,變野指針。此時向 a 發送消息極易崩潰。

下面咱們將基於objc_storeWeak(&a, b)函數,使用僞代碼模擬「runtime如何實現weak屬性」:

// 使用僞代碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong id obj1; objc_initWeak(&obj1, obj); /*obj引用計數變爲0,變量做用域結束*/ objc_destroyWeak(&obj1);

下面對用到的兩個方法objc_initWeakobjc_destroyWeak作下解釋:

整體說來,做用是: 經過objc_initWeak函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak函數釋放該變量(obj1)。

下面分別介紹下方法的內部實現:

objc_initWeak函數的實現是這樣的:在將「附有weak修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak函數。

obj1 = 0;
obj_storeWeak(&obj1, obj);

也就是說:

weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的)

而後obj_destroyWeak函數將0(nil)做爲參數,調用objc_storeWeak函數。

objc_storeWeak(&obj1, 0);

前面的源代碼與下列源代碼相同。

// 使用僞代碼模擬:runtime如何實現weak屬性
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); /* ... obj的引用計數變爲0,被置nil ... */ objc_storeWeak(&obj1, 0);

objc_storeWeak 函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak 表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從 weak 表中刪除,在後面的相關一題會詳解。

使用僞代碼是爲了方便理解,下面咱們「真槍實彈」地實現下:

如何讓不使用weak修飾的@property,擁有weak的效果。

咱們從setter方法入手:

- (void)setObject:(NSObject *)object
{
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; }

也就是有兩個步驟:

  1. 在setter方法中作以下設置:

    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
  2. 在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。作到這點,一樣要藉助 runtime:

    //要銷燬的目標對象
    id objectToBeDeallocated;
    //能夠理解爲一個「事件」:當上面的目標對象銷燬時,同時要發生的「事件」。
    id objectWeWantToBeReleasedWhenThatHappens;
    objc_setAssociatedObject(objectToBeDeallocted,
                         someUniqueKey,
                         objectWeWantToBeReleasedWhenThatHappens,
                         OBJC_ASSOCIATION_RETAIN);

知道了思路,咱們就開始實現 cyl_runAtDealloc 方法,實現過程分兩部分:

第一部分:建立一個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助 block 執行「事件」。

// .h文件

// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 這個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。 typedef void (^voidBlock)(void); @interface CYLBlockExecutor : NSObject - (id)initWithBlock:(voidBlock)block; @end

// .m文件

// .m文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 這個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。 #import "CYLBlockExecutor.h" @interface CYLBlockExecutor() { voidBlock _block; } @implementation CYLBlockExecutor - (id)initWithBlock:(voidBlock)aBlock { self = [super init]; if (self) { _block = [aBlock copy]; } return self; } - (void)dealloc { _block ? _block() : nil; } @end

第二部分:核心代碼:利用runtime實現cyl_runAtDealloc方法

// CYLNSObject+RunAtDealloc.h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 利用runtime實現cyl_runAtDealloc方法 #import "CYLBlockExecutor.h" const void *runAtDeallocBlockKey = &runAtDeallocBlockKey; @interface NSObject (CYLRunAtDealloc) - (void)cyl_runAtDealloc:(voidBlock)block; @end // CYLNSObject+RunAtDealloc.m文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 利用runtime實現cyl_runAtDealloc方法 #import "CYLNSObject+RunAtDealloc.h" #import "CYLBlockExecutor.h" @implementation NSObject (CYLRunAtDealloc) - (void)cyl_runAtDealloc:(voidBlock)block { if (block) { CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block]; objc_setAssociatedObject(self, runAtDeallocBlockKey, executor, OBJC_ASSOCIATION_RETAIN); } } @end

使用方法: 導入

    #import "CYLNSObject+RunAtDealloc.h"

而後就可使用了:

NSObject *foo = [[NSObject alloc] init]; [foo cyl_runAtDealloc:^{ NSLog(@"正在釋放foo!"); }];

若是對 cyl_runAtDealloc 的實現原理有興趣,能夠看下這篇博文 Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object

9. @property中有哪些屬性關鍵字?/ @property 後面能夠有哪些修飾符?

屬性能夠擁有的特質分爲四類:

  1. 原子性--- nonatomic 特質

    在默認狀況下,由編譯器合成的方法會經過鎖定機制確保其原子性(atomicity)。若是屬性具有 nonatomic 特質,則不使用同步鎖。請注意,儘管沒有名爲「atomic」的特質(若是某屬性不具有 nonatomic 特質,那它就是「原子的」 ( atomic) ),可是仍然能夠在屬性特質中寫明這一點,編譯器不會報錯。如果本身定義存取方法,那麼就應該聽從與屬性特質相符的原子性。

  2. 讀/寫權限---readwrite(讀寫)readonly (只讀)
  3. 內存管理語義---assignstrong、 weakunsafe_unretainedcopy
  4. 方法名---getter=<name> 、setter=<name>

    getter=<name>的樣式:

    @property (nonatomic, getter=isOn) BOOL on;

    ( `setter=`這種不經常使用,也不推薦使用。故不在這裏給出寫法。)

    setter=<name>通常用在特殊的情境下,好比:

在數據反序列化、轉模型的過程當中,服務器返回的字段若是以 init 開頭,因此你須要定義一個 init 開頭的屬性,但默認生成的 setter 與 getter 方法也會以 init 開頭,而編譯器會把全部以 init 開頭的方法當成初始化方法,而初始化方法只能返回 self 類型,所以編譯器會報錯。

這時你就可使用下面的方式來避免編譯器報錯:

@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;

另外也能夠用關鍵字進行特殊說明,來避免編譯器報錯:

@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy __attribute__((objc_method_family(none)));
  1. 不經常使用的:nonnull,null_resettable,nullable

10. weak屬性須要在dealloc中置nil麼?

不須要。

在ARC環境不管是強指針仍是弱指針都無需在 dealloc 設置爲 nil , ARC 會自動幫咱們處理

即使是編譯器不幫咱們作這些,weak也不須要在 dealloc 中置nil:

正如上文的:runtime 如何實現 weak 屬性 中提到的:

咱們模擬下 weak 的 setter 方法,應該以下:

- (void)setObject:(NSObject *)object
{
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; }

也即:

在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。

11. @synthesize和@dynamic分別有什麼做用?

  1. @property有兩個對應的詞,一個是 @synthesize,一個是 @dynamic。若是 @synthesize和 @dynamic都沒寫,那麼默認的就是@syntheszie var = _var;
  2. @synthesize 的語義是若是你沒有手動實現 setter 方法和 getter 方法,那麼編譯器會自動爲你加上這兩個方法。
  3. @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶本身實現,不自動生成。(固然對於 readonly 的屬性只需提供 getter 便可)。假如一個屬性被聲明爲 @dynamic var,而後你沒有提供 @setter方法和 @getter 方法,編譯的時候沒問題,可是當程序運行到 instance.var = someVar,因爲缺 setter 方法會致使程序崩潰;或者當運行到 someVar = var時,因爲缺 getter 方法一樣會致使崩潰。編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。

12. ARC下,不顯式指定任何屬性關鍵字時,默認的關鍵字都有哪些?

  1. 對應基本數據類型默認關鍵字是

    atomic,readwrite,assign

  2. 對於普通的 Objective-C 對象

    atomic,readwrite,strong

參考連接:

  1. Objective-C ARC: strong vs retain and weak vs assign
  2. Variable property attributes or Modifiers in iOS

13. 用@property聲明的NSString(或NSArray,NSDictionary)常用copy關鍵字,爲何?若是改用strong關鍵字,可能形成什麼問題?

  1. 由於父類指針能夠指向子類對象,使用 copy 的目的是爲了讓本對象的屬性不受外界影響,使用 copy 不管給我傳入是一個可變對象仍是不可對象,我自己持有的就是一個不可變的副本.
  2. 若是咱們使用是 strong ,那麼這個屬性就有可能指向一個可變對象,若是這個可變對象在外部被修改了,那麼會影響該屬性.

    copy 此特質所表達的所屬關係與 strong 相似。然而設置方法並不保留新值,而是將其「拷貝」 (copy)。 當屬性類型爲 NSString 時,常常用此特質來保護其封裝性,由於傳遞給設置方法的新值有可能指向一個 NSMutableString 類的實例。這個類是 NSString 的子類,表示一種可修改其值的字符串,此時如果不拷貝字符串,那麼設置完屬性以後,字符串的值就可能會在對象不知情的狀況下遭人更改。因此,這時就要拷貝一份「不可變」 (immutable)的字符串,確保對象中的字符串值不會無心間變更。只要實現屬性所用的對象是「可變的」 (mutable),就應該在設置新屬性值時拷貝一份。

舉例說明:

定義一個以 strong 修飾的 array:

@property (nonatomic ,readwrite, strong) NSArray *array;

而後進行下面的操做:

    NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; NSArray *array = @[ @1, @2, @3, @4 ]; self.array = mutableArray; [mutableArray removeAllObjects];; NSLog(@"%@",self.array); [mutableArray addObjectsFromArray:array]; self.array = [mutableArray copy]; [mutableArray removeAllObjects];; NSLog(@"%@",self.array);

打印結果以下所示:

2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] ( ) 2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] ( 1, 2, 3, 4 )

(詳見倉庫內附錄的 Demo。)

爲了理解這種作法,首先要知道,兩種狀況:

  1. 對非集合類對象的 copy 與 mutableCopy 操做;
  2. 對集合類對象的 copy 與 mutableCopy 操做。

1. 對非集合類對象的copy操做:

在非集合類對象中:對 immutable 對象進行 copy 操做,是指針複製,mutableCopy 操做時內容複製;對 mutable 對象進行 copy 和 mutableCopy 都是內容複製。用代碼簡單表示以下:

  • [immutableObject copy] // 淺複製
  • [immutableObject mutableCopy] //深複製
  • [mutableObject copy] //深複製
  • [mutableObject mutableCopy] //深複製

好比如下代碼:

NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy NSString *stringCopy = [string copy];

查看內存,會發現 string、stringCopy 內存地址都不同,說明此時都是作內容拷貝、深拷貝。即便你進行以下操做:

[string appendString:@"origion!"]

stringCopy 的值也不會所以改變,可是若是不使用 copy,stringCopy 的值就會被改變。 集合類對象以此類推。 因此,

用 @property 聲明 NSString、NSArray、NSDictionary 常用 copy 關鍵字,是由於他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進行賦值操做,爲確保對象中的字符串值不會無心間變更,應該在設置新屬性值時拷貝一份。

二、集合類對象的copy與mutableCopy

集合類對象是指 NSArray、NSDictionary、NSSet ... 之類的對象。下面先看集合類immutable對象使用 copy 和 mutableCopy 的一個例子:

NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]]; NSArray *copyArray = [array copy]; NSMutableArray *mCopyArray = [array mutableCopy];

查看內容,能夠看到 copyArray 和 array 的地址是同樣的,而 mCopyArray 和 array 的地址是不一樣的。說明 copy 操做進行了指針拷貝,mutableCopy 進行了內容拷貝。但須要強調的是:此處的內容拷貝,僅僅是拷貝 array 這個對象,array 集合內部的元素仍然是指針拷貝。這和上面的非集合 immutable 對象的拷貝仍是挺類似的,那麼mutable對象的拷貝會不會相似呢?咱們繼續往下,看 mutable 對象拷貝的例子:

NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil]; NSArray *copyArray = [array copy]; NSMutableArray *mCopyArray = [array mutableCopy];

查看內存,如咱們所料,copyArray、mCopyArray和 array 的內存地址都不同,說明 copyArray、mCopyArray 都對 array 進行了內容拷貝。一樣,咱們能夠得出結論:

在集合類對象中,對 immutable 對象進行 copy,是指針複製, mutableCopy 是內容複製;對 mutable 對象進行 copy 和 mutableCopy 都是內容複製。可是:集合對象的內容複製僅限於對象自己,對象元素仍然是指針複製。用代碼簡單表示以下:

[immutableObject copy] // 淺複製 [immutableObject mutableCopy] //單層深複製 [mutableObject copy] //單層深複製 [mutableObject mutableCopy] //單層深複製

這個代碼結論和非集合類的很是類似。

參考連接:iOS 集合的深複製與淺複製

14. @synthesize合成實例變量的規則是什麼?假如property名爲foo,存在一個名爲_foo的實例變量,那麼還會自動合成新變量麼?

在回答以前先說明下一個概念:

實例變量 = 成員變量 = ivar

這些說法,筆者下文中,可能都會用到,指的是一個東西。

正如 Apple官方文檔 You Can Customize Synthesized Instance Variable Names 所說: enter image description here

若是使用了屬性的話,那麼編譯器就會自動編寫訪問屬性所需的方法,此過程叫作「自動合成」( auto synthesis)。須要強調的是,這個過程由編譯器在編譯期執行,因此編輯器裏看不到這些「合成方法」 (synthesized method)的源代碼。除了生成方法代碼以外,編譯器還要自動向類中添加適當類型的實例變量,而且在屬性名前面加下劃線,以此做爲實例變量的名字。

@interface CYLPerson : NSObject @property NSString *firstName; @property NSString *lastName; @end

在上例中,會生成兩個實例變量,其名稱分別爲 _firstName 與 _lastName。也能夠在類的實現代碼裏經過 @synthesize 語法來指定實例變量的名字:

@implementation CYLPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end 

上述語法會將生成的實例變量命名爲 _myFirstName 與 _myLastName ,而再也不使用默認的名字。通常狀況下無須修改默認的實例變量名,可是若是你不喜歡如下劃線來命名實例變量,那麼能夠用這個辦法將其改成本身想要的名字。筆者仍是推薦使用默認的命名方案,由於若是全部人都堅持這套方案,那麼寫出來的代碼你們都能看得懂。

總結下 @synthesize 合成實例變量的規則,有如下幾點:

  1. 若是指定了成員變量的名稱,會生成一個指定的名稱的成員變量,
  2. 若是這個成員已經存在了就再也不生成了.
  3. 若是是 @synthesize foo; 還會生成一個名稱爲foo的成員變量,也就是說:

    若是沒有指定成員變量的名稱會自動生成一個屬性同名的成員變量,

  4. 若是是 @synthesize foo = _foo; 就不會生成成員變量了.

假如 property 名爲 foo,存在一個名爲 _foo 的實例變量,那麼還會自動合成新變量麼? 不會。以下圖:

enter image description here

15. 在有了自動合成屬性實例變量以後,@synthesize還有哪些使用場景?

回答這個問題前,咱們要搞清楚一個問題,什麼狀況下不會autosynthesis(自動合成)?

  1. 同時重寫了 setter 和 getter 時
  2. 重寫了只讀屬性的 getter 時
  3. 使用了 @dynamic 時
  4. 在 @protocol 中定義的全部屬性
  5. 在 category 中定義的全部屬性
  6. 重載的屬性

    當你在子類中重載了父類中的屬性,你必須 使用 @synthesize 來手動合成ivar。

除了後三條,對其餘幾個咱們能夠總結出一個規律:當你想手動管理 @property 的全部內容時,你就會嘗試經過實現 @property 的全部「存取方法」(the accessor methods)或者使用 @dynamic 來達到這個目的,這時編譯器就會認爲你打算手動管理 @property,因而編譯器就禁用了 autosynthesis(自動合成)。

由於有了 autosynthesis(自動合成),大部分開發者已經習慣不去手動定義ivar,而是依賴於 autosynthesis(自動合成),可是一旦你須要使用ivar,而 autosynthesis(自動合成)又失效了,若是不去手動定義ivar,那麼你就得藉助 @synthesize 來手動合成 ivar。

其實,@synthesize 語法還有一個應用場景,可是不太建議你們使用:

能夠在類的實現代碼裏經過 @synthesize 語法來指定實例變量的名字:

@implementation CYLPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end 

上述語法會將生成的實例變量命名爲 _myFirstName 與 _myLastName,而再也不使用默認的名字。通常狀況下無須修改默認的實例變量名,可是若是你不喜歡如下劃線來命名實例變量,那麼能夠用這個辦法將其改成本身想要的名字。筆者仍是推薦使用默認的命名方案,由於若是全部人都堅持這套方案,那麼寫出來的代碼你們都能看得懂。

舉例說明:應用場景:

//
// .m文件 // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong // 打開第14行和第17行中任意一行,就可編譯成功 @import Foundation; @interface CYLObject : NSObject @property (nonatomic, copy) NSString *title; @end @implementation CYLObject { // NSString *_title; } //@synthesize title = _title; - (instancetype)init { self = [super init]; if (self) { _title = @"微博@iOS程序犭袁"; } return self; } - (NSString *)title { return _title; } - (void)setTitle:(NSString *)title { _title = [title copy]; } @end

結果編譯器報錯: enter image description here

當你同時重寫了 setter 和 getter 時,系統就不會生成 ivar(實例變量/成員變量)。這時候有兩種選擇:

  1. 要麼如第14行:手動建立 ivar
  2. 要麼如第17行:使用@synthesize foo = _foo; ,關聯 @property 與 ivar。

更多信息,請戳- 》 When should I use @synthesize explicitly?

16. objc中向一個nil對象發送消息將會發生什麼?

在 Objective-C 中向 nil 發送消息是徹底有效的——只是在運行時不會有任何做用:

  1. 若是一個方法返回值是一個對象,那麼發送給nil的消息將返回0(nil)。例如:

    Person * motherInlaw = [[aPerson spouse] mother];

    若是 spouse 對象爲 nil,那麼發送給 nil 的消息 mother 也將返回 nil。

  2. 若是方法返回值爲指針類型,其指針大小爲小於或者等於sizeof(void*),float,double,long double 或者 long long 的整型標量,發送給 nil 的消息將返回0。
  3. 若是方法返回值爲結構體,發送給 nil 的消息將返回0。結構體中各個字段的值將都是0。
  4. 若是方法的返回值不是上述提到的幾種狀況,那麼發送給 nil 的消息的返回值將是未定義的。

具體緣由以下:

objc是動態語言,每一個方法在運行時會被動態轉爲消息發送,即:objc_msgSend(receiver, selector)。

那麼,爲了方便理解這個內容,仍是貼一個objc的源代碼:

// runtime.h(類在runtime中的定義)
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong struct objc_class { Class isa OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class,由於Objc的類的自己也是一個Object,爲了處理這個關係,runtime就創造了Meta Class,當給類發送[NSObject alloc]這樣消息時,其實是把這個消息發給了Class Object #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父類 const char *name OBJC2_UNAVAILABLE; // 類名 long version OBJC2_UNAVAILABLE; // 類的版本信息,默認爲0 long info OBJC2_UNAVAILABLE; // 類信息,供運行期使用的一些位標識 long instance_size OBJC2_UNAVAILABLE; // 該類的實例變量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存,對象接到一個消息會根據isa指針查找消息對象,這時會在method Lists中遍歷,若是cache了,經常使用的方法調用時就可以提升調用的效率。 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議鏈表 #endif } OBJC2_UNAVAILABLE;

objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,而後在該類中的方法列表以及其父類方法列表中尋找方法運行,而後在發送消息的時候,objc_msgSend方法不會返回值,所謂的返回內容都是具體調用時執行的。 那麼,回到本題,若是向一個nil對象發送消息,首先在尋找對象的isa指針時就是0地址返回了,因此不會出現任何錯誤。

17. objc中向一個對象發送消息[obj foo]和objc_msgSend()函數之間有什麼關係?

具體緣由同上題:該方法編譯以後就是objc_msgSend()函數調用.

咱們用 clang 分析下,clang 提供一個命令,能夠將Objective-C的源碼改寫成C++語言,藉此能夠研究下[obj foo]和objc_msgSend()函數之間有什麼關係。

如下面的代碼爲例,因爲 clang 後的代碼達到了10萬多行,爲了便於區分,添加了一個叫 iOSinit 方法,

//
// main.m // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved. // #import "CYLTest.h" int main(int argc, char * argv[]) { @autoreleasepool { CYLTest *test = [[CYLTest alloc] init]; [test performSelector:(@selector(iOSinit))]; return 0; } }

在終端中輸入

clang -rewrite-objc main.m

就能夠生成一個main.cpp的文件,在最低端(10萬4千行左右)

enter image description here

咱們能夠看到大概是這樣的:

((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));

也就是說:

[obj foo];在objc動態編譯時,會被轉意爲:objc_msgSend(obj, @selector(foo));

18. 何時會報unrecognized selector的異常?

簡單來講:

當調用該對象上某個方法,而該對象上沒有實現這個方法的時候, 能夠經過「消息轉發」進行解決。

簡單的流程以下,在上一題中也提到過:

objc是動態語言,每一個方法在運行時會被動態轉爲消息發送,即:objc_msgSend(receiver, selector)。

objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,而後在該類中的方法列表以及其父類方法列表中尋找方法運行,若是,在最頂層的父類中依然找不到相應的方法時,程序在運行時會掛掉並拋出異常unrecognized selector sent to XXX 。可是在這以前,objc的運行時會給出三次拯救程序崩潰的機會:

  1. Method resolution

    objc運行時會調用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。若是你添加了函數,那運行時系統就會從新啓動一次消息發送的過程,不然 ,運行時就會移到下一步,消息轉發(Message Forwarding)。

  2. Fast forwarding

    若是目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其餘對象的機會。 只要這個方法返回的不是nil和self,整個消息發送的過程就會被重啓,固然發送的對象會變成你返回的那個對象。不然,就會繼續Normal Fowarding。 這裏叫Fast,只是爲了區別下一步的轉發機制。由於這一步不會建立任何新的對象,但下一步轉發會建立一個NSInvocation對象,因此相對更快點。

  3. Normal forwarding

    這一步是Runtime最後一次給你挽救的機會。首先它會發送-methodSignatureForSelector:消息得到函數的參數和返回值類型。若是-methodSignatureForSelector:返回nil,Runtime則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。若是返回了一個函數簽名,Runtime就會建立一個NSInvocation對象併發送-forwardInvocation:消息給目標對象。

爲了能更清晰地理解這些方法的做用,git倉庫裏也給出了一個Demo,名稱叫「 _objc_msgForward_demo 」,可運行起來看看。

19. 一個objc對象如何進行內存佈局?(考慮有父類的狀況)

  • 全部父類的成員變量和本身的成員變量都會存放在該對象所對應的存儲空間中.
  • 每個對象內部都有一個isa指針,指向他的類對象,類對象中存放着本對象的

    1. 對象方法列表(對象可以接收的消息列表,保存在它所對應的類對象中)
    2. 成員變量的列表,
    3. 屬性列表,

    它內部也有一個isa指針指向元對象(meta class),元對象內部存放的是類方法列表,類對象內部還有一個superclass的指針,指向他的父類對象。

每一個 Objective-C 對象都有相同的結構,以下圖所示:

enter image description here

翻譯過來就是

Objective-C 對象的結構圖
ISA指針
根類的實例變量
倒數第二層父類的實例變量
...
父類的實例變量
類的實例變量
  • 根對象就是NSObject,它的superclass指針指向nil
  • 類對象既然稱爲對象,那它也是一個實例。類對象中也有一個isa指針指向它的元類(meta class),即類對象是元類的實例。元類內部存放的是類方法列表,根元類的isa指針指向本身,superclass指針指向NSObject類。

如圖: enter image description here

20. 一個objc對象的isa的指針指向什麼?有什麼做用?

指向他的類對象,從而能夠找到對象上的方法

21. 下面的代碼輸出什麼?

    @implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end

答案:

都輸出 Son

NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son

這個題目主要是考察關於 Objective-C 中對 self 和 super 的理解。

咱們都知道:self 是類的隱藏參數,指向當前調用方法的這個類的實例。那 super 呢?

不少人會想固然的認爲「 super 和 self 相似,應該是指向父類的指針吧!」。這是很廣泛的一個誤區。其實 super 是一個 Magic Keyword, 它本質是一個編譯器標示符,和 self 是指向的同一個消息接受者!他們兩個的不一樣點在於:super 會告訴編譯器,調用 class 這個方法時,要去父類的方法,而不是本類裏的。

上面的例子無論調用[self class]仍是[super class],接受消息的對象都是當前 Son *xxx 這個對象。

當使用 self 調用方法時,會從當前類的方法列表中開始找,若是沒有,就從父類中再找;而當使用 super 時,則從父類的方法列表中開始找。而後調用父類的這個方法。

這也就是爲何說「不推薦在 init 方法中使用點語法」,若是想訪問實例變量 iVar 應該使用下劃線( _iVar ),而非點語法(self.iVar )。

點語法( self.iVar )的壞處就是子類有可能覆寫 setter 。假設 Person 有一個子類叫 ChenPerson,這個子類專門表示那些姓「陳」的人。該子類可能會覆寫 lastName 屬性所對應的設置方法:

//
// ChenPerson.m // // // Created by https://github.com/ChenYilong on 15/8/30. // Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved. // #import "ChenPerson.h" @implementation ChenPerson @synthesize lastName = _lastName; - (instancetype)init { self = [super init]; if (self) { NSLog(@"🔴類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class])); NSLog(@"🔴類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([super class])); } return self; } - (void)setLastName:(NSString*)lastName { //設置方法一:若是setter採用是這種方式,就可能引發崩潰 // if (![lastName isEqualToString:@"陳"]) // { // [NSException raise:NSInvalidArgumentException format:@"姓不是陳"]; // } // _lastName = lastName; //設置方法二:若是setter採用是這種方式,就可能引發崩潰 _lastName = @"陳"; NSLog(@"🔴類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"會調用這個方法,想一下爲何?"); } @end

在基類 Person 的默認初始化方法中,可能會將姓氏設爲空字符串。此時若使用點語法( self.lastName )也即 setter 設置方法,那麼調用將會是子類的設置方法,若是在剛剛的 setter 代碼中採用設置方法一,那麼就會拋出異常,

爲了方便採用打印的方式展現,究竟發生了什麼,咱們使用設置方法二。

若是基類的代碼是這樣的:

//
// Person.m // nil對象調用點語法 // // Created by https://github.com/ChenYilong on 15/8/29. // Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved. // #import "Person.h" @implementation Person - (instancetype)init { self = [super init]; if (self) { self.lastName = @""; //NSLog(@"🔴類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class])); //NSLog(@"🔴類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, self.lastName); } return self; } - (void)setLastName:(NSString*)lastName { NSLog(@"🔴類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"根本不會調用這個方法"); _lastName = @"炎黃"; } @end

那麼打印結果將會是這樣的:

 🔴類名與方法名:-[ChenPerson setLastName:](在第36行),描述:會調用這個方法,想一下爲何? 🔴類名與方法名:-[ChenPerson init](在第19行),描述:ChenPerson 🔴類名與方法名:-[ChenPerson init](在第20行),描述:ChenPerson

我在倉庫裏也給出了一個相應的 Demo(名字叫:Demo_21題_下面的代碼輸出什麼)。有興趣能夠跑起來看一下,主要看下他是怎麼打印的,思考下爲何這麼打印。

接下來讓咱們利用 runtime 的相關知識來驗證一下 super 關鍵字的本質,使用clang重寫命令:

    $ clang -rewrite-objc test.m

將這道題目中給出的代碼被轉化爲:

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

從上面的代碼中,咱們能夠發如今調用 [self class] 時,會轉化成 objc_msgSend函數。看下函數定義:

    id objc_msgSend(id self, SEL op, ...)

咱們把 self 作爲第一個參數傳遞進去。

而在調用 [super class]時,會轉化成 objc_msgSendSuper函數。看下函數定義:

    id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一個參數是 objc_super 這樣一個結構體,其定義以下:

struct objc_super {
       __unsafe_unretained id receiver; __unsafe_unretained Class super_class; };

結構體有兩個成員,第一個成員是 receiver, 相似於上面的 objc_msgSend函數第一個參數self 。第二個成員是記錄當前類的父類是什麼。

因此,當調用 [self class] 時,實際先調用的是 objc_msgSend函數,第一個參數是 Son當前的這個實例,而後在 Son 這個類裏面去找 - (Class)class這個方法,沒有,去父類 Father裏找,也沒有,最後在 NSObject類中發現這個方法。而 - (Class)class的實現就是返回self的類別,故上述輸出結果爲 Son。

objc Runtime開源代碼對- (Class)class方法的實現:

- (Class)class {
    return object_getClass(self); }

而當調用 [super class]時,會轉換成objc_msgSendSuper函數。第一步先構造 objc_super 結構體,結構體第一個成員就是self 。 第二個成員是 (id)class_getSuperclass(objc_getClass(「Son」)) , 實際該函數輸出結果爲 Father。

第二步是去 Father這個類裏去找 - (Class)class,沒有,而後去NSObject類去找,找到了。最後內部是使用objc_msgSend(objc_super->receiver, @selector(class))去調用,

此時已經和[self class]調用相同了,故上述輸出結果仍然返回 Son。

參考連接:微博@Chun_iOS的博文刨根問底Objective-C Runtime(1)- Self & Super

22. runtime如何經過selector找到對應的IMP地址?(分別考慮類方法和實例方法)

每個類對象中都一個方法列表,方法列表中記錄着方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,經過這個方法名稱就能夠在方法列表中找到對應的方法實現.

23. 使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?

  • 在ARC下不須要。
  • 在MRC中,對於使用retain或copy策略的須要 。

    在MRC下也不須要

不管在MRC下仍是ARC下均不須要。

2011年版本的Apple API 官方文檔 - Associative References 一節中有一個MRC環境下的例子:

// 在MRC下,使用runtime Associate方法關聯的對象,不須要在主對象dealloc的時候釋放
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong // 摘自2011年版本的Apple API 官方文檔 - Associative References static char overviewKey; NSArray *array = [[NSArray alloc] initWithObjects:@"One", @"Two", @"Three", nil]; // For the purposes of illustration, use initWithFormat: to ensure // the string can be deallocated NSString *overview = [[NSString alloc] initWithFormat:@"%@", @"First three numbers"]; objc_setAssociatedObject ( array, &overviewKey, overview, OBJC_ASSOCIATION_RETAIN ); [overview release]; // (1) overview valid [array release]; // (2) overview invalid

文檔指出

At point 1, the string overview is still valid because the OBJC_ASSOCIATION_RETAIN policy specifies that the array retains the associated object. When the array is deallocated, however (at point 2), overview is released and so in this case also deallocated.

咱們能夠看到,在[array release];以後,overview就會被release釋放掉了。

既然會被銷燬,那麼具體在什麼時間點?

根據 WWDC 2011, Session 322 (第36分22秒) 中發佈的內存銷燬時間表,被關聯的對象在生命週期內要比對象自己釋放的晚不少。它們會在被 NSObject -dealloc 調用的 object_dispose() 方法中釋放。

對象的內存銷燬時間表,分四個步驟:

// 對象的內存銷燬時間表
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 根據 WWDC 2011, Session 322 (36分22秒)中發佈的內存銷燬時間表 

 1. 調用 -release :引用計數變爲零
     * 對象正在被銷燬,生命週期即將結束.
     * 不能再有新的 __weak 弱引用, 不然將指向 nil.
     * 調用 [self dealloc] 
 2. 子類 調用 -dealloc
     * 繼承關係中最底層的子類 在調用 -dealloc
     * 若是是 MRC 代碼 則會手動釋放實例變量們(iVars)
     * 繼承關係中每一層的父類 都在調用 -dealloc
 3. NSObject 調 -dealloc
     * 只作一件事:調用 Objective-C runtime 中的 object_dispose() 方法
 4. 調用 object_dispose()
     * 爲 C++ 的實例變量們(iVars)調用 destructors 
     * 爲 ARC 狀態下的 實例變量們(iVars) 調用 -release 
     * 解除全部使用 runtime Associate方法關聯的對象
     * 解除全部 __weak 引用
     * 調用 free()

對象的內存銷燬時間表:參考連接

24. objc中的類方法和實例方法有什麼本質區別和聯繫?

類方法:

  1. 類方法是屬於類對象的
  2. 類方法只能經過類對象調用
  3. 類方法中的self是類對象
  4. 類方法能夠調用其餘的類方法
  5. 類方法中不能訪問成員變量
  6. 類方法中不能直接調用對象方法

實例方法:

  1. 實例方法是屬於實例對象的
  2. 實例方法只能經過實例對象調用
  3. 實例方法中的self是實例對象
  4. 實例方法中能夠訪問成員變量
  5. 實例方法中直接調用實例方法
  6. 實例方法中也能夠調用類方法(經過類名)

下一篇文章將發佈在這裏,會對如下問題進行總結,並將本篇文章的勘誤一併列出,歡迎指正!請持續關注微博@iOS程序犭袁

@property部分主要參考 Apple官方文檔:Properties Encapsulate an Object’s Values runtime部分主要參考Apple官方文檔:Declared Properties

25. _objc_msgForward函數是作什麼的,直接調用它將會發生什麼?

26. runtime如何實現weak變量的自動置nil?

27. 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?

28. runloop和線程有什麼關係?

29. runloop的mode做用是什麼?

30. 以+ scheduledTimerWithTimeInterval...的方式觸發的timer,在滑動頁面上的列表時,timer會暫定回調,爲何?如何解決?

31. 猜測runloop內部是如何實現的?

32. objc使用什麼機制管理對象內存?

33. ARC經過什麼方式幫助開發者管理內存?

34. 不手動指定autoreleasepool的前提下,一個autorealese對象在什麼時刻釋放?(好比在一個vc的viewDidLoad中建立)

35. BAD_ACCESS在什麼狀況下出現?

36. 蘋果是如何實現autoreleasepool的?

37. 使用block時什麼狀況會發生引用循環,如何解決?

38. 在block內如何修改block外部變量?

39. 使用系統的某些block api(如UIView的block版本寫動畫時),是否也考慮引用循環問題?

40. GCD的隊列(dispatch_queue_t)分哪兩種類型?

41. 如何用GCD同步若干個異步調用?(如根據若干個url異步加載多張圖片,而後在都下載完成後合成一張整圖)

42. dispatch_barrier_async的做用是什麼?

43. 蘋果爲何要廢棄dispatch_get_current_queue

44. 如下代碼運行結果如何?

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}

45. addObserver:forKeyPath:options:context:各個參數的做用分別是什麼,observer中須要實現哪一個方法才能得到KVO回調?

46. 如何手動觸發一個value的KVO

47. 若一個類有實例變量NSString *_foo,調用setValue:forKey:時,能夠以foo仍是_foo做爲key?

48. KVC的keyPath中的集合運算符如何使用?

49. KVC和KVO的keyPath必定是屬性麼?

50. 如何關閉默認的KVO的默認實現,並進入自定義的KVO實現?

51. apple用什麼方式實現對一個對象的KVO?

52. IBOutlet連出來的視圖屬性爲何能夠被設置成weak?

53. IB中User Defined Runtime Attributes如何使用?

54. 如何調試BAD_ACCESS錯誤

55. lldb(gdb)經常使用的調試命令?

相關文章
相關標籤/搜索