iOS 底層探索:對(duì)象的生命周期 & strong & weak & 強(qiáng)弱引用

iOS 底層探索: 學(xué)習(xí)大綱 OC篇

前言

  • 之前我們分析過內(nèi)存管理的一些操作, OC中的內(nèi)存管理是通過引用計(jì)數(shù)器來實(shí)現(xiàn)的熊锭。一個(gè)對(duì)象的聲明周期取決于它是否還沒其他對(duì)象引用毕荐,即retainCount是否等于0垄懂。 但在有些情況下,在某個(gè)對(duì)象的生命周期中模捂,我們并不希望對(duì)象的銷毀時(shí)間由是否被其他對(duì)象應(yīng)用來決定勋拟,而是這個(gè)對(duì)象本該是什么時(shí)候銷毀就什么時(shí)候被銷毀。因此引入對(duì)象的生命周期苔货、犀概、弱引用、強(qiáng)引用的概念夜惭。

準(zhǔn)備:

內(nèi)容:

    1. 對(duì)象的生命周期
    1. strong & weak
    1. 強(qiáng)弱引用

一姻灶、對(duì)象的生命周期

定義:

在OC中一個(gè)對(duì)象的生命周期就是指,這個(gè)對(duì)象從創(chuàng)建到銷毀的運(yùn)行時(shí)(runtime)的生命過程诈茧。從內(nèi)存管理引用計(jì)數(shù)的層面來講产喉,就是引用計(jì)數(shù)從1變成0的過程。

  • MRC中若皱,手動(dòng)管理內(nèi)存镊叁,一個(gè)對(duì)象的生命周期經(jīng)歷了alloc、retain走触、release晦譬、dealloc等一些列的過程,直到對(duì)象的引用計(jì)數(shù)為0互广,被釋放結(jié)束了敛腌。
  • ARC中,內(nèi)存是系統(tǒng)(LLVM和Runtime的共同結(jié)果)自動(dòng)管理的惫皱,其實(shí) ARC 內(nèi)部機(jī)制原理也是來源于mrc像樊,一個(gè)對(duì)象的生命周期,大多是由系統(tǒng)自動(dòng)管理的旅敷;
對(duì)象的創(chuàng)建途徑:
    1. 程序顯示的創(chuàng)建并初始化生棍;
    1. 對(duì)象作為另一個(gè)對(duì)象的副本;
    1. unArchiving:從已歸檔的二進(jìn)制數(shù)據(jù)流中解碼媳谁,如果一個(gè)對(duì)象是從一個(gè)nib文件中被unArchive的話涂滴,在所有的nib文件中的對(duì)象都被裝載到內(nèi)存之后友酱,就會(huì)收到一個(gè)名叫 awakeFromNib 的消息
圖解對(duì)象生命周期

總結(jié):在對(duì)象的創(chuàng)建和初始化之后,只要對(duì)象的retainCount的值比0大柔纵,那么它就會(huì)一直存在在內(nèi)存中缔杉。通過想一個(gè)對(duì)象發(fā)送retain消息,或者進(jìn)行copy操作搁料。其他的對(duì)象可以引用并持有該對(duì)象的所有權(quán)或详。同時(shí),移除引用的時(shí)候要發(fā)送release消息郭计。

二霸琴、weak 和 strong

weak的底層實(shí)現(xiàn)原理

舉例:

int main(){
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}

clang 編譯報(bào)錯(cuò)提示:

int main(){
    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    id __attribute__((objc_ownership(weak))) obj1 = obj;
}
  • objc_ownership字面意思是:獲得對(duì)象的所有權(quán),是對(duì)對(duì)象weak的初始化的一個(gè)操作

那么開啟匯編調(diào)試:
  • 調(diào)用objc_initWeak存入sidetable表昭伸;
  • 調(diào)用objc_loadWeakRetain返回自身沈贝,并引用計(jì)數(shù)+1(refcnts的value+固定增量值);
  • __weak修飾的 obj1是個(gè)作用域內(nèi)的臨時(shí)變量勋乾,所以出了作用域就被釋放了宋下。

objc_initWeak為出發(fā)點(diǎn)去Objc-4源碼中查看:


