一直想作這樣一個小冊子,來記錄本身平時開發、閱讀博客、看書、代碼分析和與人交流中遇到的各類問題。以前有過這樣的嘗試,但都是無疾而終。不過,天天接觸的東西多,有些東西不記下來,忘得也是很快,第二次遇到一樣的問題時,還得再查一遍。好記性不如爛筆頭,因此又決定重拾此事,時不時回頭看看,溫故而知新。html
這裏面的每一個問題,不會太長。或是讀書筆記,或是摘抄,亦或是驗證,每一個問題的篇幅爭取在六七百字的樣子。筆記和摘抄的出處會詳細標明。問題的個數不限,湊齊3500字左右就發一篇。爭取每個月至少發兩篇吧,權當是對本身學習的一個整理。ios
本期主要記錄瞭如下幾個問題:git
咱們在聲明一個NSString屬性時,對於其內存相關特性,一般有兩種選擇(基於ARC環境):strong與copy。那這二者有什麼區別呢?何時該用strong,何時該用copy呢?讓咱們先來看個例子。github
咱們定義一個類,併爲其聲明兩個字符串屬性,以下所示:web
1
2
3
4
5
6
|
@interface TestStringClass ()
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, copy) NSString *copyedString;
@end
|
上面的代碼聲明瞭兩個字符串屬性,其中一個內存特性是strong,一個是copy。下面咱們來看看它們的區別。數組
首先,咱們用一個不可變字符串來爲這兩個屬性賦值,sass
1
2
3
4
5
6
7
8
9
10
|
- (void)test {
NSString *string = [NSString stringWithFormat:@"abc"];
self.strongString = string;
self.copyedString = string;
NSLog(@"origin string: %p, %p", string, &string);
NSLog(@"strong string: %p, %p", _strongString, &_strongString);
NSLog(@"copy string: %p, %p", _copyedString, &_copyedString);
}
|
其輸出結果是:安全
1
2
3
|
origin string: 0x7fe441592e20, 0x7fff57519a48
strong string: 0x7fe441592e20, 0x7fe44159e1f8
copy string: 0x7fe441592e20, 0x7fe44159e200
|
咱們要以看到,這種狀況下,無論是strong仍是copy屬性的對象,其指向的地址都是同一個,即爲string指向的地址。若是咱們換做MRC環境,打印string的引用計數的話,會看到其引用計數值是3,即strong操做和copy操做都使原字符串對象的引用計數值加了1。網絡
接下來,咱們把string由不可變改成可變對象,看看會是什麼結果。即將下面這一句多線程
1
|
NSString *string = [NSString stringWithFormat:@"abc"];
|
改爲:
1
|
NSMutableString *string = [NSMutableString stringWithFormat:@"abc"];
|
其輸出結果是:
1
2
3
|
origin string: 0x7ff5f2e33c90, 0x7fff59937a48
strong string: 0x7ff5f2e33c90, 0x7ff5f2e2aec8
copy string: 0x7ff5f2e2aee0, 0x7ff5f2e2aed0
|
能夠發現,此時copy屬性字符串已再也不指向string字符串對象,而是深拷貝了string字符串,並讓_copyedString對象指向這個字符串。在MRC環境下,打印二者的引用計數,能夠看到string對象的引用計數是2,而_copyedString對象的引用計數是1。
此時,咱們若是去修改string字符串的話,能夠看到:由於_strongString與string是指向同一對象,因此_strongString的值也會跟隨着改變(須要注意的是,此時_strongString的類型其實是NSMutableString,而不是NSString);而_copyedString是指向另外一個對象的,因此並不會改變。
因爲NSMutableString是NSString的子類,因此一個NSString指針能夠指向NSMutableString對象,讓咱們的strongString指針指向一個可變字符串是OK的。
而上面的例子能夠看出,當源字符串是NSString時,因爲字符串是不可變的,因此,無論是strong仍是copy屬性的對象,都是指向源對象,copy操做只是作了次淺拷貝。
當源字符串是NSMutableString時,strong屬性只是增長了源字符串的引用計數,而copy屬性則是對源字符串作了次深拷貝,產生一個新的對象,且copy屬性對象指向這個新的對象。另外須要注意的是,這個copy屬性對象的類型始終是NSString,而不是NSMutableString,所以其是不可變的。
這裏還有一個性能問題,即在源字符串是NSMutableString,strong是單純的增長對象的引用計數,而copy操做是執行了一次深拷貝,因此性能上會有所差別。而若是源字符串是NSString時,則沒有這個問題。
因此,在聲明NSString屬性時,究竟是選擇strong仍是copy,能夠根據實際狀況來定。不過,通常咱們將對象聲明爲NSString時,都不但願它改變,因此大多數狀況下,咱們建議用copy,以避免因可變字符串的修改致使的一些非預期問題。
關於字符串的內存管理,還有些有意思的東西,能夠參考NSString特性分析學習。
常常在看一些第三方庫的代碼時,或者本身在寫一些基礎類時,都會用到斷言。因此在此總結一下Objective-C中關於斷言的一些問題。
Foundation中定義了兩組斷言相關的宏,分別是:
1
2
|
NSAssert / NSCAssert
NSParameterAssert / NSCParameterAssert
|
這兩組宏主要在功能和語義上有所差異,這些區別主要有如下兩點:
當斷言失敗時,一般是會拋出一個以下所示的異常:
1
|
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'true is not equal to false'
|
Foundation爲了處理斷言,專門定義了一個NSAssertionHandler來處理斷言的失敗狀況。NSAssertionHandler對象是自動建立的,用於處理失敗的斷言。當斷言失敗時,會傳遞一個字符串給NSAssertionHandler對象來描述失敗的緣由。每一個線程都有本身的NSAssertionHandler對象。當調用時,一個斷言處理器會打印包含方法和類(或函數)的錯誤消息,並引起一個NSInternalInconsistencyException異常。就像上面所看到的同樣。
咱們不多直接去調用NSAssertionHandler的斷言處理方法,一般都是自動調用的。
NSAssertionHandler提供的方法並很少,就三個,以下所示:
1
2
3
4
5
6
7
8
9
|
// 返回與當前線程的NSAssertionHandler對象。
// 若是當前線程沒有相關的斷言處理器,則該方法會建立一個並指定給當前線程
+ (NSAssertionHandler *)currentHandler
// 當NSCAssert或NSCParameterAssert斷言失敗時,會調用這個方法
- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)object lineNumber:(NSInteger)fileName description:(NSString *)line, format,...
// 當NSAssert或NSParameterAssert斷言失敗時,會調用這個方法
- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ...
|
另外,還定義了一個常量字符串,
1
|
NSString * const NSAssertionHandlerKey;
|
主要是用於在線程的threadDictionary字典中獲取或設置斷言處理器。
關於斷言,還須要注意的一點是在Xcode 4.2之後,在release版本中斷言是默認關閉的,這是由宏NS_BLOCK_ASSERTIONS來處理的。也就是說,當編譯release版本時,全部的斷言調用都是無效的。
咱們能夠自定義一個繼承自NSAssertionHandler的斷言處理類,來實現一些咱們本身的需求。如Mattt Thompson的NSAssertionHandler實例同樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@interface LoggingAssertionHandler : NSAssertionHandler
@end
@implementation LoggingAssertionHandler
- (void)handleFailureInMethod:(SEL)selector
object:(id)object
file:(NSString *)fileName
lineNumber:(NSInteger)line
description:(NSString *)format, ...
{
NSLog(@"NSAssert Failure: Method %@ for object %@ in %@#%i", NSStringFromSelector(selector), object, fileName, line);
}
- (void)handleFailureInFunction:(NSString *)functionName
file:(NSString *)fileName
lineNumber:(NSInteger)line
description:(NSString *)format, ...
{
NSLog(@"NSCAssert Failure: Function (%@) in %@#%i", functionName, fileName, line);
}
@end
|
上面說過,每一個線程都有本身的斷言處理器。咱們能夠經過爲線程的threadDictionary字典中的NSAssertionHandlerKey指定一個新值,來改變線程的斷言處理器。
以下代碼所示:
1
2
3
4
5
6
7
8
9
10
|
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSAssertionHandler *assertionHandler = [[LoggingAssertionHandler alloc] init];
[[[NSThread currentThread] threadDictionary] setValue:assertionHandler
forKey:NSAssertionHandlerKey];
// ...
return YES;
}
|
而何時應該使用斷言呢?一般咱們指望程序按照咱們的預期去運行時,如調用的參數爲空時流程就沒法繼續下去時,可使用斷言。但另外一方面,咱們也須要考慮,在這加斷言確實是須要的麼?咱們是否能夠經過更多的容錯處理來使程序正常運行呢?
Mattt Thompson在NSAssertionHandler中的倒數第二段說得挺有意思,在此摘抄一下:
1
|
But if we look deeper into NSAssertionHandler—and indeed, into our own hearts, there are lessons to be learned about our capacity for kindness and compassion; about our ability to forgive others, and to recover from our own missteps. We can't be right all of the time. We all make mistakes. By accepting limitations in ourselves and others, only then are we able to grow as individuals.
|
在IB與相關文件作鏈接時,咱們常常會用到兩個關鍵字:IBOutlet和IBAction。常常用xib或storyboard的童鞋應該用這兩上關鍵字很是熟悉了。不過UIKit還提供了另外一個僞關鍵字IBOutletCollection,咱們使用這個關鍵字,能夠將界面上一組相同的控件鏈接到同一個數組中。
咱們先來看看這個僞關鍵字的定義,能夠從UIKit.framework的頭文件UINibDeclarations.h找到以下定義:
1
2
3
|
#ifndef IBOutletCollection
#define IBOutletCollection(ClassName)
#endif
|
另外,在Clang源碼中,有更安全的定義方式,以下所示:
1
|
#define IBOutletCollection(ClassName) __attribute__((iboutletcollection(ClassName)))
|
從上面的定義能夠看到,與IBOutlet不一樣的是,IBOutletCollection帶有一個參數,該參數是一個類名。
一般狀況下,咱們使用一個IBOutletCollection屬性時,屬性必須是strong的,且類型是NSArray,以下所示:
1
|
@property (strong, nonatomic) IBOutletCollection(UIScrollView) NSArray *scrollViews;
|
假定咱們的xib文件中有三個橫向的scrollView,咱們即可以將這三個scrollView都鏈接至scrollViews屬性,而後在咱們的代碼中即可以作一些統一處理,以下所示:
1
2
3
4
5
6
7
8
9
10
11
|
- (void)setupScrollViewImages
{
for (UIScrollView *scrollView in self.scrollViews) {
[self.imagesData enumerateObjectsUsingBlock:^(NSString *imageName, NSUInteger idx, BOOL *stop) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(CGRectGetWidth(scrollView.frame) * idx, 0, CGRectGetWidth(scrollView.frame), CGRectGetHeight(scrollView.frame))];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.image = [UIImage imageNamed:imageName];
[scrollView addSubview:imageView];
}];
}
}
|
這段代碼會影響到三個scrollView。這樣作的好處是咱們不須要手動經過addObject:方法將scrollView添加到scrollViews中。
不過在使用IBOutletCollection時,須要注意兩點:
關於第二點,咱們以上面的scrollViews爲例,做以下修改:
1
|
@property (strong, nonatomic) IBOutletCollection(UIScrollView) NSSet *scrollViews;
|
實際上咱們在控制檯打印這個scrollViews時,結果以下所示:
1
2
3
4
5
6
|
(lldb) po self.scrollViews
<__NSArrayI 0x1740573d0>(
<UIScrollView: 0x12d60d770; frame = (0 0; 320 162); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1740574f0>; layer = <CALayer: 0x174229480>; contentOffset: {0, 0}; contentSize: {0, 0}>,
<UIScrollView: 0x12d60dee0; frame = (0 0; 320 161); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x174057790>; layer = <CALayer: 0x1742297c0>; contentOffset: {0, 0}; contentSize: {0, 0}>,
<UIScrollView: 0x12d60e650; frame = (0 0; 320 163); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1740579a0>; layer = <CALayer: 0x1742298e0>; contentOffset: {0, 0}; contentSize: {0, 0}>
)
|
能夠看到,它指向的是一個NSArray數組。
另外,IBOutletCollection實際上在iOS 4版本中就有了。不過,如今的Objective-C已經支持object literals了,因此定義數組能夠直接用@[],方便了許多。並且object literals方式能夠添加不在xib中的用代碼定義的視圖,因此顯得更加靈活。固然,兩種方式選擇哪種,就看咱們本身的實際須要和喜愛了。
NSRecursiveLock實際上定義的是一個遞歸鎖,這個鎖能夠被同一線程屢次請求,而不會引發死鎖。這主要是用在循環或遞歸操做中。咱們先來看一個示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
|
這段代碼是一個典型的死鎖狀況。在咱們的線程中,RecursiveMethod是遞歸調用的。因此每次進入這個block時,都會去加一次鎖,而從第二次開始,因爲鎖已經被使用了且沒有解鎖,因此它須要等待鎖被解除,這樣就致使了死鎖,線程被阻塞住了。調試器中會輸出以下信息:
1
2
|
value = 5
*** -[NSLock lock]: deadlock (<NSLock: 0x1700ceee0> '(null)') *** Break on _NSLockError() to debug.
|
在這種狀況下,咱們就可使用NSRecursiveLock。它能夠容許同一線程屢次加鎖,而不會形成死鎖。遞歸鎖會跟蹤它被lock的次數。每次成功的lock都必須平衡調用unlock操做。只有全部達到這種平衡,鎖最後才能被釋放,以供其它線程使用。
因此,對上面的代碼進行一下改造,
1
|
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
|
這樣,程序就能正常運行了,其輸出以下所示:
1
2
3
4
5
|
value = 5
value = 4
value = 3
value = 2
value = 1
|
NSRecursiveLock除了實現NSLocking協議的方法外,還提供了兩個方法,分別以下:
1
2
3
4
5
|
// 在給定的時間以前去嘗試請求一個鎖
- (BOOL)lockBeforeDate:(NSDate *)limit
// 嘗試去請求一個鎖,並會當即返回一個布爾值,表示嘗試是否成功
- (BOOL)tryLock
|
這兩個方法均可以用於在多線程的狀況下,去嘗試請求一個遞歸鎖,而後根據返回的布爾值,來作相應的處理。以下代碼所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
BOOL flag = [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
if (flag) {
NSLog(@"lock before date");
[lock unlock];
} else {
NSLog(@"fail to lock before date");
}
});
|
在前面的代碼中,咱們又添加了一段代碼,增長一個線程來獲取遞歸鎖。咱們在第二個線程中嘗試去獲取遞歸鎖,固然這種狀況下是會失敗的,輸出結果以下:
1
2
3
4
5
6
|
value = 5
value = 4
fail to lock before date
value = 3
value = 2
value = 1
|
另外,NSRecursiveLock還聲明瞭一個name屬性,以下:
1
|
@property(copy) NSString *name
|
咱們可使用這個字符串來標識一個鎖。Cocoa也會使用這個name做爲錯誤描述信息的一部分。
在看KVOController的代碼時,又看到了NSHashTable這個類,因此就此整理一下。
NSHashTable效仿了NSSet(NSMutableSet),但提供了比NSSet更多的操做選項,尤爲是在對弱引用關係的支持上,NSHashTable在對象/內存處理時更加的靈活。相較於NSSet,NSHashTable具備如下特性:
初始化NSHashTable時,咱們能夠設置一個初始選項,這個選項肯定了這個NSHashTable對象後面全部的行爲。這個選項是由NSHashTableOptions枚舉來定義的,以下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
enum {
// 默認行爲,強引用集合中的對象,等同於NSSet
NSHashTableStrongMemory = 0,
// 在將對象添加到集合以前,會拷貝對象
NSHashTableCopyIn = NSPointerFunctionsCopyIn,
// 使用移位指針(shifted pointer)來作hash檢測及肯定兩個對象是否相等;
// 同時使用description方法來作描述字符串
NSHashTableObjectPointerPersonality = NSPointerFunctionsObjectPointerPersonality,
// 弱引用集合中的對象,且在對象被釋放後,會被正確的移除。
NSHashTableWeakMemory = NSPointerFunctionsWeakMemory
};
typedef NSUInteger NSHashTableOptions;
|
固然,咱們還可使用NSPointerFunctions來初始化,但只有使用NSHashTableOptions定義的這些值,才能確保NSHashTable的各個API能夠正確的工做—包括拷貝、歸檔及快速枚舉。
我的認爲NSHashTable吸引人的地方在於能夠持有元素的弱引用,並且在對象被銷燬後能正確地將其移除。咱們來寫個示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// 具體調用以下
@implementation TestHashAndMapTableClass {
NSMutableDictionary *_dic;
NSSet *_set;
NSHashTable *_hashTable;
}
- (instancetype)init {
self = [super init];
if (self) {
[self testWeakMemory];
NSLog(@"hash table [init]: %@", _hashTable);
}
return self;
}
- (void)testWeakMemory {
if (!_hashTable) {
_hashTable = [NSHashTable weakObjectsHashTable];
}
NSObject *obj = [[NSObject alloc] init];
[_hashTable addObject:obj];
NSLog(@"hash table [testWeakMemory] : %@", _hashTable);
}
|
這段代碼的輸出結果以下:
1
2
3
4
5
|
hash table [testWeakMemory] : NSHashTable {
[6] <NSObject: 0x7fa2b1562670>
}
hash table [init]: NSHashTable {
}
|
能夠看到,在離開testWeakMemory方法,obj對象被釋放,同時對象在集合中的引用也被安全的刪除。
這樣看來,NSHashTable彷佛比NSSet(NSMutableSet)要好啊。那是否是咱們就應用都使用NSHashTable呢?Peter Steinberger在The Foundation Collection Classes給了咱們一組數據,顯示在添加對象的操做中,NSHashTable全部的時間差很少是NSMutableSet的2倍,而在其它操做中,性能大致相近。因此,若是咱們只須要NSSet的特性,就儘可能用NSSet。
另外,Mattt Thompson在NSHashTable & NSMapTable的結尾也寫了段挺有意思的話,在此直接摘抄過來:
1
|
As always, it's important to remember that programming is not about being clever: always approach a problem from the highest viable level of abstraction. NSSet and NSDictionary are great classes. For 99% of problems, they are undoubtedly the correct tool for the job. If, however, your problem has any of the particular memory management constraints described above, then NSHashTable & NSMapTable may be worth a look.
|
最近在靜態庫中寫了一個XXViewController類,而後在主工程的xib中,將xib的類指定爲XXViewController,程序運行時,報了以下錯誤:
1
|
Unknown class XXViewController in Interface Builder file.
|
以前也遇到這個問題,但已記得不太清楚,因此又開始在stackoverflow上找答案。
其實這個問題與Interface Builder無關,最直接的緣由仍是相關的symbol沒有從靜態庫中加載進來。這種問題的處理就是在Target的」Build Setting」–>「Other Link Flags」中加上」-all_load -ObjC」這兩個標識位,這樣就OK了。
咱們的某個業務有這麼一個需求,進入一個列表後須要立馬又push一個web頁面,作一些活動的推廣。在iOS 8上,咱們的實現是一切OK的;但到了iOS 7上,就發現這個web頁面push不出來了,同時控制檯給了一條警告消息,即以下:
1
|
Unbalanced calls to begin/end appearance transitions for ...
|
在這種狀況下,點擊導航欄中的返回按鈕時,直接顯示一個黑屏。
咱們到stackoverflow上查了一下,有這麼一段提示:
1
|
occurs when you try and display a new viewcontroller before the current view controller is finished displaying.
|
意思是說在當前視圖控制器完成顯示以前,又試圖去顯示一個新的視圖控制器。
因而咱們去排查代碼,果真發現,在viewDidLoad裏面去作了次網絡請求操做,且請求返回後就去push這個web活動推廣頁。此時,當前的視圖控制器可能並未顯示完成(即未完成push操做)。
1
|
Basically you are trying to push two view controllers onto the stack at almost the same time.
|
當幾乎同時將兩個視圖控制器push到當前的導航控制器棧中時,或者同時pop兩個不一樣的視圖控制器,就會出現不肯定的結果。因此咱們應該確保同一時間,對同一個導航控制器棧只有一個操做,即使當前的視圖控制器正在動畫過程當中,也不該該再去push或pop一個新的視圖控制器。
因此最後咱們把web活動的數據請求放到了viewDidAppear裏面,並作了些處理,這樣問題就解決了。