CADisplayLink娩井、NSTimer使用注意點:
1.CADisplayLink、NSTimer會對target產(chǎn)生強(qiáng)引用,如果target又對他們產(chǎn)生強(qiáng)引用,就會產(chǎn)生循環(huán)引用
2.這兩個定時器存在不準(zhǔn)時的可能性
- 解決循環(huán)引用的問題
解決方案1:消息轉(zhuǎn)發(fā)機(jī)制+中間對象進(jìn)行處理
//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無需設(shè)置時間,因為其保證調(diào)用頻率和屏幕的刷幀頻率一致,60FTP(1秒調(diào)用60次).
由此,可以使用系統(tǒng)內(nèi)部的NSProxy類,NSProxy本來就是用于設(shè)計消息轉(zhuǎn)發(fā)的
//MXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target {
//NSProxy對象不需要調(diào)用init,因為它根本沒有init方法,只需要調(diào)用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,方法調(diào)用流程:即原先消息機(jī)制的3個部分
- 繼承自NSProxy,直接就進(jìn)入消息轉(zhuǎn)發(fā),但其并沒有
- (id)forwardingTargetForSelector:(SEL)aSelector
方法
MXProxy *pro = [MXProxy proxyWithTarget:self];
NSLog(@"%d",[pro isKindOfClass:[UIViewController class]]);
//該運行結(jié)果為1
以上結(jié)果為1,是因為若是繼承自NSProxy的對象調(diào)用對應(yīng)的NSObject的方法,由于其內(nèi)部就是消息轉(zhuǎn)發(fā),故會令消息轉(zhuǎn)發(fā)者發(fā)送對應(yīng)消息,即假設(shè)proxy是繼承自NSProxy,則[proxy isKindOfClass]方法的實際調(diào)用者還是消息轉(zhuǎn)發(fā)者,而不是proxy
解決方案2:定義NSTimer使用block方法創(chuàng)建
__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特性是若外部變量使用的是弱指針進(jìn)行引用,則block會對該變量有一個弱引用,同理,若是強(qiáng)指針進(jìn)行引用,則block會對該變量有一個強(qiáng)引用.故傳入弱指針解決循環(huán)引用只對block有效,定時器中傳入的target,由于外部只是將參數(shù)地址傳入,后賦值給timer內(nèi)部對應(yīng)的成員變量,故傳入的是強(qiáng)指針還是弱指針是沒有效果的
- 解決定時器不準(zhǔn)的問題
原因:CADisplayLink和NSTimer底層都是由runloop實現(xiàn)的,是依賴于runloop的,如果runloop的任務(wù)過于繁重,可能導(dǎo)致這兩個定時器不準(zhǔn)時
即假設(shè)定時器設(shè)置每隔1s調(diào)用一次方法,則runloop會每跑一次圈,就計算下時間,若沒達(dá)到1s,則繼續(xù)跑圈,當(dāng)達(dá)到1s,就處理定時器任務(wù)但runloop的跑圈時間是不固定的,故會導(dǎo)致定時器時間不準(zhǔn)時
由于GCD的定時器是直接與系統(tǒng)內(nèi)核掛鉤的,與runloop無關(guān),故無論外部的runloop發(fā)生怎樣的操作,都不會影響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);
//第二個參數(shù):從什么時候開始,若需要延遲執(zhí)行,則傳入dispatch_time(DISPATCH_TIME_NOW, 延遲的秒數(shù) * NSEC_PER_SEC)
//第三個參數(shù):每個幾秒執(zhí)行
//第四個參數(shù):傳入0即可
dispatch_source_set_event_handler(timer, ^{
NSLog(@"123");
});
dispatch_resume(timer);
注:GCD創(chuàng)建的對象在ARC環(huán)境中都不需要我們?nèi)ス芾韮?nèi)存
內(nèi)存布局:
- 堆區(qū)的地址是從小到大分配的,且內(nèi)存地址(十六進(jìn)制)的最低位一定是0.因為內(nèi)存對齊(最小單位為16)
- 棧區(qū)的地址是從大到小分配的
Tagged Pointer:
- 從64bit開始,iOS引入了tagged pointer技術(shù),用于優(yōu)化NSNumber、NSDate、NSString等小對象的存儲
- 在沒有使用tagged pointer之前,NSNumber等對象需要動態(tài)分配內(nèi)存,維護(hù)引用計數(shù)等,NSNumber指針存儲的是堆中NSNumber對象的地址值
- 使用tagged pointer之后,NSNumber指針里存儲的數(shù)據(jù)變成了Tag+ Data,也就是將數(shù)據(jù)存儲在了指針中
- 當(dāng)指針不夠存儲數(shù)據(jù)時,才會使用動態(tài)分配內(nèi)存的方式來存儲數(shù)據(jù)
- objc_msgSend能識別tagged pointer,直接從指針提取數(shù)據(jù),節(jié)省了以前的調(diào)用開銷
一道面試題:
@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" ];
});
}
//運行結(jié)果崩潰,會報壞內(nèi)存訪問
原因:for循環(huán)中實際是頻繁調(diào)用setter方法,而ARC環(huán)境中的setter方法實際會轉(zhuǎn)換為MRC中對應(yīng)的代碼內(nèi)容
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
[name retain];
}
}
當(dāng)其他線程同時執(zhí)行setter方法時,可能存在當(dāng)name屬性已經(jīng)release,但其他線程繼續(xù)調(diào)用release,導(dǎo)致其壞內(nèi)存訪問
其中,如上的代碼策略對應(yīng)的不同,即strong對應(yīng)retain,copy對應(yīng)copy
但上述面試題中,若self.name的字符串為tagged pointer時.不會報錯,是因為tagged pointer本身就不是OC對象,是指針的賦值,不存在調(diào)用setter和getter進(jìn)行賦值
- 如何判斷一個指針是否為Tagged Pointer倔幼?
iOS平臺霹粥,最高有效位是1(第64bit),其中需要保證其有64位,即0x后面的數(shù)字須有16位
Mac平臺洞渔,最低有效位是1
MRC:
- 在iOS中漂羊,使用引用計數(shù)來管理OC對象的內(nèi)存
- 一個新創(chuàng)建的OC對象引用計數(shù)默認(rèn)是1,當(dāng)引用計數(shù)減為0阶女,OC對象就會銷毀颊糜,釋放其占用的內(nèi)存空間
- 調(diào)用retain會讓OC對象的引用計數(shù)+1,調(diào)用release會讓OC對象的引用計數(shù)-1
在進(jìn)行setter操作時,會先進(jìn)行判斷是否為同一對象,若為同一對象,則不會做任何事情,若為不同對象,則需要先釋放之前的對象,在retain新的對象
- 若是使用retain修飾,則setter會執(zhí)行上面的代碼
- 若是使用assign修飾,則setter只會進(jìn)行單純的賦值操作
//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方法放在最后執(zhí)行
[super dealloc]
}
//在換屬性時/自己掛掉時,要記得release操作
在MRC中,若使用retain關(guān)鍵詞,系統(tǒng)會自動生成上述的setter和getter,但dealloc中還是需要自己完成,即釋放還是需要自己去完成
內(nèi)存管理的經(jīng)驗總結(jié):
- 當(dāng)調(diào)用alloc秃踩、new衬鱼、copy、mutableCopy方法返回了一個對象憔杨,在不需要這個對象時鸟赫,要調(diào)用release或者autorelease來釋放它
- 想擁有某個對象,就讓它的引用計數(shù)+1;不想再擁有某個對象抛蚤,就讓它的引用計數(shù)-1
- 通過類方法創(chuàng)建的對象,在系統(tǒng)內(nèi)部已經(jīng)自動幫忙調(diào)用release,即除了alloc台谢、copy、new方法創(chuàng)建對象,需要調(diào)用release,其余是不需要release的
使用MRC進(jìn)行開發(fā)
self.dataArr = [[[NSMutableArray alloc]init]autorelease];
//含義是:NSMutableArray *dataArr = [[NSMutableArray alloc]init];
self.dataArr = dataArr;
[dataArr release];
故在dealloc方法中,還需要調(diào)用self.dataArr = nil;
Copy:
拷貝的目的:產(chǎn)生一個副本對象,跟原對象互不影響
修改了原對象不會影響副本對象
修改了副本對象,不會影響原對象iOS提供了兩個拷貝方法:
1.copy:不可變拷貝,產(chǎn)生不可變副本
2.mutableCopy:可變拷貝,產(chǎn)生可變副本
若兩個對象(str1和str2)都為不可變字符串,即NSString,若str1 = [str2 copy],會發(fā)現(xiàn)str1和str2的內(nèi)存地址是相同的
原因:由于拷貝的目的是,產(chǎn)生一個副本對象,跟原對象互不影響
且str1是一個不可變字符串,本身就沒法修改內(nèi)容,故可以直接令拷貝出來的字符串對象也指向原先相同的字符串對象
- 深拷貝和淺拷貝:
1.深拷貝:內(nèi)容拷貝,產(chǎn)生新的對象
2,淺拷貝:指針拷貝,沒有產(chǎn)生新的對象(拷貝的內(nèi)容沒有拷貝)
注意點:
1.當(dāng)策略寫的是copy,屬性不要寫不可變類型
基本上有關(guān)文字的,使用copy修飾,對于字典和數(shù)組,還是使用strong來的多
2.屬性的修飾也一定是copy,不存在mutableCopy,因為mutableCopy只存在于NSString等foundation框架的部分類
3.自定義對象只需管好copy即可
4.自定義類需要實現(xiàn)copy操作,需要手動實現(xiàn)copyWithZone方法
引用計數(shù)的存儲:
在64位系統(tǒng)中,引用計數(shù)可以直接存儲在優(yōu)化過的isa指針中,若引用計數(shù)過大,則isa中has_sidetable_rc的值為1,并且引用計數(shù)會存儲在Side Table類中
//Side Table結(jié)構(gòu)體定義:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //是一個存放對象引用計數(shù)的散列表
weak_table_t weak_table;
}
__strong
: 強(qiáng)引用指針
__weak
: 弱指針,當(dāng)所指內(nèi)容不存在時,指針會自動變?yōu)閚il
__unsafe_unretained
:也不會產(chǎn)生強(qiáng)引用,但當(dāng)指針?biāo)傅膬?nèi)容不存在時,會報野指針錯誤
幾道面試題:
1.weak指針的實現(xiàn)原理:
將弱引用存儲到一張哈希表中,對象要銷毀時,會取出當(dāng)前對象對應(yīng)的弱引用表,把弱引用表中的內(nèi)容給清除掉(runtime)
2.ARC幫助我們做了什么?
ARC即LLVM+runtime的結(jié)果
ARC通過LLVM編譯器,自動生成retain,release,autorelease代碼,弱引用這樣的存在,是通過runtime在對象銷毀時,自動將弱引用清空掉
自動釋放池:
使用release方法,會導(dǎo)致在release代碼后,若繼續(xù)使用被銷毀的對象,則會報壞內(nèi)存訪問錯誤,若使用autorelease,則無需關(guān)心這個問題
從源碼可以看出,@autoreleasepool通過轉(zhuǎn)換為c++代碼,即開頭是一個構(gòu)造函數(shù):objc_autoreleasePoolPush()函數(shù),結(jié)尾是一個析構(gòu)函數(shù):
objc_autoreleasePoolPop()函數(shù)
自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool
岁经、AutoreleasePoolPage
__AtAutoreleasePool
是一個結(jié)構(gòu)體,內(nèi)部包含了構(gòu)造函數(shù):objc_autoreleasePoolPush()和析構(gòu)函數(shù):
objc_autoreleasePoolPop(),而push和pop兩個函數(shù)都是與AutoreleasePoolPage相關(guān)
調(diào)用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
1)每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存朋沮,除了用來存放它內(nèi)部的成員變量,剩下的空間用來存放autorelease對象(即調(diào)用autorelease方法的對象)的地址
2)所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
- 其中begin()中即為起始指針的地址(0X1000)加上指針自身大小(56字節(jié))
end()即為起始指針的地址(0X1000)加上AutoreleasePoolPage(4096字節(jié))大小- begin和end之間才是存放autorelease對象的,其余是存放AutoreleasePoolPage原先的一些成員變量的
- 調(diào)用push方法會將一個POOL_BOUNDARY入棧缀壤,并且返回其存放的內(nèi)存地址
- 調(diào)用pop方法時傳入一個POOL_BOUNDARY的內(nèi)存地址樊拓,會從最后一個入棧的對象開始發(fā)送release消息,直到遇到這個POOL_BOUNDARY
id *next
指向了下一個能存放autorelease對象地址的區(qū)域- 可以通過以下私有函數(shù)來查看自動釋放池的情況:
extern void _objc_autoreleasePoolPrint(void);
autorelease對象在什么時機(jī)調(diào)用release?
iOS在主線程的Runloop中注冊了2個Observer
第1個Observer監(jiān)聽了kCFRunLoopEntry事件塘慕,會調(diào)用objc_autoreleasePoolPush()
第2個Observer監(jiān)聽了kCFRunLoopBeforeWaiting事件筋夏,會調(diào)用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
監(jiān)聽了kCFRunLoopBeforeExit事件图呢,會調(diào)用objc_autoreleasePoolPop()方法里有局部對象,出了方法后會立即釋放嗎?
一般情況下,局部變量在方法結(jié)束后就會被立即釋放
但當(dāng)局部變量有調(diào)用autorelease時,由于系統(tǒng)會在runloop的observer監(jiān)聽到runloop準(zhǔn)備休眠時自動調(diào)用自動釋放池的pop和push方法,若多個方法在同一個runloop運行循環(huán)中時,會待runloop進(jìn)入下一個循環(huán)(休眠)時進(jìn)行對autorelease的操作,有可能導(dǎo)致需等待多個方法結(jié)束后才會被釋放