-
一、拋出一個問題:使用
CADisplayLink
、NSTimer
有什么注意點?1.1-1.6的demo-
1.1、分析:
CADisplayLink
、NSTimer
會對target
產生強引用,如果target又對它們產生強引用,那么就會引發循環引用,如下在控制器里面的代碼會產生 相互強引用 的問題-
CADisplayLink(在當前控制器按返回按鈕,你會發現 dealloc 方法不會走,而linkTest還在一直調用,原因是:self強引用CADisplayLink,而CADisplayLink內部又在強引用self(
displayLinkWithTarget:self
))。@property(nonatomic,strong) CADisplayLink *link; // 保證調用頻率和屏幕的刷幀頻率一致 60FPS self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)]; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; -(void)linkTest{ NSLog(@"%s",__func__); } -(void)dealloc{ NSLog(@"%s", __func__); [self.link invalidate]; }
-
NSTimer(在當前控制器按返回按鈕,你會發現 dealloc 方法不會走,而timerTest還在一直調用,原因是:self強引用NSTimer,而NSTimer內部又在強引用self(target:self ))。
@property (strong, nonatomic) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
-
-
1.2、解決上面互相強引用的辦法
-
NSTimer 有一個block的方法,我們可以利用block的弱指針來解決
__weak typeof(self) weakSelf = self;
,傳weakSelf
進去,如下@property (strong, nonatomic) NSTimer *timer; __weak typeof(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { [weakSelf timerTest]; }]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
-
-
1.3、通過中間對象(代理對象)的方式來解決,下面用到了消息轉發機制(會先發送消息、再動態解析、最后再消息轉發)
通過中間對象(代理對象)的方式來解決下面是創建了一個繼承于類 :
JKMiddleProxy : NSObject
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface JKMiddleProxy : NSObject + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end NS_ASSUME_NONNULL_END #import "JKMiddleProxy.h" @implementation JKMiddleProxy + (instancetype)proxyWithTarget:(id)target { JKMiddleProxy *proxy = [[JKMiddleProxy alloc] init]; proxy.target = target; return proxy; } // 消息轉發機制(會先發送消息、再動態解析、最后再消息轉發) - (id)forwardingTargetForSelector:(SEL)aSelector { return self.target; } @end
- 使用如下(不管是CADisplayLink還是NSTimer,把self換為中間對象
[JKMiddleProxy proxyWithTarget:self]
就好)
#import "JKMiddleProxy.h" @property (strong, nonatomic) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKMiddleProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
- 使用如下(不管是CADisplayLink還是NSTimer,把self換為中間對象
-
1.4、效率更加高的中間對象(不需要進行發送消息和再動態解析,直接進行消息轉發),利用
NSProxy
可以略過 發送消息和動態解析。-
下面是創建了一個繼承于類 :JKProxy : NSProxy
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface JKProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end NS_ASSUME_NONNULL_END #import "JKProxy.h" @implementation JKProxy + (instancetype)proxyWithTarget:(id)target { // NSProxy對象不需要調用init,因為它本來就沒有init方法 JKProxy *proxy = [JKProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @end
使用和上面1.3一樣,直接(
[JKProxy proxyWithTarget:self]
)self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
-
-
1.5、看下面的打印結果
1 和 0
(原因是JKProxy繼承于 NSProxy,在調用isKindOfClass的時候直接走的消息轉發(- forwardInvocation),會轉換成ViewController的調用isKindOfClass,而JKMiddleProxy繼承于NSObject,不會進入forwardInvocation進而invokeWithTarget)JKProxy *proxy1 = [JKProxy proxyWithTarget:vc]; JKMiddleProxy *proxy2 = [JKMiddleProxy proxyWithTarget:vc]; NSLog(@"%d %d", [proxy1 isKindOfClass:[ViewController class]], [proxy2 isKindOfClass:[ViewController class]]);
-
-
二、GCD定時器:比較準時,它直接和系統內核掛鉤的(NSTimer依賴于RunLoop,如果RunLoop的任務過于繁重,可能會導致NSTimer不準時)
-
2.1、使用如下:(可以看demo里面的
GCDTimerViewController
有具體的源碼)// 定義GCD定時器對象 dispatch_source_t @property(nonatomic,strong) dispatch_source_t gcdTimer; // 創建隊列 dispatch_queue_t queue = dispatch_get_main_queue(); // 創建定時器 self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // 設置時間 /* dispatch_source_t _Nonnull source: 定時器 dispatch_time_t start: 開始的時間,dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),start多長時間后開始,NSEC_PER_SEC(納秒) uint64_t interval:時間間隔 uint64_t leeway: 誤差,寫0就好 */ uint64_t start = 2.0; uint64_t interval = 1.0; dispatch_source_set_timer(self.gcdTimer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0); // 設置回調 static int count = 0; dispatch_source_set_event_handler(self.gcdTimer, ^{ count ++; NSLog(@"count== %d",count); }); // 啟動定時器 dispatch_resume(self.gcdTimer);
-
2.2、如果上面的想在子線程執行的話,我們可以自己創建隊列(下面是一個串行隊列)
// DISPATCH_QUEUE_SERIAL 串行 // DISPATCH_QUEUE_CONCURRENT 并行 dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
在 2.1 里面回調是用的block,咱們還可以用函數,把
dispatch_source_set_event_handler
換為dispatch_source_set_event_handler_f
dispatch_source_set_event_handler_f(self.gcdTimer, timerFire); void timerFire(void *param) { NSLog(@"定時器打印 - %@", [NSThread currentThread]); }
-
2.3、對上面GCD定時器的一個封裝 JKGCDTimer自己下載,下面展示一下使用
-
第1種使用方式(block返回執行的任務)
// 導入,這個是封裝的類名 #import "JKGCDTimer.h" @property(nonatomic,strong) NSString *gcdTimerKeyName; // 第1種使用方式(Block里面做task) static int number = 0; /** task 定時器開啟后執行的任務 startTime 多長時間后開啟任務 intervalTime 時間間隔 repeats 是否重復執行任務 YES: 重復 NO: 執行一次 async 同步還是異步執行任務 YES:async(全局并發隊列) NO: sync(主隊列) */ self.gcdTimerKeyName = [JKGCDTimer execTask:^{ number ++; NSLog(@"number==%d-------%@",number,[NSThread currentThread]); } startTime:2.0 intervalTime:1.0 repeats:YES async:YES];
-
第2種使用方式(在自己的控制器里面的方法 實現任務)
// 導入,這個是封裝的類名 #import "JKGCDTimer.h" @property(nonatomic,strong) NSString *gcdTimerKeyName; /** target 自己VC的 self selector 自己VC里面的 方法 startTime 多長時間后開啟任務 intervalTime 時間間隔 repeats 是否重復執行任務 YES: 重復 NO: 執行一次 async 同步還是異步執行任務 YES:async(全局并發隊列) NO: sync(主隊列) */ self.gcdTimerKeyName = [JKGCDTimer execTaskTarget:self selector:@selector(timerExecTask) startTime:2.0 intervalTime:1.0 repeats:YES async:YES]; #pragma mark 采用自己控制器執行任務的方法 -(void)timerExecTask{ static int number = 0; number ++; NSLog(@"number==%d-------%@",number,[NSThread currentThread]); }
-
-
-
三、iOS 程序的內存布局
-
3.1、先用一個圖展示
iOS 程序的內存布局代碼段:編譯之后的代碼
-
數據段
- 字符串常量:比如NSString *str = @"456"
- 已初始化數據:已初始化的全局變量、靜態變量等,比如:
static int c = 20;
- 未初始化數據:未初始化的全局變量、靜態變量等,比如:
static int d;
堆:通過alloc、malloc、calloc等動態分配的空間, 分配的內存空間地址越來越大 ,如:
NSObject *obj = [[NSObject alloc] init];
棧:函數調用開銷,比如局部變量。分配的內存空間地址越來越小,如:
int e; int f = 20;
-
3.2、Tagged Pointer (推薦博客一 、推薦博客二、推薦博客三),這是一個蘋果對內存做的優化技術,將一個對象的指針拆成兩部分,一部分直接保存數據,另一部分作為特殊標記,表示這是一個特別的指針,不指向任何一個地址。
-
(1)、從64bit開始,iOS引入了Tagged Pointer技術,用于優化NSNumber、NSDate、NSString等小對象的存儲
以字符串為例
從上面可以看出 str1存的@"123"
是比較小的,內存地址最后一位 是 9,轉化為 二進制是:1001
,最后一位是 1,而 str2存的@"fffffffffffff"
比較大,Tagged Pointer不能再存,只能放到堆區
,從上面的打印可以看出其內存地址的最后一位是 0。 (2)、在沒有使用Tagged Pointer之前, NSNumber等對象需要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值
(3)、使用Tagged Pointer之后,NSNumber指針里面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中
(4)、當指針不夠存儲數據時,才會使用動態分配內存的方式來存儲數據(
如上面的str2
)(5)、objc_msgSend能識別Tagged Pointer,比如NSNumber的intValue方法,直接從指針提取數據,節省了以前的調用開銷,如下:
-
(6)、那怎么判斷一個指針是不是 Tagged Pointer 呢?可以通過 objc 源碼看到對應的判斷方法如下:
static inline bool _objc_isTaggedPointer(const void *ptr) { return ((intptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; } #if OBJC_MSB_TAGGED_POINTERS # define _OBJC_TAG_MASK (1ULL<<63) #else # define _OBJC_TAG_MASK 1 #endif #if TARGET_OS_OSX && __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
iOS平臺,最高有效位是1(第64bit)
Mac平臺,最低有效位是1
看下面的例子:NSString *str1 = [NSString stringWithFormat:@"%@",@"abc"]; NSString *str2 = [NSString stringWithFormat:@"%@",@"ffffffffffffffffffff"]; NSLog(@"%p %p %@ %@",str1,str2,[str1 class],[str2 class]); 打印結果為: 0xad16dee4304feb33 0x6000005dd470 NSTaggedPointerString __NSCFString
分析: str1 的內存地址是:0xad16dee4304feb33,最左邊a在十六進制里面是 10,轉化為二進制是 1010,可以看到是最高有效位是: 1;而str2的內存地址是0x6000005dd470 ,結尾是0,就能確定在堆區。
-
-
-
3.3、思考以下2段代碼能發生什么事?有什么區別?
-
第 1 段代碼
@property (strong, nonatomic) NSString *name; 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"]; }); }
-
第 2 段代碼(崩潰,壞內存訪問)
@property (strong, nonatomic) NSString *name; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 1000; i++) { dispatch_async(queue, ^{ self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; }); }
答:第 2 段代碼會壞內存訪問,原因是:第2段代碼在給 self.name賦值會走下面的方法,由于 第2段代碼是 異步并行的會多個線程調用
- (void)setName:(NSString *)name
, _name釋放[_name release]
兩次,從而造成壞內存訪問;然而第1段代碼[NSString stringWithFormat:@"abc"]
就不是一個OC對象,僅僅是一個Tagged Pointer
中存儲的數據,把指針變量的值取出來給成員變量self.name
而已。解決第2段代碼崩潰的辦法在self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; });
上下加鎖和解鎖就好了,來防止兩次release
。// set方法的本質 - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } } // set方法在ARC下表面的現象 - (void)setName:(NSString *)name { _name =name }
-
-
四、copy 與 mutableCopy
-
4.1、拷貝的目的
- 產生一個副本對象,跟源對象互不影響
- 修改了源對象,不會影響副本對象
- 修改了副本對象,不會影響源對象
-
4.2、iOS 提供了2個拷貝方法
copy
與mutableCopy
- copy,不可變拷貝,產生不可變副本
- mutableCopy,可變拷貝,產生可變副本
-
4.3、深拷貝和淺拷貝
- 深拷貝:內容拷貝,產生新的對象
- 淺拷貝:指針拷貝,沒有產生新的對象
-
4.4.以字符串為例舉例
-
原字符串是不可變的(三個字符串的內存地址一樣)
NSString *str1 = [NSString stringWithFormat:@"123"]; // 淺拷貝:指針拷貝,同一塊內存地址 NSString *str2 = [str1 copy]; // 深拷貝,對象拷貝,生成新的內存地址 NSMutableString *str3 = [str1 mutableCopy]; NSLog(@"%p %p %p",str1,str2,str3); 打印結果:0xcd37ac23abfbc18c 0xcd37ac23abfbc18c 0x600000910840
原字符串是不可變的 -
原字符串是可變的(三個字符串的內存地址不一樣)
NSMutableString *str1 = [NSMutableString stringWithFormat:@"123"]; // 深拷貝,對象拷貝,生成新的內存地址 NSString *str2 = [str1 copy]; // 深拷貝,對象拷貝,生成新的內存地址 NSMutableString *str3 = [str1 mutableCopy]; NSLog(@"%p %p %p",str1,str2,str3); 打印結果:0x6000038c9470 0xdae0103e19bd5106 0x6000038c9140
原字符串是可變的
-
-
4.5、copy和mutableCopy的總結圖
copy和mutableCopy的總結圖 -
4.6、自定義一個類的copy方法
-
自定義的類(JKStudent)遵守
<NSCopying>
協議@property (strong, nonatomic) int number;
-
實現
- (id)copyWithZone:(NSZone *)zone
方法- (id)copyWithZone:(NSZone *)zone { JKStudent *student = [[JKStudent allocWithZone:zone] init]; student.number = self. number; return student; }
-
-
-
五、引用計數的存儲在哪里?
-
在64bit中,引用計數可以直接存儲在優化過的isa指針中,也可能存儲在SideTable類中
引用計數的存儲- extra_rc : 里面存儲的值是引用計數器減1
- has_sidetable_rc: 引用計數器是否過大無法存儲在isa中; 如果為1,那么引用計數器會存儲在一個叫sideTable的類的屬性中,refcnts是一個存放著對象引用計數的散列表
-
-
六、 看兩個面試題
6.1、weak指針的實現原理?
答:將那些弱引用存在一個哈希表里面,到時候這個對象要銷毀,它就會取出當前對象對應的弱引用表,把若引用表里面存儲的若引用都給清除掉。-
6.2、__weak與__unsafe_unretained的區別?
-
定義一個 JKString 類繼承于 NSObject,
__strong JKString *string1; __weak JKString *string2; // 不安全的,當JKString對象銷毀的時候,string3不會被賦空,會產生野指針的情況 __unsafe_unretained JKString *string3; NSLog(@"begin"); { JKString *string = [[JKString alloc]init]; string3 = string; } NSLog(@"%@",string3);
答: __weak與__unsafe_unretained共同點是:都不會產生強引用,__weak更加安全,當__weak指向的對象銷毀的時候,這個指針的值被清空(nil),防止野指針的錯誤。而__unsafe_unretained指向的對象銷毀的時候,這個指針的值不會被清空,會產生野指針的錯誤
-
七、ARC都幫我們做了什么?
答:ARC是LLVM編譯器和Runtime系統相互協作的一個結果,具體是利用編譯器給我們生成內存管理相關的代碼,然后在程序運行的過程中又幫我們處理弱引用這種操作。-
八、autorelease自動釋放池
- 8.1、自動釋放池的主要底層數據結構是
__AtAutoreleasePool
、AutoreleasePoolPage
- 8.2、調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
-
8.3、objc4源碼:NSObject.mm
AutoreleasePoolPage - 8.4、AutoreleasePoolPage的結構
- 每個AutoreleasePoolPage對象占用4096字節內存,除了用來存放它內部的成員變量,剩下的空間用來存放autorelease對象的地址
- 所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
AutoreleasePoolPage的結構 - 調用push方法會將一個POOL_BOUNDARY入棧,并且返回其存放的內存地址
- 調用pop方法時傳入一個POOL_BOUNDARY(
boundary 美[?ba?nd?ri, -dri] 分界線
)的內存地址,會從最后一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY -
id *next
指向了下一個能存放autorelease對象地址的區域
- 8.5、Runloop和Autorelease
- iOS在主線程的Runloop中注冊了2個Observer
- 第1個Observer
- 監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
- 第2個Observer
- 監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
- 監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()
- 第1個Observer
- iOS在主線程的Runloop中注冊了2個Observer
- 8.1、自動釋放池的主要底層數據結構是
-
九、方法里面有局部變量,出了方法后會立即被釋放嗎?
- ARC與MRC的切換:
Build Settings
搜索automatic re
ARC與MRC的切換
答:因下面演示的需要大家可以建一個類JKString類,解釋如下:
-
(1)、如果這個局部對象最終是通過autorelease的形式(MRC)來去釋放的話,就意味著它不是馬上釋放,而是等它那次所處的RunLoop休眠之前就會進行相應的release操作;
局部對象最終是通過autorelease的形式來去釋放的 -
(2)、如果ARC生成的是release代碼的話, 確實局部變量是立馬就會釋放。
ARC生成的是release代碼的話
- ARC與MRC的切換:
-
十、OC對象的內存管理(下面是結論)
10.1、在iOS中,使用引用計數來管理OC對象的內存
10.2、一個新創建的OC對象引用計數默認是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的內存空間
10.3、調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1
-
10.4、內存管理的經驗總結
- 當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調用release或者autorelease來釋放它
- 想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1
-
10.5、可以通過以下私有函數來查看自動釋放池的情況(聲明一下C的函數,程序會自動尋找該函數,extern),在MRC下測試,多在
@autoreleasepool { }
寫對象測試調用下面的函數extern void _objc_autoreleasePoolPrint(void); @autoreleasepool { JKString *string1 = [[[JKString alloc]init] autorelease]; _objc_autoreleasePoolPrint(); @autoreleasepool { JKString *string3 = [[[JKString alloc]init] autorelease]; @autoreleasepool { JKString *string4 = [[[JKString alloc]init] autorelease]; } } }
OC底層知識(十二) : 內存管理
最后編輯于 :
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
推薦閱讀更多精彩內容
- 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...