《iOS Tips 一》

weixin_33912445發表於2018-04-21

主要內容有三:

  1. copy VS strong
  2. newName
  3. __attribute__

Tip1:對於 NSString 而言,@property 中的 strong 和 copy 有什麼區別 ?

在專案中發現有人用 strong,有人用 copy,還有混著用的。
問之,為什麼要用strong, 為什麼這麼寫 ?
答曰:一樣的都是。
so 真的一樣嗎?我們細細來看,這裡先說有什麼區別:

首先,宣告一個 MLPerson:

//MLPerson.h
#import <Foundation/Foundation.h>
@interface MLPerson : NSObject
@property (nonatomic, strong) NSString *strongName;
@property (nonatomic, copy)   NSString *lcopyName;
@end

//MLPerson.m
#import "MLPerson.h"
@implementation MLPerson
- (NSString *)description
{
   return [NSString stringWithFormat:@"strong Name :%@  -- copy Name: %@", _strongName,_lcopyName];
}
@end

如上所示,在 MLPerson 類中新增了兩個屬性: strongNamelcopyName, 這兩個屬性分別用 strong 和 copy 修飾, 為了檢視方便,重寫了 description 方法。


看官卻道,咦?這廝為何 copy 修飾的不起一個 copyName 的名字呢?
欲知詳細,且看 Tip2


在 main.m 中測試程式碼如下:

#import <Foundation/Foundation.h>
#import "MLPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MLPerson *jack = [MLPerson new];
        NSMutableString *name = [NSMutableString stringWithString:@"Hello world"];
        jack.strongName = name;
        jack.copyName = name;
        NSLog(@"%@",jack);
        // 修改name
        [name appendString:@" one"];
        NSLog(@"%@",jack);
        
    }
    return 0;
}

控制輸出結果:

strong Name :Hello world  -- copy Name: Hello world
strong Name :Hello world one  -- copy Name: Hello world
Program ended with exit code: 0

在 jack 這個物件生成之後,分別為其兩個屬性賦值,這個值是一個可變字串,因 NSMutableString 是 NSString 的子類,所以這麼做是沒有問題的。賦值結束之後,將 name 後面追加了 " one" 這一個字串,現在 name 的值是: "Hello world one"。 按照物件導向封裝思想而言,此時此刻 jack 的這兩個屬性都不應該改變,因為對物件的修改最好是通過 setter 方法或者公開的方法進行。通過控制檯列印的結果來看,strong 修飾的屬性是不能滿足要求的,而copy則是可以滿足要求的。
所以,在開發的過程中,為了讓類的封裝性不被破壞,針對 NSString 最好使用 copy 來進行修飾,這樣的程式碼會更清晰一些,也不容出錯。

原因解釋:


5256736-19d4e39f7046bdee.png
圖 1-1

如圖 1-1 所示,當執行 jack.strongName = name 和 jack.lcopyName = name 的時候,實際上,_strongName 指向的是 ① 的 "Hello world" 而 _lcopyName 指向的確實另一份內容與 name 指向的位置一樣的 "Hello world", 後面程式碼對 name 的追加是修改的 ① 處的 "Hello world" ,結果一目瞭然。
而對於不可變字串而言,則沒有什麼區別。


Tip2: 宣告瞭一個屬性名字為 copyName 為什麼編譯不通過,如果就想使用這個名字該如何去做?

在宣告屬性的時候,尤其是為了區分兩個屬性,經常用寫成 newName、copyName 或者其他,但是往往 Xcode 編譯不通過,並且報錯,如圖 1-2 所示:

❌ Property follows Cocoa naming convention for returning 'owned' objects
5256736-12eedf5caed94e2d.png
圖 1-2

在開發者文件《Memory Management Policy》中有這麼一條記憶體管理策略:

You own any object you create
You create an object using a method whose name begins with "alloc", "new", "copy", or "mutableCopy".
(for example, alloc, newObject, or mutableCopy).

