CADisplayLink、NSTimer使用注意
@property (strong, nonatomic) CADisplayLink *link;
@property (strong, nonatomic) NSTimer *timer;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 保證調用頻率和屏幕的刷幀頻率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)linkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.link invalidate];
[self.timer invalidate];
}
CADisplayLink、NSTimer會對target產生強引用,如果target又對它們產生強引用,那么就會引發循環引用, dealloc方法并不會走
解決方案:
Timer用block方案
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
timer對block強引用,block對self弱引用
使用代理對象(NSProxy)
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[NJFProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
@interface NJFProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation NJFProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy對象不需要調用init,因為它本來就沒有init方法
NJFProxy *proxy = [NJFProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
GCD定時器
NSTimer依賴于RunLoop,如果RunLoop的任務過于繁重,可能會導致NSTimer不準時
GCD定時器不受RunLoop約束
@interface NJFTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (void)cancelTask:(NSString *)name;
@end
@implementation NJFTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 隊列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : 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(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定時器的唯一標識
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 設置回調
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重復的任務
[self cancelTask:name];
}
});
// 啟動定時器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
dispatch_semaphore_signal(semaphore_);
}
@end
iOS程序的內存布局
代碼段:編譯之后的代碼
數據段
字符串常量:比如NSString *str = @"123"
已初始化數據:已初始化的全局變量、靜態變量等
未初始化數據:未初始化的全局變量、靜態變量等棧:函數調用開銷,比如局部變量,分配的內存空間地址越來越小
堆:通過alloc、malloc、calloc等動態分配的空間,分配的內存空間地址越來越大
Tagged Pointer
從64bit開始,iOS引入了Tagged Pointer技術,用于優化NSNumber、NSDate、NSString等小對象的存儲
在沒有使用Tagged Pointer之前, NSNumber等對象需要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值
使用Tagged Pointer之后,NSNumber指針里面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中
當指針不夠存儲數據時,才會使用動態分配內存的方式來存儲數據
objc_msgSend能識別Tagged Pointer,比如NSNumber的intValue方法,直接從指針提取數據,節省了以前的調用開銷
如何判斷一個指針是否為Tagged Pointer?
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_MSB_TAGGED_POINTERS
如果是iOS平臺, 最高有效位是1(第64bit, 使用高位優先規則 MSB)
# define _OBJC_TAG_MASK (1UL<<63)
#else
如果是mac平臺, 最低有效位是1(低位優先規則 LSB)
# define _OBJC_TAG_MASK 1UL
#endif
面試題
思考以下2段代碼能發生什么事?有什么區別?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
// 加鎖
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 解鎖
});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
先看如下代碼執行結果(ios項目)
NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"abc"];
NSLog(@"%@ %@", [str1 class], [str2 class]);
NSLog(@"%p %p", str1,str2);
__NSCFString NSTaggedPointerString
0x28129d540 0xf5ad71b08c1d52a1
str1內存地址很明顯不夠64位,高位補0 & 1 = 0,最高位不是1,所以不是Tagged Pointer,是OC對象
str2內存地址從右往左,第16位是f, 換算成二進制就是0b1111,最高位1 & 1 = 1,所以是Tagged Pointer,不是OC對象
self.name會調用如下代碼,由于是異步全局并發隊列,所以會有多條線程執行release操作,會造成EXC_BAD_ACCESS,壞內存訪問
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
OC對象的內存管理
在iOS中,使用引用計數來管理OC對象的內存
一個新創建的OC對象引用計數默認是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的內存空間
調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1
內存管理的經驗總結
當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調用release或者autorelease來釋放它
想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1可以通過以下私有函數來查看自動釋放池的情況
extern void _objc_autoreleasePoolPrint(void);
copy和mutableCopy
1、淺拷貝
淺拷貝就是對內存地址的復制,讓目標對象指針和源對象指向同一片內存空間,當內存銷毀的時候,指向這片內存的幾個指針需要重新定義才可以使用,要不然會成為野指針。
淺拷貝就是拷貝指向原來對象的指針,使原對象的引用計數+1,可以理解為創建了一個指向原對象的新指針而已,并沒有創建一個全新的對象
2、深拷貝
深拷貝是指拷貝對象的具體內容,而內存地址是自主分配的,拷貝結束之后,兩個對象雖然存的值是相同的,但是內存地址不一樣,兩個對象也互不影響,互不干涉。
深拷貝就是拷貝出和原來僅僅是值一樣,但是內存地址完全不一樣的新的對象,創建后和原對象沒有任何關系
深拷貝就是內容拷貝,淺拷貝就是指針拷貝
No1:可變對象的
copy
和mutableCopy
方法都是深拷貝(區別完全深拷貝與單層深拷貝)
No2:不可變對象的copy
方法是淺拷貝,mutableCopy
方法是深拷貝。
No3:copy
方法返回的對象都是不可變對象。
引用計數的存儲
在64bit中,引用計數可以直接存儲在優化過的isa指針中,也可能存儲在SideTable類中
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {//優化過的isa
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {//引用計數不是儲存在isa中,而是儲存在sidetable中
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
struct SideTable {
spinlock_t slock;
*******************************
refcnts是一個存放著對象引用計數的散列表
*******************************
RefcountMap refcnts;
*************************************
弱引用表,當前對象的內存地址作為key,指向該對象的弱引用指針作為value
*************************************
weak_table_t weak_table;
....
};
dealloc
當一個對象要釋放時,會自動調用dealloc
- (void)dealloc {
_objc_rootDealloc(self);
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);//清除成員變量
if (assoc) _object_remove_assocations(obj);//清除關聯對象
*************************************
將指向當前的對象的弱指針置為nil
*************************************
obj->clearDeallocating();
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
......
自動釋放池
自動釋放池的主要底層數據結構是:__AtAutoreleasePool、AutoreleasePoolPage
調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
源碼分析
@autoreleasepool { }
轉為C++底層源碼
__AtAutoreleasePool __autoreleasepool
聲明了一個__AtAutoreleasePool類型的局部變量
struct __AtAutoreleasePool {
__AtAutoreleasePool() {//構造函數,在創建結構體的時候調用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {//// 析構函數,在結構體銷毀的時候調用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
源碼https://opensource.apple.com/tarballs/objc4/
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
static inline void *push()
{
id *dest;
//判斷是否是debug 環境
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
AutoreleasePoolPage的結構
class AutoreleasePoolPage : private AutoreleasePoolPageData{
}
struct AutoreleasePoolPageData
{
magic_t const magic; 16 檢查結構體完整性
__unsafe_unretained id *next; 8 next指針,指向最新添加的 autoreleased 對象的下個位置
pthread_t const thread; 8 指向當前線程 用來取出自動釋放池
AutoreleasePoolPage * const parent; 8 指向父結點,第一個結點的 parent 值為nil
AutoreleasePoolPage *child; 8 指向子結點,最后一個結點的 child 值為nil
uint32_t const depth; 4 深度 鏈表鏈層的深度 從0開始,往后遞增1
uint32_t hiwat; 4 最大入棧數量
.....
}; 56個字節
- 每個AutoreleasePoolPage對象占用4096字節內存,除了用來存放它內部的成員變量,剩下的空間用來存放autorelease對象的地址
- 所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
- 調用push方法會將一個POOL_BOUNDARY入棧,并且返回其存放的內存地址
- 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最后一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY
POOL_BOUNDARY 邊界(哨兵對象),每創建一個@autoreleasepool { } 就會創建一個POOL_BOUNDARY
示例
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
NSObject *obj1 = [[[NSObject alloc] init] autorelease];
NSObject *obj2 = [[[NSObject alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 3; i++) {
NSObject *obj3 = [[[NSObject alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
NSObject *obj4 = [[[NSObject alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
objc[1602]: ##############
objc[1602]: AUTORELEASE POOLS for thread 0x1000d2dc0
objc[1602]: 9 releases pending.
objc[1602]: [0x10100b000] ................ PAGE (hot) (cold)
objc[1602]: [0x10100b038] ################ POOL 0x10100b038
objc[1602]: [0x10100b040] 0x10066e1f0 NSObject
objc[1602]: [0x10100b048] 0x10066d5d0 NSObject
objc[1602]: [0x10100b050] ################ POOL 0x10100b050
objc[1602]: [0x10100b058] 0x10066d140 NSObject
objc[1602]: [0x10100b060] 0x10066c8d0 NSObject
objc[1602]: [0x10100b068] 0x100666de0 NSObject
objc[1602]: [0x10100b070] ################ POOL 0x10100b070
objc[1602]: [0x10100b078] 0x100665f50 NSObject
objc[1602]: ##############
Program ended with exit code: 0
每頁AutoreleasePoolPage可以容納(4096 - 56)/8 = 505 個對象,第一頁會加入一個特殊的對象邊界。也就是說第一頁可以加 504個自己的對象,之后的都是505個。
Runloop和Autorelease
MRC環境
iOS在主線程的Runloop中注冊了2個Observer
第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
第2個Observer
監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()
ARC環境
出了方法后,ARC會對方法里的局部對象調用release,對象會被立即釋放