CADisplayLink、NSTimer使用注意點:
1.CADisplayLink、NSTimer會對target產生強引用,如果target又對他們產生強引用,就會產生循環引用
2.這兩個定時器存在不準時的可能性
- 解決循環引用的問題
解決方案1:消息轉發機制+中間對象進行處理
//MXTestProxy:NSObject
+ (instancetype)proxyWithTarget:(id)target {
MXTestProxy *pro = [[MXTestProxy alloc]init];
pro.target = target;
return pro;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
//CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:[MXTestProxy proxyWithTarget:self] selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//注:CADisplayLink無需設置時間,因為其保證調用頻率和屏幕的刷幀頻率一致,60FTP(1秒調用60次).
由此,可以使用系統內部的NSProxy類,NSProxy本來就是用于設計消息轉發的
//MXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target {
//NSProxy對象不需要調用init,因為它根本沒有init方法,只需要調用alloc即可
MXProxy *proxy = [MXProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
//CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:[MXProxy proxyWithTarget:self] selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
- 繼承自NSObject,方法調用流程:即原先消息機制的3個部分
- 繼承自NSProxy,直接就進入消息轉發,但其并沒有
- (id)forwardingTargetForSelector:(SEL)aSelector
方法
MXProxy *pro = [MXProxy proxyWithTarget:self];
NSLog(@"%d",[pro isKindOfClass:[UIViewController class]]);
//該運行結果為1
以上結果為1,是因為若是繼承自NSProxy的對象調用對應的NSObject的方法,由于其內部就是消息轉發,故會令消息轉發者發送對應消息,即假設proxy是繼承自NSProxy,則[proxy isKindOfClass]方法的實際調用者還是消息轉發者,而不是proxy
解決方案2:定義NSTimer使用block方法創建
__weak typeof(self)weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf test];
}];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes];
若向原先的方式使用weakSelf,即往target中傳入weakSelf會失敗,因為這個是對block才有效,因為block特性是若外部變量使用的是弱指針進行引用,則block會對該變量有一個弱引用,同理,若是強指針進行引用,則block會對該變量有一個強引用.故傳入弱指針解決循環引用只對block有效,定時器中傳入的target,由于外部只是將參數地址傳入,后賦值給timer內部對應的成員變量,故傳入的是強指針還是弱指針是沒有效果的
- 解決定時器不準的問題
原因:CADisplayLink和NSTimer底層都是由runloop實現的,是依賴于runloop的,如果runloop的任務過于繁重,可能導致這兩個定時器不準時
即假設定時器設置每隔1s調用一次方法,則runloop會每跑一次圈,就計算下時間,若沒達到1s,則繼續跑圈,當達到1s,就處理定時器任務但runloop的跑圈時間是不固定的,故會導致定時器時間不準時
由于GCD的定時器是直接與系統內核掛鉤的,與runloop無關,故無論外部的runloop發生怎樣的操作,都不會影響GCD定時器的運行
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//第二個參數:從什么時候開始,若需要延遲執行,則傳入dispatch_time(DISPATCH_TIME_NOW, 延遲的秒數 * NSEC_PER_SEC)
//第三個參數:每個幾秒執行
//第四個參數:傳入0即可
dispatch_source_set_event_handler(timer, ^{
NSLog(@"123");
});
dispatch_resume(timer);
注:GCD創建的對象在ARC環境中都不需要我們去管理內存
內存布局:
- 堆區的地址是從小到大分配的,且內存地址(十六進制)的最低位一定是0.因為內存對齊(最小單位為16)
- 棧區的地址是從大到小分配的
Tagged Pointer:
- 從64bit開始,iOS引入了tagged pointer技術,用于優化NSNumber、NSDate、NSString等小對象的存儲
- 在沒有使用tagged pointer之前,NSNumber等對象需要動態分配內存,維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值
- 使用tagged pointer之后,NSNumber指針里存儲的數據變成了Tag+ Data,也就是將數據存儲在了指針中
- 當指針不夠存儲數據時,才會使用動態分配內存的方式來存儲數據
- objc_msgSend能識別tagged pointer,直接從指針提取數據,節省了以前的調用開銷
一道面試題:
@property (strong, nonatomic) NSString *name;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i<100; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"123hjgjhgjhgjg" ];
});
}
//運行結果崩潰,會報壞內存訪問
原因:for循環中實際是頻繁調用setter方法,而ARC環境中的setter方法實際會轉換為MRC中對應的代碼內容
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
[name retain];
}
}
當其他線程同時執行setter方法時,可能存在當name屬性已經release,但其他線程繼續調用release,導致其壞內存訪問
其中,如上的代碼策略對應的不同,即strong對應retain,copy對應copy
但上述面試題中,若self.name的字符串為tagged pointer時.不會報錯,是因為tagged pointer本身就不是OC對象,是指針的賦值,不存在調用setter和getter進行賦值
- 如何判斷一個指針是否為Tagged Pointer?
iOS平臺,最高有效位是1(第64bit),其中需要保證其有64位,即0x后面的數字須有16位
Mac平臺,最低有效位是1
MRC:
- 在iOS中,使用引用計數來管理OC對象的內存
- 一個新創建的OC對象引用計數默認是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的內存空間
- 調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1
在進行setter操作時,會先進行判斷是否為同一對象,若為同一對象,則不會做任何事情,若為不同對象,則需要先釋放之前的對象,在retain新的對象
- 若是使用retain修飾,則setter會執行上面的代碼
- 若是使用assign修飾,則setter只會進行單純的賦值操作
//MRC中setter方法寫法
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
[name retain];
}
}
//dealloc方法
- (void)dealloc {
[_name release];
_name = nil;
//也可以用下面這一句代替上面兩句,這兩句是等價的
//self.name = nil;
//在dealloc方法中,父類的dealloc方法放在最后執行
[super dealloc]
}
//在換屬性時/自己掛掉時,要記得release操作
在MRC中,若使用retain關鍵詞,系統會自動生成上述的setter和getter,但dealloc中還是需要自己完成,即釋放還是需要自己去完成
內存管理的經驗總結:
- 當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調用release或者autorelease來釋放它
- 想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1
- 通過類方法創建的對象,在系統內部已經自動幫忙調用release,即除了alloc、copy、new方法創建對象,需要調用release,其余是不需要release的
使用MRC進行開發
self.dataArr = [[[NSMutableArray alloc]init]autorelease];
//含義是:NSMutableArray *dataArr = [[NSMutableArray alloc]init];
self.dataArr = dataArr;
[dataArr release];
故在dealloc方法中,還需要調用self.dataArr = nil;
Copy:
拷貝的目的:產生一個副本對象,跟原對象互不影響
修改了原對象不會影響副本對象
修改了副本對象,不會影響原對象iOS提供了兩個拷貝方法:
1.copy:不可變拷貝,產生不可變副本
2.mutableCopy:可變拷貝,產生可變副本
若兩個對象(str1和str2)都為不可變字符串,即NSString,若str1 = [str2 copy],會發現str1和str2的內存地址是相同的
原因:由于拷貝的目的是,產生一個副本對象,跟原對象互不影響
且str1是一個不可變字符串,本身就沒法修改內容,故可以直接令拷貝出來的字符串對象也指向原先相同的字符串對象
- 深拷貝和淺拷貝:
1.深拷貝:內容拷貝,產生新的對象
2,淺拷貝:指針拷貝,沒有產生新的對象(拷貝的內容沒有拷貝)
注意點:
1.當策略寫的是copy,屬性不要寫不可變類型
基本上有關文字的,使用copy修飾,對于字典和數組,還是使用strong來的多
2.屬性的修飾也一定是copy,不存在mutableCopy,因為mutableCopy只存在于NSString等foundation框架的部分類
3.自定義對象只需管好copy即可
4.自定義類需要實現copy操作,需要手動實現copyWithZone方法
引用計數的存儲:
在64位系統中,引用計數可以直接存儲在優化過的isa指針中,若引用計數過大,則isa中has_sidetable_rc的值為1,并且引用計數會存儲在Side Table類中
//Side Table結構體定義:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //是一個存放對象引用計數的散列表
weak_table_t weak_table;
}
__strong
: 強引用指針
__weak
: 弱指針,當所指內容不存在時,指針會自動變為nil
__unsafe_unretained
:也不會產生強引用,但當指針所指的內容不存在時,會報野指針錯誤
幾道面試題:
1.weak指針的實現原理:
將弱引用存儲到一張哈希表中,對象要銷毀時,會取出當前對象對應的弱引用表,把弱引用表中的內容給清除掉(runtime)
2.ARC幫助我們做了什么?
ARC即LLVM+runtime的結果
ARC通過LLVM編譯器,自動生成retain,release,autorelease代碼,弱引用這樣的存在,是通過runtime在對象銷毀時,自動將弱引用清空掉
自動釋放池:
使用release方法,會導致在release代碼后,若繼續使用被銷毀的對象,則會報壞內存訪問錯誤,若使用autorelease,則無需關心這個問題
從源碼可以看出,@autoreleasepool通過轉換為c++代碼,即開頭是一個構造函數:objc_autoreleasePoolPush()函數,結尾是一個析構函數:
objc_autoreleasePoolPop()函數
自動釋放池的主要底層數據結構是:__AtAutoreleasePool
、AutoreleasePoolPage
__AtAutoreleasePool
是一個結構體,內部包含了構造函數:objc_autoreleasePoolPush()和析構函數:
objc_autoreleasePoolPop(),而push和pop兩個函數都是與AutoreleasePoolPage相關
調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
1)每個AutoreleasePoolPage對象占用4096字節內存,除了用來存放它內部的成員變量,剩下的空間用來存放autorelease對象(即調用autorelease方法的對象)的地址
2)所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
- 其中begin()中即為起始指針的地址(0X1000)加上指針自身大小(56字節)
end()即為起始指針的地址(0X1000)加上AutoreleasePoolPage(4096字節)大小- begin和end之間才是存放autorelease對象的,其余是存放AutoreleasePoolPage原先的一些成員變量的
- 調用push方法會將一個POOL_BOUNDARY入棧,并且返回其存放的內存地址
- 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最后一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY
id *next
指向了下一個能存放autorelease對象地址的區域- 可以通過以下私有函數來查看自動釋放池的情況:
extern void _objc_autoreleasePoolPrint(void);
autorelease對象在什么時機調用release?
iOS在主線程的Runloop中注冊了2個Observer
第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
第2個Observer監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()方法里有局部對象,出了方法后會立即釋放嗎?
一般情況下,局部變量在方法結束后就會被立即釋放
但當局部變量有調用autorelease時,由于系統會在runloop的observer監聽到runloop準備休眠時自動調用自動釋放池的pop和push方法,若多個方法在同一個runloop運行循環中時,會待runloop進入下一個循環(休眠)時進行對autorelease的操作,有可能導致需等待多個方法結束后才會被釋放