上面這條規則說的是在 MRR(memory retain release)記憶體管理下的一條規則。
宣告屬性為 copyName ,也就是會預設產生 setCopyName 和 copyName 這兩個方法 setter 和 getter 方法, 然而根據記憶體規則來說,通過 newXXX 方法就是持有 newXXX 方法返回的物件,getter 方法並不是用來持有物件的,這樣就造成了奇異,so 編譯器直接報錯。
解決這個問題,最簡單的方法就是改名,比如:

@property (nonatomic, copy) NSString *theCopyName;

當然有句話就:就不信邪!
如果執意要用這個名字的話,可以修改編譯器預設為建立的 getter 方法的名字:

@property (nonatomic, copy, getter = theCopyName) NSString *copyName;

這樣編譯也是通過的。
還有一種方式就是使用 Function attribute 來修飾 getter 方法

@property (nonatomic, copy)   NSString *copyName ;
- (NSString *)copyName __attribute__((objc_method_family(none)));

上面這種寫法也是可以通過驗證的。

當然這裡又有: __attribute__ 是什麼的疑問了?且看 Tip3


Tip3: __attribute__ 是什麼?

從上面可以看到,當為 copyName 的 getter 方法新增了 attribute 後面這一段之後,編譯器便不再報錯。儘管你不知道 attribute 是什麼,卻仍然可以推斷出它讓編譯器忽略了對這個方法的記憶體規則檢查即其為編譯器提供了上下文。
首先,來看下 NSFoundation 框架中 NS_REQUIRES_SUPER 的使用:
NS_REQUIRES_SUPER 是在 NSObjCRuntime.h 中定義的預編譯指令,定義如下

#ifndef NS_REQUIRES_SUPER
#if __has_attribute(objc_requires_super)
#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))
#else
#define NS_REQUIRES_SUPER
#endif
#endif

可以看到第三句中:

#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))

和上面針對屬性的使用非常相似,這句話定義了之後,當使用 NS_REQUIRES_SUPER 地方在預編譯時期就會被替換為 __attribute__((objc_requires_super)) (這一點在後面來進行驗證)
在 MLPerson 類中新增方法 - work :

//MLPerson.h
@interface MLPerson : NSObject

@property (nonatomic, strong) NSString *strongName;
@property (nonatomic, copy)   NSString *copyName ;
//@property (nonatomic, copy, getter=theCopyName)   NSString *copyName;
- (NSString *)copyName __attribute__((objc_method_family(none)));

- (void)work NS_REQUIRES_SUPER;
@end


//MLPerson.m
#import "MLPerson.h"

@implementation MLPerson

- (NSString *)description
{
    return [NSString stringWithFormat:@"strong Name :%@  -- copy Name: %@", _strongName,_copyName];
}


- (void)work {
    NSLog(@"MLPerson - work method");
}
@end

建立 MLStudent 類繼承自 MLPerson 類:

//MStudent.h
#import "MLPerson.h"
@interface MLStudent : MLPerson

@end

//MStudent.m
#import "MLStudent.h"

@implementation MLStudent
- (void)work {
    NSLog(@"MLStudent - work method");
}
@end

在為 MLStudent 類新增 work 方法的時候,可以看到如下警告資訊,如圖 1-3:

5256736-efd625a42831c25c.png
圖 1-3

當然如果不呼叫的話也是可以編譯通過的,但是這裡會彈出警告⚠️讓寫此類的人知道此處應該通過 [super work] 呼叫父類的方法。

  1. 通過觀察 * NS_REQUIRES_SUPER* 的使用,對先前 MLPerson 中 copyName 的 getter 方法修飾做出修改,如下:
#if __has_attribute(objc_method_family)
#define ML_OBJC_METHOD_FAMILY_NONE __attribute__((objc_method_family(none)))
#else
#define ML_OBJC_METHOD_FAMILY_NONE
#endif

#import <Foundation/Foundation.h>

@interface MLPerson : NSObject

@property (nonatomic, strong) NSString *strongName;
@property (nonatomic, copy)   NSString *copyName ;
//@property (nonatomic, copy, getter=theCopyName)   NSString *copyName;
//- (NSString *)copyName __attribute__((objc_method_family(none)));