/** 
 * Initialize a fresh weak pointer to some object location. 
 * It would be used for code like: 
 *
 * (The nil case) 
 * __weak id weakPtr;
 * (The non-nil case) 
 * NSObject *o = ...;
 * __weak id weakPtr = o;
 * 
 * This function IS NOT thread-safe with respect to concurrent 
 * modifications to the weak variable. (Concurrent weak clear is safe.)
 *
 * @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}
 //location : __weak指針 的地址 ,存儲(chǔ)指針的地址辑莫,這樣便可以在最后將其指向的對(duì)象置為nil学歧。
// newObj :所引用的對(duì)象。即例子中的obj 各吨。
  • 從上面的代碼可以看出objc_initWeak方法只是一個(gè)深層次函數(shù)調(diào)用的入口枝笨,在該方法內(nèi)部調(diào)用了storeWeak方法。下面我們來看下storeWeak方法的實(shí)現(xiàn)代碼揭蜒。
static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {// 如果weak ptr之前弱引用過一個(gè)obj横浑,則將這個(gè)obj所對(duì)應(yīng)的SideTable取出,賦值給oldTable
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;// 如果weak ptr之前沒有弱引用過一個(gè)obj屉更,則oldTable = nil
    }
    if (haveNew) { // 如果weak ptr要weak引用一個(gè)新的obj徙融,則將該obj對(duì)應(yīng)的SideTable取出,賦值給newTable
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil; // 如果weak ptr不需要引用一個(gè)新obj瑰谜,則newTable = nil
    }

// 加鎖操作欺冀,防止多線程中競(jìng)爭(zhēng)沖突
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

// location 應(yīng)該與 oldObj 保持一致,如果不同萨脑,說明當(dāng)前的 location 已經(jīng)處理過 oldObj 可是又被其他線程所修改
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized())   // 如果cls還沒有初始化隐轩,先初始化,再嘗試設(shè)置weak

        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls; // 這里記錄一下previouslyInitializedClass渤早, 防止改if分支再次進(jìn)入

            goto retry; // 重新獲取一遍newObj职车,這時(shí)的newObj應(yīng)該已經(jīng)初始化過了
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); // 如果weak_ptr之前弱引用過別的對(duì)象oldObj,則調(diào)用weak_unregister_no_lock,在oldObj的weak_entry_t中移除該weak_ptr地址
    }

    // Assign new value, if any.
    if (haveNew) {  // 如果weak_ptr需要弱引用新的對(duì)象newObj

// (1) 調(diào)用weak_register_no_lock方法悴灵,將weak ptr的地址記錄到newObj對(duì)應(yīng)的weak_entry_t中
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

// (2) 更新newObj的isa的weakly_referenced bit標(biāo)志位
        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
// (3)*location 賦值军援,也就是將weak ptr直接指向了newObj〕蒲可以看到,這里并沒有將newObj的引用計(jì)數(shù)+1
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    // 解鎖涯竟,其他線程可以訪問oldTable, newTable了
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

// 返回newObj赡鲜,此時(shí)的newObj與剛傳入時(shí)相比,weakly-referenced bit位置1
    return (id)newObj;
}

storeWeak具體流程如下:

  1. storeWeak 方法實(shí)際上是接收了5個(gè)參數(shù)庐船,分別是 haveOld银酬、haveNew和crashIfDeallocating ,這三個(gè)參數(shù)都是以模板的方式傳入的筐钟,是三個(gè)bool類型的參數(shù)揩瞪。分別表示weak指針之前是否指向了一個(gè)弱引用,weak指針是否需要指向一個(gè)新的引用篓冲,若果被弱引用的對(duì)象正在析構(gòu)李破,此時(shí)再弱引用該對(duì)象是否應(yīng)該crash。
  2. 該方法維護(hù)了 oldTable 和 newTable 分別表示舊的引用弱表和新的弱引用表壹将,它們都是 SideTable 的hash表嗤攻。
  3. 如果weak指針之前指向了一個(gè)弱引用,則會(huì)調(diào)用 weak_unregister_no_lock 方法將舊的weak指針地址移除诽俯。
  4. 如果weak指針需要指向一個(gè)新的引用妇菱,則會(huì)調(diào)用 weak_register_no_lock 方法將新的weak指針地址添加到弱引用表中
  5. 調(diào)用 setWeaklyReferenced_nolock 方法修改weak新引用的對(duì)象的bit標(biāo)志位

有關(guān)SideTableiOS 底層探索:內(nèi)存管理 (上) 中講的有喔暴区,這里主要講下:weak_table_t

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries; //hash數(shù)組闯团,用來存儲(chǔ)弱引用對(duì)象的相關(guān)信息weak_entry_t
    size_t    num_entries; //hash數(shù)組中的元素個(gè)數(shù)
    uintptr_t mask; //hash數(shù)組長(zhǎng)度-1,會(huì)參與hash計(jì)算仙粱。(注意房交,這里是hash數(shù)組的長(zhǎng)度,而不是元素個(gè)數(shù)伐割。比如涌萤,數(shù)組長(zhǎng)度可能是64,而元素個(gè)數(shù)僅存了2個(gè))
    uintptr_t max_hash_displacement; //可能會(huì)發(fā)生的hash沖突的最大次數(shù)口猜,用于判斷是否出現(xiàn)了邏輯錯(cuò)誤(hash表中的沖突次數(shù)絕不會(huì)超過改值)
};
  • weak_table_t是一個(gè)典型的hash結(jié)構(gòu)负溪。
  • weak_entries是一個(gè)動(dòng)態(tài)數(shù)組,用來存儲(chǔ)weak_entry_t類型的元素济炎,這些元素實(shí)際上就是OC對(duì)象的弱引用信息川抡。
  • weak_entry_t是存儲(chǔ)在弱引用表中的一個(gè)內(nèi)部結(jié)構(gòu)體,它負(fù)責(zé)維護(hù)和存儲(chǔ)指向一個(gè)對(duì)象的所有弱引用hash表。其定義如下:
typedef objc_object ** weak_referrer_t; //objc_object是weak_entry_t表中weak弱引用對(duì)象的范型對(duì)象的結(jié)構(gòu)體結(jié)構(gòu)崖堤。
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  //范型
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1; //最低有效位侍咱,也是標(biāo)志位。當(dāng)標(biāo)志位 0 時(shí)密幔,增加引用表指針緯度楔脯。
            uintptr_t        num_refs : PTR_MINUS_1; //引用數(shù)值。這里記錄弱引用表中引用有效數(shù)字胯甩,因?yàn)槿跻帽硎褂玫氖庆o態(tài) hash 結(jié)構(gòu)昧廷,所以需要使用變量來記錄數(shù)目。
            uintptr_t        mask; //計(jì)數(shù)輔助量偎箫。
            uintptr_t        max_hash_displacement; //hash 元素上限閥值木柬。
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    }
}

weak的整體實(shí)現(xiàn)流程如圖:

【總結(jié)】:
1:?先我們知道有?個(gè)?常?逼的家伙-sideTable
2:得到sideTable的weakTable 弱引?表
3:創(chuàng)建?個(gè)weak_entry_t
4:把referent加?到weak_entry_t的數(shù)組inline_referrers
5:把weak_table擴(kuò)容?下
6:把new_entry加?到weak_table中

objc_loadWeakRetain的執(zhí)行流程如下:

/*
  Once upon a time we eagerly cleared *location if we saw the object 
  was deallocating. This confuses code like NSPointerFunctions which 
  tries to pre-flight the raw storage and assumes if the storage is 
  zero then the weak system is done interfering. That is false: the 
  weak system is still going to check and clear the storage later. 
  This can cause objc_weak_error complaints and crashes.
  So we now don't touch the storage until deallocation completes.
*/

