iOS-底層原理 33:內存管理(二)強引用分析

iOS 底層原理 文章匯總

本文主要是通過定時器來梳理強引用的幾種解決方案

強應用(強持有)

假設此時有兩個界面A、B,從A push 到B界面,在B界面中有如下定時器代碼。當從B pop回到A界面[圖片上傳中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
時,發現定時器沒有停止,其方法仍然在執行,為什么?

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

其主要原因是B界面沒有釋放,即沒有執行dealloc方法,導致timer也無法停止和釋放

解決方式一

  • 重寫didMoveToParentViewController方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無論push 進來 還是 pop 出去 正常跑
    // 就算繼續push 到下一層 pop 回去還是繼續
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

解決方式二

  • 定義timer時,采用閉包的形式,因此不需要指定target
- (void)blockTimer{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer fire - %@",timer);
    }];
}

現在,我們從底層來深入研究,為什么B界面有了timer之后,導致B界面釋放不掉,即不會走到dealloc方法。我們可以通過官方文檔查看timerWithTimeInterval:target:selector:userInfo:repeats:方法中對target的描述

官方文檔描述

從文檔中可以看出,timer對傳入的target具有強持有,即timer持有self。由于timer是定義在B界面中,所以self也持有timer,因此 self -> timer -> self構成了循環引用

iOS-底層原理 30:Block底層原理文章中,針對循環應用提供了幾種解決方式。我們我們嘗試通過__weak弱引用來解決,代碼修改如下

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

我們再次運行程序,進行push-pop跳轉。發現問題還是存在,即定時器方法仍然在執行,并沒有執行B的dealloc方法,為什么呢?

  • 我們使用__weak雖然打破了 self -> timer -> self之前的循環引用,即引用鏈變成了self -> timer -> weakSelf -> self。但是在這里我們的分析并不全面,此時還有一個Runloop對timer的強持有,因為Runloop生命周期B界面更長,所以導致了timer無法釋放,同時也導致了B界面的self也無法釋放。所以,最初引用鏈應該是這樣的
    引用鏈-1

    加上weakSelf之后,變成了這樣
    引用鏈-2

weakSelf 與 self

對于weakSelfself,主要有以下兩個疑問

  • 1、weakSelf會對引用計數進行+1操作嗎?

  • 2、weakSelfself 的指針地址相同嗎,是指向同一片內存嗎?

  • 帶著疑問,我們在weakSelf前后打印self的引用計數

NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

運行結果如下,發現前后self的引用計數都是8

引用計數獲取結果

因此可以得出一個結論:weakSelf沒有對內存進行+1操作

  • 繼續打印weakSelfself對象,以及指針地址
po weakSelf
po self

po &weakSelf
po &self

結果如下

打印結果

從打印結果可以看出,當前self取地址 和 weakSelf取地址的值是不一樣的。意味著有兩個指針地址,指向的是同一片內存空間,即weakSelf 和 self 的內存地址是不一樣,都指向同一片內存空間
圖示

  • 從上面打印可以看出,此時timer捕獲的是<LGTimerViewController: 0x7f890741f5b0>,是一個對象,所以無法通過weakSelf來解決強持有。即引用鏈關系為:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)。所以RunLoop對整個 對象的空間有強持有,runloop沒停,timer 和 weakSelf是無法釋放的

  • 而我們在Block原理中提及的block的循環引用,與timer的是有區別的。通過block底層原理的方法__Block_object_assign可知,block捕獲的是 對象的指針地址,即weakself 是 臨時變量的指針地址,跟self沒有關系,因為weakSelf是新的地址空間。所以此時的weakSelf相當于中間值。其引用關系鏈為self -> block -> weakSelf(臨時變量的指針地址),可以通過地址拿到指針

所以在這里,我們需要區別下blocktimer循環引用的模型

  • timer模型self -> timer -> weakSelf -> self,當前的timer捕獲的是B界面的內存,即vc對象的內存,即weakSelf表示的是vc對象

  • Block模型self -> block -> weakSelf -> self,當前的block捕獲的是指針地址,即weakSelf表示的是指向self的臨時變量的指針地址

解決 強引用(強持有)

以下幾種方法的思路均是:依賴中介者模式打破強持有,其中推薦思路四

思路一:pop時在其他方法中銷毀timer

根據前面的解釋,我們知道由于Runloop對timer的強持有,導致了Runloop間接的強持有了self(因為timer中捕獲的是vc對象)。所以導致dealloc方法無法執行。需要查看在pop時,是否還有其他方法可以銷毀timer。這個方法就是didMoveToParentViewController

  • didMoveToParentViewController方法,是用于當一個視圖控制器中添加或者移除viewController后,必須調用的方法。目的是為了告訴iOS,已經完成添加/刪除子控制器的操作。

  • 在B界面中重寫didMoveToParentViewController方法

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無論push 進來 還是 pop 出去 正常跑
    // 就算繼續push 到下一層 pop 回去還是繼續
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

思路二:中介者模式,即不使用self,依賴于其他對象

