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

iOS 底層原理 文章匯總

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

強應用(強持有)

假設此時有兩個界面A灵奖、B美尸,從A push 到B界面尼啡,在B界面中有如下定時器代碼论颅。當從B pop回到A界面[圖片上傳中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
時,發(fā)現(xiàn)定時器沒有停止,其方法仍然在執(zhí)行显蝌,為什么?

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

其主要原因是B界面沒有釋放订咸,即沒有執(zhí)行dealloc方法曼尊,導致timer也無法停止和釋放

解決方式一

  • 重寫didMoveToParentViewController方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無論push 進來 還是 pop 出去 正常跑
    // 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
    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);
    }];
}

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

官方文檔描述

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

iOS-底層原理 30:Block底層原理文章中阻肩,針對循環(huán)應用提供了幾種解決方式。我們我們嘗試通過__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跳轉(zhuǎn)。發(fā)現(xiàn)問題還是存在吁朦,即定時器方法仍然在執(zhí)行柒室,并沒有執(zhí)行B的dealloc方法,為什么呢逗宜?

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

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

weakSelf 與 self

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

  • 1、weakSelf會對引用計數(shù)進行+1操作嗎诲泌?

  • 2亿昏、weakSelfself 的指針地址相同嗎,是指向同一片內(nèi)存嗎档礁?

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

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

運行結果如下吝沫,發(fā)現(xiàn)前后self的引用計數(shù)都是8

引用計數(shù)獲取結果

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

  • 繼續(xù)打印weakSelfself對象呻澜,以及指針地址
po weakSelf
po self

po &weakSelf
po &self

結果如下

打印結果

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

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

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

所以在這里砰苍,我們需要區(qū)別下blocktimer循環(huán)引用的模型

  • timer模型self -> timer -> weakSelf -> self,當前的timer捕獲的是B界面的內(nèi)存,即vc對象的內(nèi)存踏烙,即weakSelf表示的是vc對象

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

解決 強引用(強持有)

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

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

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

  • didMoveToParentViewController方法,是用于當一個視圖控制器中添加或者移除viewController后魂角,必須調(diào)用的方法昵济。目的是為了告訴iOS,已經(jīng)完成添加/刪除子控制器的操作野揪。

  • 在B界面中重寫didMoveToParentViewController方法

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

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

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

  • 將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

運行發(fā)現(xiàn)執(zhí)行dealloc之后,timer還是會繼續(xù)執(zhí)行廊酣。原因是解決了中介者的釋放能耻,但是沒有解決中介者的回收,即self.target的回收亡驰。所以這種方式有缺陷

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

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

運行結果如下凡辱,發(fā)現(xiàn)pop之后戒职,timer釋放,從而中介者也會進行回收釋放


思路二運行結果-2

思路三:自定義封裝timer

這種方式是根據(jù)思路二的原理透乾,自定義封裝timer洪燥,其步驟如下

  • 自定義timerWapper
    • 在初始化方法中,定義一個timer乳乌,其target是自己捧韵。即timerWapper中的timer,一直監(jiān)聽自己汉操,判斷selector再来,此時的selector已交給了傳入的target(即vc對象),此時有一個方法fireHomeWapper磷瘤,在方法中芒篷,判斷target是否存在
      • 如果target存在,則需要讓vc知道采缚,即向傳入的target發(fā)送selector消息针炉,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知fireHome方法扳抽,就這事這種方式定時器方法能夠執(zhí)行的原因

      • 如果target不存在篡帕,已經(jīng)釋放了,則釋放當前的timerWrapper贸呢,即打破了RunLoop對timeWrapper的強持有 (timeWrapper <-×- RunLoop

    • 自定義cjl_invalidate方法中釋放timer赂苗。這個方法在vc的dealloc方法中調(diào)用,即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朴沿,即監(jiān)聽自己
            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發(fā)送selector消息败砂,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知`fireHome`方法魏铅,就這事這種方式定時器方法能夠執(zhí)行的原因
        //objc_msgSend發(fā)送消息昌犹,執(zhí)行定時器方法
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
    }else{
        //如果target不存在,已經(jīng)釋放了览芳,則釋放當前的timerWrapper
        [wapper.timer invalidate];
        wapper.timer = nil;
    }
}

//在vc的dealloc方法中調(diào)用斜姥,通過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虛基類,可以交給其子類實現(xiàn)糕非,NSProxy的介紹在iOS-底層原理 30:Block底層原理已經(jīng)介紹過了蒙具,這里不再重復

  • 首先定義一個繼承自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;
}

這樣做的主要目的是將強引用的注意力轉(zhuǎn)移成了消息轉(zhuǎn)發(fā)朽肥。虛基類只負責消息轉(zhuǎn)發(fā)禁筏,即使用NSProxy作為中間代理、中間者

這里有個疑問衡招,定義的proxy對象篱昔,在dealloc釋放時,還存在嗎蚁吝?

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

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市框冀,隨后出現(xiàn)的幾起案子流椒,更是在濱河造成了極大的恐慌,老刑警劉巖明也,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宣虾,死亡現(xiàn)場離奇詭異惯裕,居然都是意外死亡,警方通過查閱死者的電腦和手機绣硝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門蜻势,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鹉胖,你說我怎么就攤上這事握玛。” “怎么了甫菠?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵挠铲,是天一觀的道長。 經(jīng)常有香客問我淑蔚,道長市殷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任刹衫,我火速辦了婚禮醋寝,結果婚禮上,老公的妹妹穿的比我還像新娘带迟。我一直安慰自己音羞,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布仓犬。 她就那樣靜靜地躺著嗅绰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搀继。 梳的紋絲不亂的頭發(fā)上窘面,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音叽躯,去河邊找鬼财边。 笑死,一個胖子當著我的面吹牛点骑,可吹牛的內(nèi)容都是我干的酣难。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼黑滴,長吁一口氣:“原來是場噩夢啊……” “哼憨募!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起袁辈,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤菜谣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葛菇,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡甘磨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了眯停。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡卿泽,死狀恐怖莺债,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情签夭,我是刑警寧澤齐邦,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站第租,受9級特大地震影響措拇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜慎宾,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一丐吓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趟据,春花似錦券犁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至咳促,卻和暖如春稚新,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跪腹。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工褂删, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尺迂。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓笤妙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親噪裕。 傳聞我的和親對象是個殘疾皇子蹲盘,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容