id
objc_loadWeakRetained(id *location)
{
    id obj;
    id result;
    Class cls;

    SideTable *table;
    
 retry:
    // fixme std::atomic this load
    obj = *location;
    if (!obj) return nil;
    if (obj->isTaggedPointer()) return obj;
    
    table = &SideTables()[obj];
    
    table->lock();
    if (*location != obj) {
        table->unlock();
        goto retry;
    }
    
    result = obj;

    cls = obj->ISA();
    if (! cls->hasCustomRR()) {
        // Fast case. We know +initialize is complete because
        // default-RR can never be set before then.
        ASSERT(cls->isInitialized());
        if (! obj->rootTryRetain()) {
            result = nil;
        }
    }
    else {
        // Slow case. We must check for +initialize and call it outside
        // the lock if necessary in order to avoid deadlocks.
        if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
            BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
                class_getMethodImplementation(cls, @selector(retainWeakReference));
            if ((IMP)tryRetain == _objc_msgForward) {
                result = nil;
            }
            else if (! (*tryRetain)(obj, @selector(retainWeakReference))) {
                result = nil;
            }
        }
        else {
            table->unlock();
            class_initialize(cls, obj);
            goto retry;
        }
    }
        
    table->unlock();
    return result;
}
  • 這段代碼的核心就是:retainWeakReference->......rootRetain的過程。進(jìn)行引用計(jì)數(shù)加1