在timer模式中,我們重點關注的是fireHome能執行,并不關心timer捕獲的target是誰,由于這里不方便使用self(因為會有強持有問題),所以可以將target換成其他對象,例如將target換成NSObject對象,將fireHome交給target執行

  • 將timer的target 由self改成objc
//**********1、定義其他對象**********
@property (nonatomic, strong) id            target;

//**********1、修改target**********
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];

//**********3、imp**********
void fireHomeObjc(id obj){
    NSLog(@"%s -- %@",__func__,obj);
}

運行結果如下

思路二運行結果-1

運行發現執行dealloc之后,timer還是會繼續執行。原因是解決了中介者的釋放,但是沒有解決中介者的回收,即self.target的回收。所以這種方式有缺陷

可以通過在dealloc方法中,取消定時器來解決,代碼如下

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}

運行結果如下,發現pop之后,timer釋放,從而中介者也會進行回收釋放


思路二運行結果-2

思路三:自定義封裝timer

這種方式是根據思路二的原理,自定義封裝timer,其步驟如下

  • 自定義timerWapper
    • 在初始化方法中,定義一個timer,其target是自己。即timerWapper中的timer,一直監聽自己,判斷selector,此時的selector已交給了傳入的target(即vc對象),此時有一個方法fireHomeWapper,在方法中,判斷target是否存在
      • 如果target存在,則需要讓vc知道,即向傳入的target發送selector消息,并將此時的timer參數也一并傳入,所以vc就可以得知fireHome方法,就這事這種方式定時器方法能夠執行的原因

      • 如果target不存在,已經釋放了,則釋放當前的timerWrapper,即打破了RunLoop對timeWrapper的強持有 (timeWrapper <-×- RunLoop

    • 自定義cjl_invalidate方法中釋放timer。這個方法在vc的dealloc方法中調用,即vc釋放,從而導致timerWapper釋放,打破了vctimeWrapper的的強持有( vc -×-> timeWrapper
//*********** .h文件 ***********
@interface CJLTimerWapper : NSObject

- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cjl_invalidate;

@end

//*********** .m文件 ***********
#import "CJLTimerWapper.h"
#import <objc/message.h>

@interface CJLTimerWapper ()

@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;

@end

@implementation CJLTimerWapper

- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    if (self == [super init]) {
        //傳入vc
        self.target = aTarget;
        //傳入的定時器方法
        self.aSelector = aSelector;
        
        if ([self.target respondsToSelector:self.aSelector]) {
            Method method = class_getInstanceMethod([self.target class], aSelector);
            const char *type = method_getTypeEncoding(method);
            //給timerWapper添加方法
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
            
            //啟動一個timer,target是self,即監聽自己
            self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

//一直跑runloop
void fireHomeWapper(CJLTimerWapper *wapper){
    //判斷target是否存在
    if (wapper.target) {
        //如果存在則需要讓vc知道,即向傳入的target發送selector消息,并將此時的timer參數也一并傳入,所以vc就可以得知`fireHome`方法,就這事這種方式定時器方法能夠執行的原因
        //objc_msgSend發送消息,執行定時器方法
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
    }else{
        //如果target不存在,已經釋放了,則釋放當前的timerWrapper
        [wapper.timer invalidate];
        wapper.timer = nil;
    }
}

//在vc的dealloc方法中調用,通過vc釋放,從而讓timer釋放
- (void)cjl_invalidate{
    [self.timer invalidate];
    self.timer = nil;
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

@end

  • timerWapper的使用
//定義
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];

//釋放
- (void)dealloc{
     [self.timerWapper cjl_invalidate];
}

運行結果如下


自定義timerWapper運行結果

這種方式看起來比較繁瑣,步驟很多,而且針對timerWapper,需要不斷的添加method,需要進行一系列的處理。

思路四:利用NSProxy虛基類的子類

下面來介紹一種timer強引用最常用的處理方式:NSProxy子類

可以通過NSProxy虛基類,可以交給其子類實現,NSProxy的介紹在iOS-底層原理 30:Block底層原理已經介紹過了,這里不再重復

  • 首先定義一個繼承自NSProxy的子類
//************NSProxy子類************
@interface CJLProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

@interface CJLProxy()
@property (nonatomic, weak) id object;
@end

@implementation CJLProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    CJLProxy *proxy = [CJLProxy alloc];
    proxy.object = object;
    return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}


  • timer中的target傳入NSProxy子類對象,即timer持有NSProxy子類對象
//************解決timer強持有問題************
self.proxy = [CJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];

//在dealloc中將timer正常釋放
- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}

這樣做的主要目的是將強引用的注意力轉移成了消息轉發。虛基類只負責消息轉發,即使用NSProxy作為中間代理、中間者

這里有個疑問,定義的proxy對象,在dealloc釋放時,還存在嗎?

  • proxy對象會正常釋放,因為vc正常釋放了,所以可以釋放其持有者,即timer和proxytimer的釋放也打破了runLoop對proxy的強持有。完美的達到了兩層釋放,即 vc -×-> proxy <-×- runloop,解釋如下:
    • vc釋放,導致了proxy的釋放

    • dealloc方法中,timer進行了釋放,所以runloop強引用也釋放了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容