上篇文章講了各類鎖的使用和讀寫鎖的應用, 看完本文章你將瞭解到c++
- DisplayLink和timer的使用和原理
- 內存分配和內存管理
- 自動釋放池原理
- weak指針原理和釋放時機
- 引用計數原理
CADisplayLink
是將任務添加到runloop
中,loop
每次循環便會調用target
的selector
,使用這個也能監測卡頓問題。首先介紹下API
git
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
//runloop沒循環一圈都會調用
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//從runloop中刪除
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//取消
- (void)invalidate;
複製代碼
咱們在一個須要push
的VC
中運行來觀察聲明週期github
@property (nonatomic,strong) CADisplayLink *link;
//初始化
self.link = [FYDisplayLink displayLinkWithTarget:self selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
@synchronized (self) {
NSLog(@"FPS:%d",fps);
fps = 0;
}
});
dispatch_resume(timer);
//全局變量
dispatch_source_t timer;
static int fps;
- (void)test{
@synchronized (self) {
fps += 1;
}
}
- (void)dealloc{
[self.link invalidate];
NSLog(@"%s",__func__);
}
//log
2019-07-30 17:44:37.217781+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:38.212477+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:39.706000+0800 day17-定時器[29637:6504821] FPS:89
2019-07-30 17:44:40.706064+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:41.705589+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:42.706268+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:43.705942+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:44.705792+0800 day17-定時器[29637:6504821] FPS:60
複製代碼
初始化以後,對fps
使用了簡單版本的讀寫鎖,能夠看到fps
基本穩定在60左右,點擊按鈕返回以後,link
和VC
並無正常銷燬。咱們分析一下,VC(self)
->link
->target(self)
,致使了死循環,釋放的時候,沒法釋放self
和link
,那麼咱們改動一下link
->target(self)
中的強引用,改爲弱引用,代碼改爲下面的算法
@interface FYTimerTarget : NSObject
@property (nonatomic,weak) id target;
@end
@implementation FYTimerTarget
-(id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
@end
FYProxy *proxy=[FYProxy proxyWithTarget:self];
self.link = [FYDisplayLink displayLinkWithTarget:self selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
- (void)test{
NSLog(@"%s",__func__);
}
//log
2019-07-30 17:59:04.339934 -[ViewController test]
2019-07-30 17:59:04.356292 -[ViewController test]
2019-07-30 17:59:04.371428 -[FYTimerTarget dealloc]
2019-07-30 17:59:04.371634 -[ViewController dealloc]
複製代碼
FYTimerTarget
對target
進行了弱引用,self
對FYTimerTarget
進行強引用,在銷燬了的時候,先釋放self
,而後檢查self
的FYTimerTarget
,FYTimerTarget
只有一個參數weak
屬性,能夠直接釋放,釋放完FYTimerTarget
,而後釋放self(VC)
,最終能夠正常。編程
使用NSTimer
的時候,timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
會對aTarget
進行強引用,因此咱們對這個aTarget
進行一個簡單的封裝數組
@interface FYProxy : NSProxy
@property (nonatomic,weak) id target;
+(instancetype)proxyWithTarget:(id)target;
@end
@implementation FYProxy
- (void)dealloc{
NSLog(@"%s",__func__);
}
+ (instancetype)proxyWithTarget:(id)target{
FYProxy *obj=[FYProxy alloc];
obj.target = target;
return obj;
}
//轉發
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
@end
複製代碼
FYProxy
是繼承NSProxy
,而NSProxy
不是繼承NSObject
的,而是另一種基類,不會走objc_msgSend()
的三大步驟,當找不到函數的時候直接執行- (void)forwardInvocation:(NSInvocation *)invocation
,和- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
直接進入消息轉發階段。或者將繼承關係改爲FYTimerTarget : NSObject
,這樣子target
找不到的函數仍是會走消息轉發的三大步驟,咱們再FYTimerTarget
添加消息動態解析性能優化
-(id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
複製代碼
這樣子target
的aSelector
轉發給了self.target
處理,成功弱引用了self
和函數的轉發處理。bash
FYTimerTarget *obj =[FYTimerTarget new];
obj.target = self;
self.timer = [NSTimer timerWithTimeInterval:1.0f
target:obj
selector:@selector(test)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
[self.timer setFireDate:[NSDate distantPast]];
//log
2019-07-30 18:03:08.723433+0800 day17-定時器[30877:6556631] -[ViewController test]
2019-07-30 18:03:09.722611+0800 day17-定時器[30877:6556631] -[ViewController test]
2019-07-30 18:03:09.847540+0800 day17-定時器[30877:6556631] -[FYTimerTarget dealloc]
2019-07-30 18:03:09.847677+0800 day17-定時器[30877:6556631] -[ViewController dealloc]
複製代碼
或者使用timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
,而後外部使用__weak self
調用函數,也不會產生循環引用。 使用block
的狀況,釋放正常。多線程
self.timer=[NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
//log
2019-07-30 18:08:24.678789+0800 day17-定時器[31126:6566530] 123
2019-07-30 18:08:25.659127+0800 day17-定時器[31126:6566530] 123
2019-07-30 18:08:26.107643+0800 day17-定時器[31126:6566530] -[ViewController dealloc]
複製代碼
因爲link
和timer
是添加到runloop
中使用的,每次一個循環則訪問timer
或者link
,而後執行對應的函數,在時間上有相對少量偏差的,每此循環,要刷新UI(在主線程),要執行其餘函數,要處理系統端口事件,要處理其餘的計算。。。總的來講,偏差仍是有的。併發
GCD
中的dispatch_source_t
的定時器是基於內核的,時間偏差相對較少。
//timer 須要強引用 或者設置成全局變量
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
//設置
dispatch_source_set_event_handler(timer, ^{
//code 定時器執行的代碼
});
//開始定時器
dispatch_resume(timer);
複製代碼
或者使用函數dispatch_source_set_event_handler_f(timer, function_t);
dispatch_source_set_event_handler_f(timer, function_t);
void function_t(void * p){
//code here
}
複製代碼
業務常用定時器的話,仍是封裝一個簡單的功能比較好,封裝首先從需求開始分析,咱們使用定時器經常使用的參數都哪些?須要哪些功能?
首先須要開始的時間,而後執行的頻率,執行的任務(函數或block),是否重複執行,這些都是須要的。 先定義一個函數
+ (NSString *)exeTask:(dispatch_block_t)block
start:(NSTimeInterval)time
interval:(NSTimeInterval)interval
repeat:(BOOL)repeat
async:(BOOL)async;
+ (NSString *)exeTask:(id)target
sel:(SEL)aciton
start:(NSTimeInterval)time
interval:(NSTimeInterval)interval
repeat:(BOOL)repeat
async:(BOOL)async;
//取消
+ (void)exeCancelTask:(NSString *)key;
複製代碼
而後將剛纔寫的拿過來,增長了一些判斷。有任務的時候纔會執行,不然直接返回nil
,當循環的時候,須要間隔大於0,不然返回,同步或異步,就或者主隊列或者異步隊列,而後用生成的key
,timer
爲value
存儲到全局變量中,在取消的時候直接用key
取出timer
取消,這裏使用了信號量,限制單線程操做。在存儲和取出(取消timer)的時候進行限制,提升其餘代碼執行的效率。
+ (NSString *)exeTask:(dispatch_block_t)block start:(NSTimeInterval)time interval:(NSTimeInterval)interval repeat:(BOOL)repeat async:(BOOL)async{
if (block == nil) {
return nil;
}
if (repeat && interval <= 0) {
return nil;
}
NSString *name =[NSString stringWithFormat:@"%d",i];
//主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
if (async) {
queue = dispatch_queue_create("async.com", DISPATCH_QUEUE_CONCURRENT);
}
//建立定時器
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設置啓動時間
dispatch_source_set_timer(_timer,
dispatch_time(DISPATCH_TIME_NOW, time*NSEC_PER_SEC), interval*NSEC_PER_SEC, 0);
//設定回調
dispatch_source_set_event_handler(_timer, ^{
block();
if (repeat == NO) {
dispatch_source_cancel(_timer);
}
});
//啓動定時器
dispatch_resume(_timer);
//存放到字典
if (name.length && _timer) {
dispatch_semaphore_wait(samephore, DISPATCH_TIME_FOREVER);
timers[name] = _timer;
dispatch_semaphore_signal(samephore);
}
return name;
}
+ (NSString *)exeTask:(id)target
sel:(SEL)aciton
start:(NSTimeInterval)time
interval:(NSTimeInterval)interval
repeat:(BOOL)repeat
async:(BOOL)async{
if (target == nil || aciton == NULL) {
return nil;
}
if (repeat && interval <= 0) {
return nil;
}
NSString *name =[NSString stringWithFormat:@"%d",i];
//主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
if (async) {
queue = dispatch_queue_create("async.com", DISPATCH_QUEUE_CONCURRENT);
}
//建立定時器
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設置啓動時間
dispatch_source_set_timer(_timer,
dispatch_time(DISPATCH_TIME_NOW, time*NSEC_PER_SEC), interval*NSEC_PER_SEC, 0);
//設定回調
dispatch_source_set_event_handler(_timer, ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
//這裏是會報警告的代碼
if ([target respondsToSelector:aciton]) {
[target performSelector:aciton];
}
#pragma clang diagnostic pop
if (repeat == NO) {
dispatch_source_cancel(_timer);
}
});
//啓動定時器
dispatch_resume(_timer);
//存放到字典
if (name.length && _timer) {
dispatch_semaphore_wait(samephore, DISPATCH_TIME_FOREVER);
timers[name] = _timer;
dispatch_semaphore_signal(samephore);
}
return name;
}
+ (void)exeCancelTask:(NSString *)key{
if (key.length == 0) {
return;
}
dispatch_semaphore_wait(samephore, DISPATCH_TIME_FOREVER);
if ([timers.allKeys containsObject:key]) {
dispatch_source_cancel(timers[key]);
[timers removeObjectForKey:key];
}
dispatch_semaphore_signal(samephore);
}
複製代碼
用的時候很簡單
key = [FYTimer exeTask:^{
NSLog(@"123");
} start:1
interval:1
repeat:YES
async:NO];
複製代碼
或者
key = [FYTimer exeTask:self sel:@selector(test) start:0 interval:1 repeat:YES async:YES];
複製代碼
取消執行的時候
[FYTimer exeCancelTask:key];
複製代碼
測試封裝的定時器
- (void)viewDidLoad {
[super viewDidLoad];
key = [FYTimer exeTask:self sel:@selector(test) start:0 interval:1 repeat:YES async:YES];
}
-(void)test{
NSLog(@"%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[FYTimer exeCancelTask:key];
}
//log
2019-07-30 21:16:48.639486+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:49.640177+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:50.639668+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:51.639590+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:52.156004+0800 day17-定時器2[48817:1300845] -[ViewController touchesBegan:withEvent:]
複製代碼
在點擊VC
的時候進行取消操做,timer
中止。
NSProxy
實際上是除了NSObject
的另一個基類,方法比較少,當找不到方法的時候執行消息轉發階段(由於沒有父類),調用函數的流程更短,性能則更好。
問題:ret1
和ret2
分別是多少?
ViewController *vc1 =[[ViewController alloc]init];
FYProxy *pro1 =[FYProxy proxyWithTarget:vc1];
FYTimerTarget *tar =[FYTimerTarget proxyWithTarget:vc1];
BOOL ret1 = [pro1 isKindOfClass:ViewController.class];
BOOL ret2 = [tar isKindOfClass:ViewController.class];
NSLog(@"%d %d",ret1,ret2);
複製代碼
咱們來分析一下,-(bool)isKindOfClass:(cls)
對象函數是判斷該對象是否的cls
的子類或者該類的實例,這點無可置疑,那麼ret1
應該是0
,ret2
應該也是0
首先看FYProxy
的實現,forwardInvocation
和methodSignatureForSelector
,在沒有該函數的時候進行消息轉發,轉發對象是self.target
,在該例子中isKindOfClass
不存在與FYProxy
,因此講該函數轉發給了VC
,則BOOL ret1 = [pro1 isKindOfClass:ViewController.class];
至關於BOOL ret1 = [ViewController.class isKindOfClass:ViewController.class];
,因此答案是1
而後ret2
是0,tar
是繼承於NSObject
的,自己有-(bool)isKindOfClass:(cls)
函數,因此答案是0。
答案是:ret1
是1
,ret2
是0
。
內存分爲保留段、數據段、堆(↓)、棧(↑)、內核區。
數據段包括
棧:函數調用開銷、好比局部變量,分配的內存空間地址愈來愈小。
堆:經過alloc、malloc、calloc等動態分配的空間,分配的空間地址愈來愈大。
驗證:
int a = 10;
int b ;
int main(int argc, char * argv[]) {
@autoreleasepool {
static int c = 20;
static int d;
int e = 10;
int f;
NSString * str = @"123";
NSObject *obj =[[NSObject alloc]init];
NSLog(@"\na:%p \nb:%p \nc:%p \nd:%p \ne:%p \nf:%p \nobj:%p\n str:%p",&a,&b,&c,&d,&e,&f,obj,str);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//log
a:0x1063e0d98
b:0x1063e0e64
c:0x1063e0d9c
d:0x1063e0e60
e:0x7ffee9820efc
f:0x7ffee9820ef8
obj:0x6000013541a0
str:0x1063e0068
複製代碼
從64bit開始,iOS引入Tagged Pointer
技術,用於優化NSNumber、NSDate、NSString
等小對象的存儲,在沒有使用以前,他們須要動態分配內存,維護計數,使用Tagged Pointer
以後,NSNumber
指針裏面的數據變成了Tag+Data
,也就是將數值直接存儲在了指針中,只有當指針不夠存儲數據時,纔會動態分配內存的方式來存儲數據,並且objc_msgSend()
可以識別出Tagged Pointer
,好比NSNumber
的intValue
方法,直接從指針提取數據,節省了之前的調用的開銷。 在iOS中,最高位是1(第64bit),在Mac中,最低有效位是1。 在runtime
源碼中objc-internal.h 370行
判斷是否使用了優化技術
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
複製代碼
咱們拿來這個能夠判斷對象是否使用了優化技術。
咱們使用幾個NSNumber
的大小數字來驗證
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__ //mac開發
// 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//iOS開發
#endif
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
bool objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
int main(int argc, char * argv[]) {
@autoreleasepool {
NSNumber *n1 = @2;
NSNumber *n2 = @3;
NSNumber *n3 = @(4);
NSNumber *n4 = @(0x4fffffffff);
NSLog(@"\n%p \n%p \n%p \n%p",n1,n2,n3,n4);
BOOL n1_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n1));
BOOL n2_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n2));
BOOL n3_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n3));
BOOL n4_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n4));
NSLog(@"\nn1:%d \nn2:%d \nn3:%d \nn4:%d ",n1_tag,n2_tag,n3_tag,n4_tag);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//log
0xbf4071e2657ccb95
0xbf4071e2657ccb85
0xbf4071e2657ccbf5
0xbf40751d9a833444
2019-07-30 21:55:52.626317+0800 day17-TaggedPointer[49770:1328036]
n1:1
n2:1
n3:1
n4:0
複製代碼
能夠看到n1 n2 n3
是通過優化的,而n4
是大數字,指針容不下該數值,不能優化。
看下面一道題,運行test1
和test2
會出現什麼問題?
- (void)test1{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 1000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
}
- (void)test2{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 1000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcsefafaefafafaefe"];
});
}
}
複製代碼
咱們先不運行,先分析一下。
首先全局隊列異步添加任務會出現多線程併發問題,在併發的時候進行寫操做會出現資源競爭問題,另一個小字符串會出現指針優化問題,小字符串和大字符串切換致使_name
結構變化,多線程同時寫入和讀會致使訪問壞內存問題,咱們來運行一下
Thread: EXC_BAD_ACCESS(code = 1)
複製代碼
直接在子線程崩潰了,崩潰函數是objc_release
。符合咱們的猜測。
驗證NSString Tagged Pointer
- (void)test{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 1; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
NSLog(@"test1 class:%@",self.name.class);
});
}
}
- (void)test2{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 1; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcsefafaefafafaefe"];
NSLog(@"test2 class:%@",self.name.class);
});
}
}
//log
test1 class:NSTaggedPointerString
test2 class:__NSCFString
複製代碼
能夠看到NSString Tagged Pointer
在小字符串的時候類是NSTaggedPointerString
,通過優化的類,大字符串的類是__NSCFString
,
拷貝分爲淺拷貝和深拷貝,淺拷貝只是引用計數+1,深拷貝是拷貝了一個對象,和以前的 互不影響, 引用計數互不影響。
拷貝目的:產生一個副本對象,跟源對象互不影響 修改源對象,不會影響到副本對象 修改副本對象,不會影響源對象
iOS提供了2中拷貝方法
void test1(){
NSString *str = @"strstrstrstr";
NSMutableString *mut1 =[str mutableCopy];
[mut1 appendFormat:@"123"];
NSString *str2 = [str copy];
NSLog(@"%p %p %p",str,mut1,str2);
}
//log
str:0x100001040
mut1:0x1007385f0
str2:0x100001040
複製代碼
能夠看到str
和str2
地址同樣,沒有從新複製出來一份,mut1
地址和str
不一致,是深拷貝,從新拷貝了一份。
咱們把字符串換成其餘經常使用的數組
void test2(){
NSArray *array = @[@"123",@"123",@"123",@"123",@"123",@"123",@"123"];
NSMutableArray *mut =[array mutableCopy];
NSString *array2 = [array copy];
NSLog(@"\n%p \n%p\n%p",array,mut,array2);
}
//log
0x102840800
0x1028408a0
0x102840800
void test3(){
NSArray *array = [@[@"123",@"123",@"123",@"123",@"123",@"123",@"123"] mutableCopy];
NSMutableArray *mut =[array mutableCopy];
NSString *array2 = [array copy];
NSLog(@"\n%p \n%p\n%p",array,mut,array2);
}
//log
0x102808720
0x1028088a0
0x1028089a0
複製代碼
從上面能夠總結看出來,不變數組拷貝出來不變數組,地址不改變,拷貝出來可變數組地址改變,可變數組拷貝出來不可變數組和可變數組,地址會改變。
咱們再換成其餘的經常使用的字典
void test4(){
NSDictionary *item = @{@"key":@"value"};
NSMutableDictionary *mut =[item mutableCopy];
NSDictionary *item2 = [item copy];
NSLog(@"\n%p \n%p\n%p",item,mut,item2);
}
//log
0x1007789c0
0x100779190
0x1007789c0
void test5(){
NSDictionary *item = [@{@"key":@"value"}mutableCopy];
NSMutableDictionary *mut =[item mutableCopy];
NSDictionary *item2 = [item copy];
NSLog(@"\n%p \n%p\n%p",item,mut,item2);
}
//log
0x1007041d0
0x1007042b0
0x1007043a0
複製代碼
從上面能夠總結看出來,不變字典拷貝出來不變字典,地址不改變,拷貝出來可變字典地址改變,可變字典拷貝出來不可變字典和可變字典,地址會改變。
由這幾個看出來,總結出來下表
類型 | copy | mutableCopy |
---|---|---|
NSString | 淺拷貝 | 深拷貝 |
NSMutableString | 淺拷貝 | 深拷貝 |
NSArray | 淺拷貝 | 深拷貝 |
NSMutableArray | 深拷貝 | 深拷貝 |
NSDictionary | 淺拷貝 | 深拷貝 |
NSMutableDictionary | 深拷貝 | 深拷貝 |
自定義的對象使用copy呢?系統的已經實現了,咱們自定義的須要本身去實現,自定義的類繼承NSCopying
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
@end
複製代碼
看到NSCopying
和NSMutableCopying
這兩個協議,對於自定義的可變對象,其實沒什麼意義,原本自定義的對象的屬性,基本都是可變的,因此只須要實現NSCopying
協議就行了。
@interface FYPerson : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int level;
@end
@interface FYPerson()<NSCopying>
@end
@implementation FYPerson
-(instancetype)copyWithZone:(NSZone *)zone{
FYPerson *p=[[FYPerson alloc]init];
p.age = self.age;
p.level = self.level;
return p;
}
@end
FYPerson *p =[[FYPerson alloc]init];
p.age = 10;
p.level = 11;
FYPerson *p2 =[p copy];
NSLog(@"%d %d",p2.age,p2.level);
//log
10 11
複製代碼
本身實現了NSCoping
協議完成了對對象的深拷貝,成功將對象的屬性複製過去了,當屬性多了怎麼辦?咱們能夠利用runtime
實現一個一勞永逸的方案。
而後將copyWithZone
利用runtime
遍歷全部的成員變量,將全部的變量都賦值,當變量多的時候,這裏也不用修改。
@implementation NSObject (add)
-(instancetype)copyWithZone:(NSZone *)zone{
Class cls = [self class];
NSObject * p=[cls new];
//成員變量個數
unsigned int count;
//賦值成員變量數組
Ivar *ivars = class_copyIvarList(self.class, &count);
//遍歷數組
for (int i = 0; i < count; i ++) {
Ivar var = ivars[i];
//獲取成員變量名字
const char * name = ivar_getName(var);
if (name != nil) {
NSString *v = [NSString stringWithUTF8String:name];
id value = [self valueForKey:v];
//給新的對象賦值
if (value != NULL) {
[p setValue:value forKey:v];
}
}
}
free(ivars);
return p;
}
@end
FYPerson *p =[[FYPerson alloc]init];
p.age = 10;
p.level = 11;
p.name = @"xiaowang";
FYPerson *p2 =[p copy];
NSLog(@"%d %d %@",p2.age,p2.level,p2.name);
//log
10
11
xiaowang
複製代碼
根據啓動順序,類別的方法在類的方法加載後邊,類別中的方法會覆蓋類的方法,因此 在基類NSObject
在類別中重寫了-(instancetype)copyWithZone:(NSZone *)zone
方法,子類就不用重寫了。達成了一勞永逸的方案。
摘自百度百科
引用計數是計算機編程語言中的一種內存管理技術,是指將資源(能夠是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變爲零時就將其釋放的過程。使用引用計數技術能夠實現自動資源管理的目的。同時引用計數還能夠指使用引用計數技術回收未使用資源的垃圾回收算法
在iOS中,使用引用計數來管理OC
對象內存,一個新建立的OC對象的引用計數默認是1,當引用計數減爲0,OC
對象就會銷燬,釋放其餘內存空間,調用retain
會讓OC
對象的引用計數+1,調用release
會讓OC
對象的引用計數-1。 當調用alloc、new、copy、mutableCopy
方法返回一個對象,在不須要這個對象時,要調用release
或者autorelease
來釋放它,想擁有某個對象,就讓他的引用計數+1,再也不擁有某個對象,就讓他引用計數-1.
在MRC中咱們常常都是這樣子使用的
FYPerson *p=[[FYPerson alloc]init];
FYPerson *p2 =[p retain];
//code here
[p release];
[p2 release];
複製代碼
可是在ARC中是系統幫咱們作了自動引用計數,不用開發者作不少繁瑣的事情了,咱們就探究下引用計數是怎麼實現的。
引用計數存儲在isa
指針中的extra_rc
,存儲值大於這個範圍的時候,則bits.has_sidetable_rc=1
而後將剩餘的RetainCount
存儲到全局的table
,key
是self
對應的值。
Retain
的runtime
源碼查找函數路徑objc_object::retain()
->objc_object::rootRetain()
->objc_object::rootRetain(bool, bool)
//大機率x==1 提升讀取指令的效率
#define fastpath(x) (__builtin_expect(bool(x), 1))
//大機率x==0 提升讀取指令的效率
#define slowpath(x) (__builtin_expect(bool(x), 0))
//引用計數+1
//tryRetain 嘗試+1
//handleOverflow 是否覆蓋
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//優化的指針 返回this
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
//old bits
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//使用聯合體技術
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);//nothing
if (!tryRetain && sideTableLocked) sidetable_unlock();//解鎖
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();////sidetable 引用計數+1
}
// don't check newisa.fast_rr; we already called any RR overrides //不嘗試retain 和 正在銷燬 什麼都不作 返回 nil if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } uintptr_t carry; //引用計數+1 (bits.extra_rc++;) newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ if (slowpath(carry)) { // newisa.extra_rc++ 溢出處理 if (!handleOverflow) { ClearExclusive(&isa.bits); return rootRetain_overflow(tryRetain); } //爲拷貝到side table 作準備 if (!tryRetain && !sideTableLocked) sidetable_lock(); sideTableLocked = true; transcribeToSideTable = true; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true; } } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(transcribeToSideTable)) { //拷貝 平外一半的 引用計數到 side table sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; } //sidetable 引用計數+1 id objc_object::sidetable_retain() { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif //取出table key=this SideTable& table = SideTables()[this]; table.lock(); size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id)this; } 複製代碼
引用計數+1,判斷了須要是指針沒有優化和isa
有沒有使用的聯合體技術,而後將判斷是否溢出,溢出的話,將extra_rc
的值複製到side table
中,設置參數isa->has_sidetable_rc=true
。
引用計數-1,在runtime
源碼中查找路徑是objc_object::release()
->objc_object::rootRelease()
->objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
,咱們進入到函數內部
ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;//指針優化的不存在計數器
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {//isa
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
//side table -1
return sidetable_release(performDealloc);
}
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive() goto underflow; } } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(sideTableLocked)) sidetable_unlock(); return false; underflow: newisa = oldisa; if (slowpath(newisa.has_sidetable_rc)) { if (!handleUnderflow) { ClearExclusive(&isa.bits); return rootRelease_underflow(performDealloc); } if (!sideTableLocked) { ClearExclusive(&isa.bits); sidetable_lock(); sideTableLocked = true; goto retry; } //side table 引用計數-1 size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); if (borrowed > 0) { newisa.extra_rc = borrowed - 1; // redo the original decrement too bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); if (!stored) { isa_t oldisa2 = LoadExclusive(&isa.bits); isa_t newisa2 = oldisa2; if (newisa2.nonpointer) { uintptr_t overflow; newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow); if (!overflow) { stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, newisa2.bits); } } } if (!stored) { // Inline update failed. // Put the retains back in the side table. sidetable_addExtraRC_nolock(borrowed); goto retry; } sidetable_unlock(); return false; } else { // Side table is empty after all. Fall-through to the dealloc path. } } //真正的銷燬 if (slowpath(newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); // does not actually return } //設置正在銷燬 newisa.deallocating = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); __sync_synchronize(); if (performDealloc) { //銷燬 ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return true; } 複製代碼
看了上邊瞭解到引用計數分兩部分,extra_rc
和side table
,探究一下 rootRetainCount()
的實現
inline uintptr_t objc_object::rootRetainCount()
{
//優化指針 直接返回
if (isTaggedPointer()) return (uintptr_t)this;
//沒優化則 到SideTable 讀取
sidetable_lock();
//isa指針
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);//啥都沒作
if (bits.nonpointer) {//使用聯合體存儲更多的數據
uintptr_t rc = 1 + bits.extra_rc;//計數數量
if (bits.has_sidetable_rc) {//當大過於 聯合體存儲的值 則另外在SideTable讀取數據
//讀取table的值 相加
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
//在sidetable 中存儲的count
return sidetable_retainCount();
}
複製代碼
當是存儲小數據的時候,指針優化,則直接返回self
,大數據的話,則table
加鎖, class
優化的以後使用聯合體存儲更多的數據,class
沒有優化則直接去sizedable
讀取數據。 優化了則在sidetable_getExtraRC_nolock()
讀取數據
//使用聯合體
size_t objc_object::sidetable_getExtraRC_nolock()
{
//不是聯合體技術 則報錯
assert(isa.nonpointer);
//key是 this,存儲了每一個對象的table
SideTable& table = SideTables()[this];
//找到 it 不然返回0
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
複製代碼
沒有優化的是直接讀取
//未使用聯合體的狀況,
uintptr_t objc_object::sidetable_retainCount()
{//沒有聯合體存儲的計數器則直接在table中取出來
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
複製代碼
當一個對象要銷燬的時候會調用dealloc
,調用軌跡是dealloc
->_objc_rootDealloc
->object_dispose
->objc_destructInstance
->free
咱們進入到objc_destructInstance
內部
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
//c++析構函數
bool cxx = obj->hasCxxDtor();
//關聯函數
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
銷燬了c++析構函數和關聯函數最後進入到clearDeallocating
,咱們進入到函數內部
//正在清除side table 和weakly referenced
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//釋放weak
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.
//釋放weak 和引用計數
clearDeallocating_slow();
}
assert(!sidetable_present());
}
複製代碼
最終調用了sidetable_clearDeallocating
和clearDeallocating_slow
實現銷燬weak
和引用計數side table
。
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
//清除weak
if (isa.weakly_referenced) {
//table.weak_table 弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
//引用計數
if (isa.has_sidetable_rc) {
//擦除 this
table.refcnts.erase(this);
}
table.unlock();
}
複製代碼
其實weak
修飾的對象會存儲在全局的SideTable
,當對象銷燬的時候會在SideTable
進行查找,時候有weak
對象,有的話則進行銷燬。
Autoreleasepool
中文名自動釋放池,裏邊裝着一些變量,當池子不須要(銷燬)的時候,release
裏邊的對象(引用計數-1)。 咱們將下邊的代碼轉化成c++
@autoreleasepool {
FYPerson *p = [[FYPerson alloc]init];
}
複製代碼
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -f main.m
轉成c++
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
FYPerson *p = ((FYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((FYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FYPerson"), sel_registerName("alloc")), sel_registerName("init"));
}
複製代碼
__AtAutoreleasePool
是一個結構體
struct __AtAutoreleasePool {
__AtAutoreleasePool() {//構造函數 生成結構體變量的時候調用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {//析構函數 銷燬的時候調用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
複製代碼
而後將上邊的代碼和c++整合到一塊兒就是這樣子
{
__AtAutoreleasePool pool = objc_autoreleasePoolPush();
FYPerson *p = [[FYPerson alloc]init];
objc_autoreleasePoolPop(pool)
}
複製代碼
在進入大括號生成一個釋放池,離開大括號則釋放釋放池,咱們再看一下釋放函數是怎麼工做的,在runtime
源碼中NSObject.mm 1848 行
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
複製代碼
pop
實現了AutoreleasePoolPage
中的對象的釋放,想了解怎麼釋放的能夠研究下源碼runtime NSObject.mm 1063行
。
其實AutoreleasePool
是AutoreleasePoolPage
來管理的,AutoreleasePoolpage
結構以下
class AutoreleasePoolPage {
magic_t const magic;
id *next;//下一個存放aotoreleass對象的地址
pthread_t const thread;//線程
AutoreleasePoolPage * const parent; //父節點
AutoreleasePoolPage *child;//子節點
uint32_t const depth;//深度
uint32_t hiwat;
}
複製代碼
AutoreleasePoolPage
在初始化在autoreleaseNewPage
申請了4096
字節除了本身變量的空間,AutoreleasePoolPage
是一個C++
實現的類
id *next
指向了棧頂最新add
進來的autorelease
對象的下一個位置AutoreleasePoolPage
的空間被佔滿時,會新建一個AutoreleasePoolPage
對象,鏈接鏈表,後來的autorelease
對象在新的page
加入AutoreleasePoolPage
每一個對象會開闢4096字節內存(也就是虛擬內存一頁的大小),除了上面的實例變量所佔空間,剩下的空間所有用來儲存autorelease
對象的地址AutoreleasePool
是按線程一一對應的(結構中的thread
指針指向當前線程)AutoreleasePool
並無單獨的結構,而是由若干個AutoreleasePoolPage
以雙向鏈表的形式組合而成(分別對應結構中的parent
指針和child
指針)其餘的都是自動釋放池的其餘對象的指針,咱們使用_objc_autoreleasePoolPrint()
能夠查看釋放池的存儲內容
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {//r1 = push()
FYPerson *p = [[FYPerson alloc]init];
_objc_autoreleasePoolPrint();
printf("\n--------------\n");
}//pop(r1)
return 0;
}
//log
objc[23958]: ##############
objc[23958]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[23958]: 3 releases pending.
objc[23958]: [0x101000000] ................ PAGE (hot) (cold)
objc[23958]: [0x101000038] ################ POOL 0x101000038
objc[23958]: [0x101000040] 0x10050cfa0 FYPerson
objc[23958]: [0x101000048] 0x10050cdb0 FYPerson
objc[23958]: ##############
--------------
複製代碼
能夠看到存儲了3 releases pending
一個對象,並且大小都8字節。再看一個複雜的,自動釋放池嵌套自動釋放池
int main(int argc, const char * argv[]) {
@autoreleasepool {//r1 = push()
FYPerson *p = [[[FYPerson alloc]init] autorelease];
FYPerson *p2 = [[[FYPerson alloc]init] autorelease];
@autoreleasepool {//r1 = push()
FYPerson *p3 = [[[FYPerson alloc]init] autorelease];
FYPerson *p4 = [[[FYPerson alloc]init] autorelease];
_objc_autoreleasePoolPrint();
printf("\n--------------\n");
}//pop(r1)
}//pop(r1)
return 0;
}
//log
objc[24025]: ##############
objc[24025]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[24025]: 6 releases pending.
objc[24025]: [0x100803000] ................ PAGE (hot) (cold)
objc[24025]: [0x100803038] ################ POOL 0x100803038
objc[24025]: [0x100803040] 0x100721580 FYPerson
objc[24025]: [0x100803048] 0x100721b10 FYPerson
objc[24025]: [0x100803050] ################ POOL 0x100803050
objc[24025]: [0x100803058] 0x100721390 FYPerson
objc[24025]: [0x100803060] 0x100717620 FYPerson
objc[24025]: ##############
複製代碼
看到了2個POOL
和四個FYPerson
對象,一共是6個對象,當出了釋放池會執行release
。
當無優化的指針調用autorelease
實際上是調用了AutoreleasePoolPage::autorelease((id)this)
->autoreleaseFast(obj)
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
//當有分頁並且分頁沒有滿就添加
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
//滿則新建一個page進行添加obj和設置hotpage
return autoreleaseFullPage(obj, page);
} else {
//沒有page則新建page進行添加
return autoreleaseNoPage(obj);
}
}
複製代碼
在MRC
中 autorealease
修飾的是的對象在沒有外部添加到自動釋放池的時候,在runloop
循環的時候會銷燬
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
//activities = 0xa0轉化成二進制 0b101 0000
系統監聽了mainRunloop 的 kCFRunLoopBeforeWaiting 和kCFRunLoopExit兩種狀態來更新autorelease的數據
//回調函數是 _wrapRunLoopWithAutoreleasePoolHandler
"<CFRunLoopObserver 0x600002538320 [0x10ce45ae8]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10f94087d), context = <CFArray 0x600001a373f0 [0x10ce45ae8]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb6dc004058>\n)}}"
複製代碼
activities = 0xa0
轉化成二進制 0b101 0000
系統監聽了mainRunloop
的 kCFRunLoopBeforeWaiting
和kCFRunLoopExit
兩種狀態來更新autorelease
的數據 回調函數是 _wrapRunLoopWithAutoreleasePoolHandler
。
void test(){
FYPerson *p =[[FYPerson alloc]init];
}
複製代碼
p
對象在某次循環中push
,在循環到kCFRunLoopBeforeWaiting
進行一次pop
,則上次循環的autolease
對象沒有其餘對象retain
的進行釋放。並非出了test()
立馬釋放。
在ARC中則執行完畢test()
會立刻釋放。
SideTable
中weak修飾的對象會在dealloc
函數執行過程當中檢測或銷燬該對象。objc_msgSend()
的消息流程從而提升性能。CADisplayLink
和Timer
本質是加到loop
循環當中,依附於循環,沒有runloop
,則不能正確執行,使用runloop
須要注意循環引用和runloop
所在的線程的釋放問題。最怕一輩子碌碌無爲,還安慰本身平凡難得。
廣告時間