weak釋放為nil過程

1淹办、調(diào)用objc_release
2眉枕、因?yàn)閷?duì)象的引用計(jì)數(shù)為0,所以執(zhí)行dealloc
3怜森、在dealloc中速挑,調(diào)用了_objc_rootDealloc函數(shù)
4、在_objc_rootDealloc中副硅,調(diào)用了object_dispose函數(shù)
5梗摇、調(diào)用objc_destructInstance
6、最后調(diào)用objc_clear_deallocating

我們只看下objc_clear_deallocating

void objc_clear_deallocating(id obj) 
{
    assert(obj);
    assert(!UseGC);
    if (obj->isTaggedPointer()) return;
    obj->clearDeallocating();
}

//執(zhí)行 clearDeallocating方法
inline void objc_object::clearDeallocating()
{
    sidetable_clearDeallocating();
}
// 執(zhí)行sidetable_clearDeallocating想许,找到weak表中的value值
void  objc_object::sidetable_clearDeallocating()
{
    SideTable *table = SideTable::tableForPointer(this);
    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    spinlock_lock(&table->slock);
    RefcountMap::iterator it = table->refcnts.find(this); //這里可以再研究下
    if (it != table->refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
//清理對(duì)象
            weak_clear_no_lock(&table->weak_table, (id)this);
        }
        table->refcnts.erase(it);
    }
    spinlock_unlock(&table->slock);
}
  • clearDeallocating函數(shù)首先根據(jù)對(duì)象地址獲取所有weak指針地址的數(shù)組伶授,然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)為nil,最后把這個(gè)entry從weak表中刪除流纹,最后清理對(duì)象的記錄糜烹。

  • objc_clear_deallocating該函數(shù) 具體流程如下:

1、從weak表中獲取廢棄對(duì)象的地址為鍵值的記錄
2漱凝、將包含在記錄中的所有附有 weak修飾符變量的地址疮蹦,賦值為nil
3、將weak表中該記錄刪除
4茸炒、從引用計(jì)數(shù)表中刪除廢棄對(duì)象的地址為鍵值的記錄

總結(jié)

weak是Runtime維護(hù)了一個(gè)hash(哈希)表愕乎,用于存儲(chǔ)指向某個(gè)對(duì)象的所有weak指針。weak表其實(shí)是一個(gè)hash(哈希)表壁公,Key是所指對(duì)象的地址感论,Value是weak指針的地址(這個(gè)地址的值是所指對(duì)象指針的地址)數(shù)組。

strong的底層實(shí)現(xiàn)原理

如果是用屬性strong紊册,同樣可以用Clang去查看源碼比肄。這里只做匯編調(diào)試如下:


我們進(jìn)入objc_storeStrong源碼:

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);//retain新值
    *location = obj;
    objc_release(prev);//release舊值
}

三、弱引用 & 強(qiáng)引用

概念

弱引用(Weak Reference):當(dāng)前對(duì)象的聲明周期不被是否由其他其他對(duì)象引用限制芳绩,它本該什么時(shí)候銷毀就什么時(shí)候銷毀掀亥。計(jì)時(shí)它的引用沒斷,但是當(dāng)它的生存周期到了就會(huì)被銷毀妥色。

強(qiáng)引用(Strong Reference ):當(dāng)前對(duì)象被其他對(duì)象引用時(shí)搪花,會(huì)執(zhí)行retain,引用計(jì)數(shù)+1.當(dāng)retainCount=0時(shí)嘹害,該對(duì)象才會(huì)被銷毀撮竿。 默認(rèn)情況下是強(qiáng)引用方式。

簡(jiǎn)單的說:

當(dāng)用指針指向某個(gè)對(duì)象時(shí)吼拥,你可以通過retain/release管理它的內(nèi)存,也可以不管理线衫。
如果你管理了凿可,就擁有對(duì)這個(gè)對(duì)象的強(qiáng)引用;
如果你沒有管理授账,那么你擁有的就是弱引用枯跑。

使用