- (NSString *)copyName ML_OBJC_METHOD_FAMILY_NONE;

- (void)work NS_REQUIRES_SUPER;
@end

這裡使用 ML_OBJC_METHOD_FAMILY_NONE 對 __attribute__ 做了預定義處理

  1. 驗證預處理階段的替換
//main.m
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import "MLPerson.h"
#import "MLStudent.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        MLPerson *jack = [MLPerson new];
        
        NSMutableString *name = [NSMutableString stringWithString:@"Hello world"];
        
        jack.strongName = name;
        jack.copyName = name;
        NSLog(@"%@",jack);
        [name appendString:@" one"];
        NSLog(@"%@",jack);
        
        
        NSString *newName = @"one world";
        jack.strongName = newName;
        jack.copyName = newName;
        NSLog(@"%@",jack);
        newName = @"xxx";
        NSLog(@"%@",jack);
        
        NSString *str = @"Hello world";   
        
        MLStudent *liLei = [MLStudent new];
        [liLei work];
    }
    return 0;
}

開啟終端,進入到專案目錄,與 main.m 在同一層級:

 $tree
.
├── MLPerson.h
├── MLPerson.m
├── MLStudent.h
├── MLStudent.m
└── main.m

0 directories, 5 files
$clang -E -fmodules main.m -o main # 對 main.m 執行預處理操作,輸出檔名為main
$tree
.
├── MLPerson.h
├── MLPerson.m
├── MLStudent.h
├── MLStudent.m
├── main # 目標檔案已經生成
└── main.m
$cat main
# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 343 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2








@import Foundation; /* clang -E: implicit import for "/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" */
@import AppKit; /* clang -E: implicit import for "/System/Library/Frameworks/AppKit.framework/Headers/AppKit.h" */
# 1 "./MLPerson.h" 1
# 18 "./MLPerson.h"
@import Foundation; /* clang -E: implicit import for "/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" */

@interface MLPerson : NSObject

@property (nonatomic, strong) NSString *strongName;
@property (nonatomic, copy) NSString *copyName ;


- (NSString *)copyName __attribute__((objc_method_family(none)));


- (void)work __attribute__((objc_requires_super));
@end
# 12 "main.m" 2
# 1 "./MLStudent.h" 1
# 11 "./MLStudent.h"
@interface MLStudent : MLPerson

@end
# 13 "main.m" 2

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MLPerson *jack = [MLPerson new];

        NSMutableString *name = [NSMutableString stringWithString:@"Hello world"];

        jack.strongName = name;
        jack.copyName = name;
        NSLog(@"%@",jack);
        [name appendString:@" one"];
        NSLog(@"%@",jack);


        NSString *newName = @"one world";
        jack.strongName = newName;
        jack.copyName = newName;
        NSLog(@"%@",jack);
        newName = @"xxx";
        NSLog(@"%@",jack);

        NSString *str = @"Hello world";

        MLStudent *liLei = [MLStudent new];
        [liLei work];
    }
    return 0;
}

從 #18 的地方擷取看到

@import Foundation; /* clang -E: implicit import for "/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" */

@interface MLPerson : NSObject

@property (nonatomic, strong) NSString *strongName;
@property (nonatomic, copy) NSString *copyName ;


- (NSString *)copyName __attribute__((objc_method_family(none)));


- (void)work __attribute__((objc_requires_super));
@end

這就是預處理之後 MLPerson 中的內容,我們可以看到註釋已經去除,並且巨集也已經被替換。
上面通過 clang -E -fmodules main.m -o main 生成了預處理之後的 main 檔案
當然通過:gcc -E -Foundation main.m -o main 也是可以做到這一步的,只不過目標檔案中匯入的 Foudation 會被展開。

至此,我們可以看到 __attribute__ 是為編譯器提供上下問的一個工具或者方式,在 Cocoa 中早有使用,目前先了解到此處,後面做專門分析。


end