__weak

  • weak嚴(yán)格的說應(yīng)當(dāng)叫“ 歸零弱引用 ”,weak相當(dāng)于老版本的assign白热,即當(dāng)對(duì)象被銷毀后敛助,會(huì)自動(dòng)的把它的指針置為nil,這樣可以防止野指針錯(cuò)誤屋确。
  • weak 作為屬性的關(guān)鍵字的作用弱引用纳击,所引用對(duì)象的計(jì)數(shù)器不會(huì)加一,并在引用對(duì)象被釋放的時(shí)候自動(dòng)被設(shè)置為 nil攻臀。
__weak NSObject *obj;

__strong

  • 變量聲明默認(rèn)都帶有strong關(guān)鍵字焕数,如果變量什么關(guān)鍵字都不寫,那么就默認(rèn)為強(qiáng)引用刨啸,strong相當(dāng)于老版本的retain 堡赔;
__strong NSObject *obj;
驗(yàn)證

將obj2聲明改為__weak

  • 從上面可以看出使用__strong 和__weak的區(qū)別,因?yàn)開_strong修飾的對(duì)象會(huì)使對(duì)象本身dretainCount+1设联,而weak的并不會(huì)善已。
    所以第一個(gè)例子的retainCount為2,obj1=nil之后retainCount為1离例,并不會(huì)對(duì)obj2造成影響换团,而第二個(gè)例子obj1=nil之后retainCount 為0了,內(nèi)存也跟著釋放了宫蛆,所以obj2也為nil啥寇。
weakSelf 與 self
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

打印weakSelf 和 self對(duì)象,以及指針地址:


  • 當(dāng)前self取地址 和 weakSelf取指針的地址的值是不一樣的。意味著有兩個(gè)指針地址辑甜,指向的是同一片內(nèi)存空間衰絮,即weakSelf 和 self 的內(nèi)存地址是不一樣,都指向同一片內(nèi)存空間的

強(qiáng)引用的舉例分析:NSTimer(計(jì)時(shí)器)

- (void)createTimer {
    self.timer = [NSTimer timerWithTimeInterval:1 target: self selector:@selector(fireHome) userInfo:nil repeats:YES];
     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}
- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}
  • 我們運(yùn)行程序磷醋,進(jìn)行push-pop跳轉(zhuǎn),發(fā)現(xiàn)定時(shí)器方法仍然在執(zhí)行猫牡,并沒有執(zhí)行B的dealloc方法;
  • 我們知道:NSTimer創(chuàng)建后邓线,需要手動(dòng)加入到Runloop中才可以運(yùn)行淌友,但timer會(huì)使得當(dāng)前控制器不走dealloc方法,導(dǎo)致timer控制器無(wú)法釋放
解決方式一: pop時(shí)在其他方法中銷毀timer
  • 重寫didMoveToParentViewController方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無(wú)論push 進(jìn)來 還是 pop 出去 正常跑
    // 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}
解決方式二:

定義timer時(shí)枢贿,采用閉包的形式担钮,因此不需要指定target:

- (void)blockTimer{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer fire - %@",timer);
    }];
}
  • 這兩個(gè)解決方法不是我們要研究的,為了對(duì)強(qiáng)引用進(jìn)行拓展研究器联。所以回到最開始的地方;

先看看官方文檔NSTimertimerWithTimeInterval:target:selector:userInfo:repeats:方法

  • 從文檔中可以看出,timer對(duì)傳入的target具有強(qiáng)持有婿崭,即timer持有self拨拓。由于timer是定義在B界面中,所以self也持有timer氓栈,因此 self -> timer -> self構(gòu)成了循環(huán)引用
  • 我們我們嘗試通過__weak即弱引用來解決渣磷,代碼修改如下:
//typeof(self)是獲取到self的類型,這樣定義的weakSelf就是和self一個(gè)類型的,加上__weak是建立一個(gè)弱引用
__weak typeof(self) weakSelf = self;  //定義了一個(gè)弱引用性質(zhì)的替身.
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

我們?cè)俅芜\(yùn)行程序,進(jìn)行push-pop跳轉(zhuǎn)授瘦。發(fā)現(xiàn)問題還是存在醋界,即定時(shí)器方法仍然在執(zhí)行,并沒有執(zhí)行B的dealloc方法提完。

  • 為什么呢物独?因?yàn)槲覀兊姆治霾⒉蝗妫藭r(shí)還有一個(gè)Runloop對(duì)timer的強(qiáng)持有氯葬,因?yàn)镽unloop的生命周期比B界面更長(zhǎng)挡篓,所以導(dǎo)致了timer無(wú)法釋放。

  • 拓展循環(huán)引用的模型:

timer模型:self -> timer -> weakSelf -> self,當(dāng)前的timer捕獲的是B界面的內(nèi)存帚称,即vc對(duì)象的內(nèi)存官研,即weakSelf表示的是vc對(duì)象

Block模型:self -> block -> weakSelf -> self,當(dāng)前的block捕獲的是指針地址闯睹,即weakSelf表示的是指向self的臨時(shí)變量的指針地址

解決方式三:中介者模式戏羽,即不使用self,依賴于其他對(duì)象

將target換成NSObject對(duì)象楼吃,將fireHome交給target執(zhí)行:

//**********1始花、定義其他對(duì)象**********
@property (nonatomic, strong) id            target;

//**********2妄讯、修改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);
}

運(yùn)行發(fā)現(xiàn)timer還是會(huì)繼續(xù)執(zhí)行酷宵。原因是解決了中介者的釋放亥贸,但是沒有解決中介者的回收,即self.target的回收浇垦。

所以還需要釋放timer

解決方式四:自定義封裝timer

這種方式是根據(jù)思路三的原理炕置,自定義封裝timer,其實(shí)現(xiàn)如下

//*********** .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;
        //傳入的定時(shí)器方法
        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);
            
            //啟動(dòng)一個(gè)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消息此虑,并將此時(shí)的timer參數(shù)也一并傳入甚纲,所以vc就可以得知`fireHome`方法,就這事這種方式定時(shí)器方法能夠執(zhí)行的原因
        //objc_msgSend發(fā)送消息朦前,執(zhí)行定時(shí)器方法
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
    }else{
        //如果target不存在介杆,已經(jīng)釋放了,則釋放當(dā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

使用方式:

@property (nonatomic, strong) LGTimerWapper *timerWapper;
//定義
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];

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

運(yùn)行結(jié)果如下:

解決方式五:利用NSProxy虛基類的子類

NSProxy子類也是處理timer強(qiáng)引用最常用的方式组民。

  • 首先定義一個(gè)繼承自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子類對(duì)象棒仍,即timer持有NSProxy子類對(duì)象
//************解決timer強(qiáng)持有問題************
@property (nonatomic, strong) LGProxy       *proxy;


self.proxy = [CJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

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

這樣做的主要目的是將強(qiáng)引用轉(zhuǎn)移成消息轉(zhuǎn)發(fā)。虛基類只負(fù)責(zé)消息轉(zhuǎn)發(fā)臭胜,即使用NSProxy作為中間代理莫其、中間者;

  • vc釋放,導(dǎo)致了proxy的釋放

  • dealloc方法中耸三,timer進(jìn)行了釋放乱陡,所以runloop強(qiáng)引用也釋放了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仪壮,隨后出現(xiàn)的幾起案子憨颠,更是在濱河造成了極大的恐慌,老刑警劉巖积锅,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爽彤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缚陷,警方通過查閱死者的電腦和手機(jī)适篙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來箫爷,“玉大人嚷节,你說我怎么就攤上這事聂儒。” “怎么了硫痰?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵衩婚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我碍论,道長(zhǎng)谅猾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任鳍悠,我火速辦了婚禮税娜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘藏研。我一直安慰自己敬矩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布蠢挡。 她就那樣靜靜地躺著弧岳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪业踏。 梳的紋絲不亂的頭發(fā)上禽炬,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音勤家,去河邊找鬼腹尖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛伐脖,可吹牛的內(nèi)容都是我干的热幔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼讼庇,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼绎巨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蠕啄,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤场勤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后歼跟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體和媳,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年嘹承,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窗价。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡叹卷,死狀恐怖撼港,靈堂內(nèi)的尸體忽然破棺而出坪它,到底是詐尸還是另有隱情,我是刑警寧澤帝牡,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布往毡,位于F島的核電站,受9級(jí)特大地震影響靶溜,放射性物質(zhì)發(fā)生泄漏开瞭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一罩息、第九天 我趴在偏房一處隱蔽的房頂上張望嗤详。 院中可真熱鬧,春花似錦瓷炮、人聲如沸葱色。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)苍狰。三九已至,卻和暖如春烘绽,著一層夾襖步出監(jiān)牢的瞬間淋昭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工安接, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翔忽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓赫段,卻偏偏與公主長(zhǎng)得像呀打,于是被迫代替她去往敵國(guó)和親矢赁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糯笙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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