iOS之武功秘籍?: 內(nèi)存管理與NSRunLoop

iOS之武功秘籍 文章匯總

寫在前面

一個優(yōu)秀的App必然是對內(nèi)存"精打細(xì)算"的脉执,本文就來探索一下內(nèi)存管理中的一些門道與RunLoop的相關(guān)知識.

本節(jié)可能用到的秘籍Demo

一烙如、內(nèi)存布局

①. 五大區(qū)

接下來我從內(nèi)存中的低地址往高地址依次介紹五大區(qū):

  • 代碼段(.text)
    • 存放著程序代碼,直接加載到內(nèi)存中
  • 初始化區(qū)域(.data)
    • 存放著初始化的全局變量、靜態(tài)變量
    • 內(nèi)存地址:一般以 0x1 開頭
  • 未初始化區(qū)域(.bss)
    • bss段存放著未初始化的全局變量、靜態(tài)變量
    • 內(nèi)存地址:一般以 0x1 開頭
  • 堆區(qū)(heap)
    • 堆區(qū)存放著通過alloc分配的對象、block copy后的對象
    • 堆區(qū)速度比較慢
    • 內(nèi)存地址:一般以 0x6 開頭
  • 棧區(qū)(stack)
    • 棧區(qū)存儲著函數(shù)甘萧、方法以及局部變量
    • 棧區(qū)比較小,但是速度比較快
    • 內(nèi)存地址:一般以 0x7 開頭

在這里提一句關(guān)于函數(shù)在內(nèi)存中的分布:函數(shù)指針存在棧區(qū)梆掸,函數(shù)實現(xiàn)存在堆區(qū)

除了五大區(qū)之外扬卷,內(nèi)存中還有保留字段內(nèi)核區(qū)

  • 內(nèi)核區(qū):以4GB手機為例,系統(tǒng)將其中的3GB給了五大區(qū)+保留區(qū)酸钦,剩余的1GB給內(nèi)核區(qū)使用,它主要是系統(tǒng)用來進(jìn)行內(nèi)核處理操作的區(qū)域
  • 保留字段:保留一定的區(qū)域給保留字段怪得,進(jìn)行一些存儲或預(yù)留給系統(tǒng)處理nil

這里有個疑問,為什么五大區(qū)的最后內(nèi)存地址是從0x00400000開始的.其主要原因是0x00000000表示nil卑硫,不能直接用nil表示一個段徒恋,所以單獨給了一段內(nèi)存用于處理nil等情況.

以下的兩張圖,便于我們更好的理解內(nèi)存分布.



平時在使用App過程中,棧區(qū)就會向下增長欢伏,堆區(qū)就會向上增長.

接下來看看堆區(qū)和棧區(qū)中的一些內(nèi)容
  • 對于alloc創(chuàng)建的對象obj入挣,分別打印了obj的對象地址 和 obj對象的指針地址(可以參考前文的匯總圖)
    • obj對象地址是以0x6開頭,說明是存放在堆區(qū)
    • obj對象的指針地址是以0x7開頭颜懊,說明是存放在棧區(qū)

那么在堆區(qū)和棧區(qū)訪問對象的順序是怎樣的呢?

  • 堆區(qū)訪問對象的順序是先拿到棧區(qū)的指針财岔,再拿到指針指向的對象,才能獲取到對象的isa河爹、屬性方法等
  • 棧區(qū)訪問對象的順序是直接通過寄存器訪問到對象的內(nèi)存空間匠璧,因此訪問速度快

②. 內(nèi)存布局相關(guān)面試題

面試題1:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有咸这,是什么區(qū)別夷恍?

  • 有區(qū)別
  • 全局變量保存在內(nèi)存的全局存儲區(qū)(即bss+data段),占用靜態(tài)的存儲單元
  • 局部變量保存在棧區(qū)媳维,只有在所在函數(shù)被調(diào)用時才動態(tài)的為變量分配存儲單元
  • 兩者訪問的權(quán)限不一樣

面試題2:Block中可以修改全局變量酿雪,全局靜態(tài)變量,局部靜態(tài)變量侄刽,局部變量嗎指黎?

  • 可以修改全局變量,全局靜態(tài)變量州丹,因為全局變量 和 靜態(tài)全局變量是全局的醋安,作用域很廣,block可以訪問到
  • 可以修改局部靜態(tài)變量杂彭,不可以修改局部變量
    • 局部靜態(tài)變量(static修飾的) 和 局部變量,被block從外面捕獲吓揪,成為 __main_block_impl_0這個結(jié)構(gòu)體的成員變量
    • 局部變量是以值方式傳遞到block的構(gòu)造函數(shù)中的亲怠,只會捕獲block中會用到的變量,由于只捕獲了變量的值柠辞,并非內(nèi)存地址团秽,所以在block內(nèi)部不能改變局部變量的值
    • 局部靜態(tài)變量是以指針形式,被block捕獲的叭首,由于捕獲的是指針习勤,所以可以修改局部靜態(tài)變量的值
  • ARC環(huán)境下,一旦使用__block修飾并在block中修改放棒,就會觸發(fā)copy操作姻报,block就會從棧區(qū)copy到堆區(qū),此時的block堆區(qū)block
  • ARC模式下间螟,Block中引用id類型的數(shù)據(jù)吴旋,無論有沒有__block修飾,都會retain厢破,對于基礎(chǔ)數(shù)據(jù)類型荣瑟,沒有__block修飾就無法修改變量值;如果有__block修飾摩泪,也是在底層修改__Block_byref_a_0結(jié)構(gòu)體笆焰,將其內(nèi)部的forwarding指針指向copy后的地址,來達(dá)到值的修改

面試題3:關(guān)于全局靜態(tài)變量的誤區(qū)

  • 全局靜態(tài)變量是可變的
  • 全局靜態(tài)變量的值只針對文件而言见坑,不同文件的全局靜態(tài)變量的內(nèi)存地址是不一樣的,也就是無論別的文件怎么修改嚷掠,本文件使用時都拿原有值/本文件修改后的值

二、內(nèi)存管理方案

①. taggedPointer

①.1 taggedPointer初探

分別調(diào)用下面兩種方法荞驴,哪個會崩潰不皆?為什么?

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *nameStr;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.queue = dispatch_queue_create("com.tcj.cn", DISPATCH_QUEUE_CONCURRENT);

    [self taggedPointerDemo];
    [self testNormal];
}

- (void)taggedPointerDemo {
  
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"tcj"];  // alloc 堆 iOS優(yōu)化 - taggedpointer
             NSLog(@"%@",self.nameStr);
        });
    }
}

- (void)testNormal {
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"又一黑馬誕生12345"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

@end

經(jīng)過運行測試之后熊楼,會發(fā)現(xiàn)testNormal會崩潰霹娄,而taggedPointerDemo方法正常運行

首先來分析下為什么會崩潰的原因?其實是多線程setter鲫骗、getter操作造成的

  • 調(diào)用setter方法會objc_retain(newValue)+objc_release(oldValue)
  • 但是加上多線程就不一樣了——在某個時刻線程1對舊值進(jìn)行relese沒有relese完畢)犬耻,同時線程2也對舊值進(jìn)行relese操作,即同一時刻對同一片內(nèi)存空間釋放多次执泰,會造成野指針問題(訪問壞的地址)

但是為什么testNormal會崩潰枕磁,而taggedPointerDemo方法正常運行?

  • testNormal中的對象為__NSCFString類型,存儲在堆上
  • taggedPointerDemo中的對象為NSTaggedPointerString類型,存儲在常量區(qū).因為nameStralloc``分配時在堆區(qū)术吝,由于較小计济,所以經(jīng)過XcodeiOS的優(yōu)化晴楔,成了NSTaggedPointerString類型,存儲在常量區(qū)

其實之前在objc源碼的方法中有看到過類似的身影——objc_retainobjc_release的對象如果是isTaggedPointer類型就直接返回(不操作)

小對象的地址分析
NSString為例峭咒,對于NSString來說

  • 一般的NSString對象指針,都是string值 + 指針地址纪岁,兩者是分開的
  • 對于Tagged Pointer指針凑队,其 指針 + 值,都能在小對象中體現(xiàn).所以Tagged Pointer 既包含指針幔翰,也包含值

在之前的文章講類的加載時漩氨,其中的_read_images源碼有一個方法對小對象進(jìn)行了處理,即initializeTaggedPointerObfuscator方法,我們下面介紹

①.2 taggedPointer深入

在推出iPhone 5s(iPhone首個采用64位架構(gòu))的時候遗增,為了節(jié)省內(nèi)存和提高執(zhí)行效率叫惊,同時也提出了taggedPointer

底層也做了對objc_debug_taggedpointer_obfuscator進(jìn)行異或的操作(兩次異或同一個數(shù)相當(dāng)于編碼解碼 -- iOS10.14之后做的混淆操作)

我們可以在objc源碼(818.2版本)中通過objc_debug_taggedpointer_obfuscator查找taggedPointer的編碼和解碼,來查看底層是如何混淆處理的

通過實現(xiàn)做修,我們可以得知霍狰,在編碼和解碼部分,經(jīng)過了兩層異或饰及,其目的是得到小對象自己,例如以 1010 0001為例,假設(shè)mask為 0101 1000

    1010 0001 
   ^0101 1000 mask(編碼)
    1111 1001
   ^0101 1000 mask(解碼)
    1010 0001

所以在外界商膊,為了獲取小對象的真實地址军援,我們也可以通過類似的方法對taggedPointer進(jìn)行解碼.我們可以將解碼的源碼拷貝到外面,將NSString混淆部分進(jìn)行解碼屏箍,如下所示

觀察解碼后的小對象地址绘梦,其中的 62 表示 bASCII 碼,再以 NSNumber 為例赴魁,同樣可以看出卸奉,1就是我們實際的值

到這里,我們驗證了小對象指針地址中確實存儲了值尚粘,那么小對象地址高位其中的0xa择卦、0xb又是什么含義呢?

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025

需要去源碼中查看_objc_isTaggedPointer源碼郎嫁,主要是通過保留最高位的值(即64位的值)秉继,判斷是否等于_OBJC_TAG_MASK(即2 ^ 63), 來判斷是否是小對象

所以0xa0xb主要是用于判斷是否是小對象taggedpointer泽铛,即判斷條件尚辑,判斷第64位是否為1taggedpointer指針地址即表示指針地址,也表示值)

  • 0xa 轉(zhuǎn)換成二進(jìn)制為 1 01064位為1盔腔,63~61后三位 表示 tagType類型 - 2)杠茬,表示NSString類型
  • 0xb 轉(zhuǎn)換為二進(jìn)制為 1 01164位為1月褥,63~61后三位 表示 tagType類型 - 3),表示NSNumber類型瓢喉,這里需要注意一點宁赤,如果NSNumber的值是-1,其地址中的值是用補碼表示的

這里可以通過_objc_makeTaggedPointer方法的參數(shù)tag類型objc_tag_index_t進(jìn)入其枚舉栓票,其中 2 表示NSString决左,3 表示NSNumber

同理,我們可以定義一個NSDate對象走贪,來驗證其tagType是否為 6.通過打印結(jié)果佛猛,其地址高位是0xe,轉(zhuǎn)換為二進(jìn)制為1 110坠狡,排除64位的1继找,剩余的3位正好轉(zhuǎn)換為十進(jìn)制是6,符合上面的枚舉值

我們在來看看NSString的內(nèi)存管理
我們可以通過NSString初始化的兩種方式逃沿,來測試NSString的內(nèi)存管理

  • 通過 WithString + @""方式初始化
  • 通過 WithFormat方式初始化

從上面可以總結(jié)出婴渡,NSString的內(nèi)存管理主要分為3種

  • __NSCFConstantString字符串常量,是一種編譯時常量凯亮,retainCount值很大缩搅,對其操作,不會引起引用計數(shù)變化触幼,存儲在字符串常量區(qū)
  • __NSCFString:是在運行時創(chuàng)建的NSString子類硼瓣,創(chuàng)建后引用計數(shù)會加1,存儲在堆上
  • NSTaggedPointerString:標(biāo)簽指針置谦,是蘋果在64位環(huán)境下對NSString堂鲤、NSNumber等對象做的優(yōu)化.對于NSString對象來說
    • 當(dāng)字符串是由數(shù)字、英文字母組合且長度小于等于9時媒峡,會自動成為NSTaggedPointerString類型瘟栖,存儲在常量區(qū)
    • 當(dāng)有中文或者其他特殊符號時,會直接成為__NSCFString類型谅阿,存儲在堆區(qū)

①.3 taggedPointer總結(jié)

  • Tagged Pointer小對象類型(用于存儲NSNumber半哟、NSDate小NSString)签餐,小對象指針不再是簡單的地址寓涨,而是地址 + 值,即真正的值氯檐,所以戒良,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已.所以可以直接進(jìn)行讀取.優(yōu)點是占用空間小,節(jié)省內(nèi)存
  • Tagged Pointer 小對象, 不會進(jìn)入 retain 和 release冠摄,而是直接返回了糯崎,意味著不需要ARC進(jìn)行管理几缭,所以可以直接被系統(tǒng)自主的釋放和回收
  • Tagged Pointer的內(nèi)存并不存儲在堆中,而是在常量區(qū)中沃呢,也不需要mallocfree年栓,所以可以直接讀取,相比存儲在堆區(qū)的數(shù)據(jù)讀取薄霜,效率上快了3倍左右.創(chuàng)建的效率相比堆區(qū)快了近100倍左右
  • taggedPointer的內(nèi)存管理方案韵洋,比常規(guī)的內(nèi)存管理,要快很多
  • Tagged Pointer64位地址中黄锤,前4位代表類型后4位主要適用于系統(tǒng)做一些處理食拜,中間56位用于存儲值
  • 優(yōu)化內(nèi)存建議:對于NSString來說鸵熟,當(dāng)字符串較小時,建議直接通過@""初始化负甸,因為存儲在常量區(qū)流强,可以直接進(jìn)行讀取.會比WithFormat初始化方式更加快速

②. nonpointer_isa

nonpointer_isa在前面章節(jié)已經(jīng)有提到過了,這是蘋果優(yōu)化內(nèi)存的一種方案: isa是個8字節(jié)(64位)的指針呻待,僅用來isa指向比較浪費打月,所以isa中就摻雜了一些其他數(shù)據(jù)來節(jié)省內(nèi)存

③. SideTable

當(dāng)引用計數(shù)存儲到一定值時,并不會再存儲到Nonpointer_isa的位域的extra_rc中蚕捉,而是會存儲到 SideTables 散列表中

③.1 散列表為什么在內(nèi)存中有多張奏篙?最多能夠多少張?

  • 如果散列表只有一張表迫淹,意味著全局所有的對象都會存儲在一張表中秘通,操作任意一個對象,都會進(jìn)行開鎖解鎖(鎖是鎖整個表的讀寫).當(dāng)開鎖時,由于所有數(shù)據(jù)都在一張表敛熬,這意味著數(shù)據(jù)不安全
  • 如果每個對象都開一個表肺稀,會耗費性能,所以也不能有無數(shù)個表
  • 散列表的類型是SideTable应民,有如下定義
  • 通過查看 sidetable_unlock 方法定位 SideTables 话原,其內(nèi)部是通過SideTablesMapget 方法獲取. 而 SideTablesMap 是通過StripedMap<SideTable>定義的

從而進(jìn)入StripedMap的定義,從這里可以看出诲锹,同一時間繁仁,真機中散列表最多只能有8張

③.2 為什么在用散列表,而不用數(shù)組归园、鏈表改备?

  • 數(shù)組:特點在于查詢方便(即通過下標(biāo)訪問),增刪比較麻煩蔓倍,所以數(shù)組的特性是讀取快悬钳,存儲不方便
  • 鏈表:特點在于增刪方便盐捷,查詢慢(需要從頭節(jié)點開始遍歷查詢),所以鏈表的特性是存儲快默勾,讀取慢
  • 散列表的本質(zhì)就是一張哈希表碉渡,哈希表集合了數(shù)組和鏈表的長處增刪改查都比較方便母剥,例如拉鏈哈希表(在之前鎖的文章中滞诺,講過的tls的存儲結(jié)構(gòu)就是拉鏈形式的),是最常用的环疼,如下所示

三习霹、ARC&MRC

面試中常常會問到ARCMRC,其實這兩者在內(nèi)存管理中才是核心所在

① MRC(手動內(nèi)存管理)

  • MRC時代炫隶,系統(tǒng)是通過對對象的引用計數(shù)來判斷是否銷毀淋叶,有以下規(guī)則
    • 對象被創(chuàng)建時引用計數(shù)都為1
    • 當(dāng)對象被其他指針引用時,需要手動調(diào)用[objc retain]伪阶,使對象的引用計數(shù)+1
    • 當(dāng)指針變量不再使用對象時煞檩,需要手動調(diào)用[objc release]釋放對象,使對象的引用計數(shù)-1
    • 當(dāng)一個對象的引用計數(shù)為0時栅贴,系統(tǒng)就會銷毀這個對象
  • 所以斟湃,在MRC模式下,必須遵守:誰創(chuàng)建檐薯,誰釋放凝赛,誰引用誰管理

② ARC(自動內(nèi)存管理)

  • ARC模式是在WWDC2011iOS5引入的自動管理機制坛缕,即自動引用計數(shù).是編譯器的一種特性.其規(guī)則與MRC一致哄酝,區(qū)別在于
    • ARC禁止手動調(diào)用retain/release/retainCount/dealloc
    • 編譯器會在適當(dāng)?shù)奈恢貌迦?code>release和autorelease
    • ARC新加了weakstrong關(guān)鍵字
  • ARCLLVMRuntime配合的結(jié)果

③ alloc

之前已經(jīng)對alloc流程有了一個詳細(xì)的介紹

④ retain

retain 會在底層調(diào)用 objc_retain

  • objc_retain 先判斷是否為 isTaggedPointer祷膳,是就直接返回不需要處理陶衅,不是在調(diào)用 obj->retain()
  • objc_object::retain 通過 fastpath 大概率調(diào)用 rootRetain(),小概率通過消息發(fā)送調(diào)用對外提供的 SEL_retain
  • rootRetain調(diào)用rootRetain(false, false)
  • rootRetain內(nèi)部實現(xiàn)其實是個do-while循環(huán):
    • 先判斷是否為nonpointer_isa(小概率事件)不是的話,則直接操作SideTables散列表中的引用計數(shù)表直晨,此時的散列表并不是只有一張搀军,而是有很多張
      • 找到對應(yīng)的散列表進(jìn)行+=SIDE_TABLE_RC_ONE,其中SIDE_TABLE_RC_ONE是左移兩位找到引用計數(shù)表
    • 判斷是否正在釋放勇皇,如果正在釋放罩句,則執(zhí)行dealloc流程
    • 調(diào)用addc函數(shù)執(zhí)行extra_rc+1,即引用計數(shù)+1操作敛摘,并給一個引用計數(shù)的狀態(tài)標(biāo)識carry门烂,用于表示extra_rc是否滿了
      • isa中的第45位(RC_ONE在arm64中為45)extra_rc進(jìn)行操作處理
    • 如果carray的狀態(tài)表示extra_rc的引用計數(shù)滿了,此時需要操作散列表,即 將滿狀態(tài)的一半拿出來存到extra_rc屯远,另一半存在 散列表的rc_half.這么做的原因是因為如果都存儲在散列表蔓姚,每次對散列表操作都需要開解鎖,操作耗時慨丐,消耗性能大坡脐,這么對半分操作的目的在于提高性能
      • 這里為什么優(yōu)先考慮使用isa進(jìn)行引用計數(shù)存儲是因為引用計數(shù)存儲在isa的bits中

retain 總結(jié):

  • retain在底層首先會判斷是否是 Nonpointer isa,如果不是房揭,則直接操作散列表 進(jìn)行+1操作
  • 如果是Nonpointer isa备闲,還需要判斷是否正在釋放,如果正在釋放捅暴,則執(zhí)行dealloc流程恬砂,釋放弱引用表和引用計數(shù)表,最后free釋放對象內(nèi)存
  • 如果不是正在釋放蓬痒,則對Nonpointer isa進(jìn)行常規(guī)的引用計數(shù)+1.這里需要注意一點的是泻骤,extra_rc在真機上只有8位用于存儲引用計數(shù)的值,當(dāng)存儲滿了時乳幸,需要借助散列表用于存儲.需要將滿了的extra_rc對半分一半(即2^7)存儲在散列表中.另一半還是存儲在extra_rc中钧椰,用于常規(guī)的引用計數(shù)的+1或者-1操作粹断,然后再返回

⑤ release

releaseretain相似,會在底層調(diào)用objc_release

  • objc_release先判斷是否為isTaggedPointer嫡霞,是就直接返回不需要處理瓶埋,不是在調(diào)用obj->release()
  • objc_object::release通過fastpath大概率調(diào)用rootRelease(),小概率通過消息發(fā)送調(diào)用對外提供的SEL_release
  • rootRelease調(diào)用rootRelease(true, false)
  • rootRelease內(nèi)部實現(xiàn)也有個do-while循環(huán)
    • 先判斷是否為nonpointer_isa(小概率事件)不是的話則直接對散列表中的引用計數(shù)進(jìn)行-1操作
    • 如果是Nonpointer isa诊沪,則對extra_rc中的引用計數(shù)值進(jìn)行-1操作养筒,并存儲此時的extra_rc狀態(tài)到carry中
    • 如果此時的狀態(tài)carray為0,則走到underflow流程
      • 判斷散列表中是否存儲了一半的引用計數(shù)
      • 如果端姚,則從散列表中取出存儲的一半引用計數(shù)晕粪,進(jìn)行-1操作,然后存儲到extra_rc中
      • 如果此時extra_rc沒有值渐裸,散列表中也是空的巫湘,則直接進(jìn)行析構(gòu),即dealloc操作昏鹃,屬于自動觸發(fā)

所以尚氛,綜上所述,release的底層流程如下圖所示

⑥ retainCount

前面說了這么多引用計數(shù)洞渤,那么我們來看看retainCount和引用計數(shù)有什么關(guān)系呢阅嘶?來看一個問題:

alloc創(chuàng)建的對象的引用計數(shù)為多少?

上述代碼打印輸出1断箫,然而在alloc流程中并沒有看到任何與retainCount相關(guān)的內(nèi)容舱痘,這又是怎么一回事呢?接下來就來看看retainCount的底層實現(xiàn)

  • 進(jìn)入retainCount -> _objc_rootRetainCount -> rootRetainCount源碼降狠,其實現(xiàn)如下

在這里我們可以通過源碼斷點調(diào)試磷杏,來查看此時的extra_rc的值溜畅,結(jié)果如下:

當(dāng)來到953行斷點時,此時的extra_rc為0,而過到954行代碼,我們在來看extra_rc的值為多少.

此時的值卻為1了.

isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED)

以上代碼將bits里面的extra_rc進(jìn)行了+1操作.

答案:alloc創(chuàng)建的對象實際的引用計數(shù)為0,其引用計數(shù)打印結(jié)果為1极祸,是因為在底層rootRetainCount方法中慈格,引用計數(shù)默認(rèn)+1了,但是這里只有對引用計數(shù)的讀取操作遥金,是沒有寫入操作的浴捆,簡單來說就是:為了防止alloc創(chuàng)建的對象被釋放(引用計數(shù)為0會被釋放),所以在編譯階段稿械,程序底層默認(rèn)進(jìn)行了+1操作.(新版objc源碼)

  • alloc創(chuàng)建的對象沒有retain和release
  • alloc創(chuàng)建對象的引用計數(shù)為0选泻,會在編譯時期,程序默認(rèn)加1美莫,所以讀取引用計數(shù)時為1

⑦ autorealese

將在自動釋放池章節(jié)講到.

⑧ dealloc

retainrelease的底層實現(xiàn)中页眯,都提及了dealloc析構(gòu)函數(shù),下面來分析dealloc的底層的實現(xiàn)

  • dealloc在底層會調(diào)用_objc_rootDealloc
  • _objc_rootDealloc調(diào)用rootDealloc
  • rootDealloc方法中
    • 判斷是否為isTaggedPointer厢呵,是的話直接返回窝撵,不是的話繼續(xù)往下走
    • 判斷isa標(biāo)識位中是否有弱引用關(guān)聯(lián)對象襟铭、c++析構(gòu)函數(shù)碌奉、額外的散列表,有的話調(diào)用object_dispose寒砖,否則直接free
  • object_dispose
    • 先判空處理
    • 接著調(diào)用objc_destructInstance(核心部分)
    • 最后再free釋放對象
  • objc_destructInstance
    • 判斷是否有c++析構(gòu)函數(shù)和關(guān)聯(lián)對象赐劣,有的話分別調(diào)用object_cxxDestruct_object_remove_assocations進(jìn)行處理
    • 然后再調(diào)用clearDeallocating
  • clearDeallocating
    • 判斷是否是nonpointer哩都,是的話調(diào)用sidetable_clearDeallocating清空散列表
    • 判斷是否弱引用和額外的引用計數(shù)表has_sidetable_rc魁兼,是的話調(diào)用clearDeallocating_slow進(jìn)行弱引用表和引用計數(shù)表的處理

所以綜上所述,dealloc的流程可以總結(jié)為:

  • 1:根據(jù)當(dāng)前對象的狀態(tài)是否直接調(diào)?free()釋放
  • 2:是否存在C++的析構(gòu)函數(shù)、移除這個對象的關(guān)聯(lián)屬性
  • 3:將指向該對象的弱引?指針置為nil
  • 4:從弱引?表中擦除對該對象的引?計數(shù)
    最后附上一張dealloc流程圖

因此到目前為止漠嵌,從最開始的alloc -> retain -> release -> dealloc就全部串聯(lián)起來了.

四璃赡、弱引用

①. weak原理

筆者在之前的[iOS之武功秘籍⑩: OC底層題目分析]中已經(jīng)講過了.


②. NSTimer中的循環(huán)引用

眾所周知使用NSTimer容易出現(xiàn)循環(huán)引用,那么我們就來分析并解決一下

假設(shè)此時有A献雅、B兩個界面,在B界面中有如下定時器代碼.

代碼運行起來所發(fā)生的問題就是 B界面 popA界面 時不會觸發(fā) B 界面dealloc函數(shù).主要原因是B界面沒有釋放碉考,即沒有執(zhí)行dealloc方法,導(dǎo)致timer也無法停止和釋放

前面我們已經(jīng)看到了release在引用計數(shù)為0時會調(diào)用dealloc消息發(fā)送挺身,此時沒有觸發(fā)dealloc函數(shù)必然是出現(xiàn)了循環(huán)引用侯谁,那么循環(huán)引用出現(xiàn)在哪個環(huán)節(jié)?其實是NSTimerAPI是被強持有的,直到Timer invalidated.

即此時timer持有self,self也持有timer墙贱,構(gòu)成了循環(huán)引用

那么能不能像block一樣使用弱引用來解決循環(huán)引用呢热芹?答案是不能的!

此時他們之間的持有關(guān)系如下:


之前在Block篇章說的是使用弱引用__weak typeof(self) weakSelf = self可以解決循環(huán)引用; 不處理引用計數(shù)惨撇,使用弱引用表管理伊脓,怎么在這里就不好使了呢?

到這我又有兩個問題?

  • weakSelf會對引用計數(shù)進(jìn)行+1操作嗎魁衙?
  • weakSelfself 的指針地址相同嗎报腔,是指向同一片內(nèi)存嗎?

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

運行后發(fā)現(xiàn)前后self的引用計數(shù)都是8.也就是 weakSelf沒有對內(nèi)存進(jìn)行+1操作

繼續(xù)打印weakSelfself對象纯蛾,以及他們的指針地址:

從打印結(jié)果可以看出 weakSelfself 指向的都是 TCJTimerViewController對象,但是weakSelfself指針并不相同——兩者并不是一個東東纵隔,只是指向同一個TCJTimerViewController對象.

通過block底層原理的方法 _Block_object_assign 可知翻诉,block捕獲的是 對象的指針地址

block持有的是weakSelf的指針地址;timer持有的是weakSelf的指針指向的對象捌刮,這里間接持有了self碰煌,所以仍然存在循環(huán)引用導(dǎo)致釋放不掉.

③. 解決NSTimer的循環(huán)引用

解決思路 : 我們需要打破這一層強持有 - self

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

  • 既然dealloc不能來,就在dealloc函數(shù)調(diào)用前解決掉這層強引用
  • 可以在viewWillDisappear绅作、viewDidDisappear中處理NSTimer芦圾,但這樣處理效果并不好,因為跳轉(zhuǎn)到下一頁定時器也會停止工作棚蓄,與業(yè)務(wù)不符
  • 使用didMoveToParentViewController可以很好地解決這層強引用.這個方法是用于當(dāng)一個視圖控制器中添加或者移除viewController后堕扶,必須調(diào)用的方法.目的是為了告訴iOS碍脏,已經(jīng)完成添加/刪除子控制器的操作.
  • B界面中重寫didMoveToParentViewController方法

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

  • 使用其他全局變量典尾,此時timer持有全局變量役拴,self也持有全局變量,只要頁面pop钾埂,self因為沒有被持有就能正常走dealloc河闰,在dealloc中再去處理timer
  • 此時的持有鏈分別是runloop->timer->target->timerself->target褥紫、self->timer
#ifdef DEBUG
#define CJNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define CJNSLog(format, ...);
#endif

#import "TCJTimerViewController.h"
#import <objc/runtime.h>

static int num = 0;

@interface TCJTimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) id  target;
@end

@implementation TCJTimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
     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];
}

void fireHomeObjc(id obj){
    CJNSLog(@"%s -- %@",__func__,obj);
}

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

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

③.3 思路三:自定義封裝timer(使用包裝者)

  • 類似于方案二姜性,但是使用更便捷
  • 如果傳入的響應(yīng)者target能響應(yīng)傳入的響應(yīng)事件selector,就使用runtime動態(tài)添加方法并開啟計時器
  • fireWapper中如果有wrapper.target髓考,就讓wrapper.target(外界響應(yīng)者)調(diào)用wrapper.aSelector(外界響應(yīng)事件)
  • fireWapper中沒有了wrapper.target部念,意味著響應(yīng)者釋放了(無法響應(yīng)了),此時定時器也就可以休息了(停止并釋放)
  • 持有鏈分別是runloop->timer->TCJTimerWrappervc->TCJTimerWrapper-->vc
//*********** .h文件 ***********
@interface TCJTimerWapper : NSObject

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

@end

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

@interface TCJTimerWapper ()

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

@end

@implementation TCJTimerWapper

- (instancetype)cj_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(TCJTimerWapper *wapper){
    //判斷target是否存在
    if (wapper.target) {
        //如果存在則需要讓vc知道,即向傳入的target發(fā)送selector消息乌询,并將此時的timer參數(shù)也一并傳入榜贴,所以vc就可以得知`fireHome`方法,就這事這種方式定時器方法能夠執(zhí)行的原因
        //objc_msgSend發(fā)送消息妹田,執(zhí)行定時器方法
        void (*cj_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         cj_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)cj_invalidate{
    [self.timer invalidate];
    self.timer = nil;
}

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

@end

#ifdef DEBUG
#define CJNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define CJNSLog(format, ...);
#endif

#import "TCJTimerViewController.h"
#import "TCJTimerWapper.h"

static int num = 0;

@interface TCJTimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TCJTimerWapper *timerWapper;
@end

@implementation TCJTimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
     self.timerWapper = [[TCJTimerWapper alloc] cj_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

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

- (void)dealloc{
    [self.timerWapper cj_invalidate];
    CJNSLog(@"%s",__func__);
}

這種方式看起來比較繁瑣,步驟很多沮趣,而且針對timerWapper屯烦,需要不斷的添加method,需要進(jìn)行一系列的處理.

③.4 思路四:利用NSProxy虛基類的子類——NSProxy有著NSObject同等的地位房铭,多用于消息轉(zhuǎn)發(fā)

  • 使用NSProxy打破NSTimer的對vc的強持有驻龟,但是強持有依然存在,需要手動關(guān)閉定時器
  • 持有鏈分別是runloop->timer->TCJProxy->timer缸匪、vc->TCJProxy-->vc
//************TCJProxy.h文件************
@interface TCJProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

//************TCJProxy.m文件************
@interface TCJProxy()
@property (nonatomic, weak) id object;
@end

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

//************TCJTimerViewController.m文件************
#ifdef DEBUG
#define CJNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define CJNSLog(format, ...);
#endif

#import "TCJTimerViewController.h"
#import "TCJProxy.h"

static int num = 0;

@interface TCJTimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TCJProxy *proxy;
@end

@implementation TCJTimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.proxy = [TCJProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}

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

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

思路一較為簡便翁狐,思路二合理使用中介者但是很拉胯,思路三適合裝逼凌蔬,思路四更適合大型項目(定時器用的較多) 詳細(xì)代碼

五露懒、AutoReleasePool 自動釋放池

自動釋放池OC中的一種內(nèi)存自動回收機制,在MRC中可以用AutoReleasePool來延遲內(nèi)存的釋放砂心,在ARC中可以用AutoReleasePool將對象添加到最近的自動釋放池懈词,不會立即釋放,會等到runloop休眠或者超出autoreleasepool作用域{}之后才會被釋放.其機制可以通過下圖來表示

  1. 從程序啟動到加載完成辩诞,主線程對應(yīng)的runloop會處于休眠狀態(tài)坎弯,等待用戶交互喚醒runloop
  2. 用戶的每一次交互都會啟動一次runloop,用于處理用戶的所有點擊译暂、觸摸事件
  3. runloop監(jiān)聽交互事件后抠忘,就會創(chuàng)建自動釋放池,并將所有延遲釋放對象添加到自動釋放池中
  4. 在一次完整的runloop結(jié)束之前外永,會向自動釋放池中的所有對象發(fā)送release消息崎脉,然后銷毀自動釋放池

① Clang分析 autoreleasepool結(jié)構(gòu)

通過clang命令對空白的main.m文件輸出一份main.cpp文件來查看@autoreleasepool底層結(jié)構(gòu)

  • clang命令為:xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

  • 轉(zhuǎn)成C++代碼如下

  • 通過上圖我們知道@autoreleasepool被轉(zhuǎn)化成__AtAutoreleasePool __autoreleasepool,這是個結(jié)構(gòu)體.__AtAutoreleasePool結(jié)構(gòu)體定義如下:

通過上圖我們可以知道以下幾點:

  • __AtAutoreleasePool是一個結(jié)構(gòu)體伯顶,有構(gòu)造函數(shù) + 析構(gòu)函數(shù)囚灼,結(jié)構(gòu)體定義的對象作用域結(jié)束后呛踊,會自動調(diào)用析構(gòu)函數(shù)
  • 其中{}作用域,優(yōu)點是結(jié)構(gòu)清晰啦撮,可讀性強谭网,可以及時創(chuàng)建銷毀

關(guān)于涉及的構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用時機,可以通過下面一個案例來驗證

從運行結(jié)果可以得出赃春,在TCJTest創(chuàng)建對象時愉择,會自動調(diào)用構(gòu)造函數(shù),在出了{(lán)}作用域后织中,會自動調(diào)用析構(gòu)函數(shù).

② 匯編分析 autoreleasepool結(jié)構(gòu)

main代碼部分加斷點锥涕,運行程序,并開啟匯編調(diào)試:

通過調(diào)試結(jié)果發(fā)現(xiàn)狭吼,和我們clang分析的結(jié)果是一樣的.

③ objc源碼分析 autoreleasepool

objc源碼中有一段對AutoreleasePool的注釋.

從中可以得出幾點:

  • 1.自動釋放池是一個關(guān)于指針的棧結(jié)構(gòu)
  • 2.其中的指針指向釋放的對象或者pool_boundary哨兵(現(xiàn)在經(jīng)常被稱為邊界
  • 3.自動釋放池是一個頁的結(jié)構(gòu)(虛擬內(nèi)存中提及過)层坠,而且這個頁是一個雙向鏈表(表示有父節(jié)點子節(jié)點,在類中提及過刁笙,即類的繼承鏈)
  • 4.自動釋放池線程是有關(guān)系

通過上面對自動釋放池的說明破花,我們知道我們研究的幾個方向:

  • 1.自動釋放池什么時候創(chuàng)建?
  • 2.對象是如何加入自動釋放池的疲吸?
  • 3.哪些對象才會加入自動釋放池座每?

帶著這些問題,我們出發(fā)來探索自動釋放池的底層原理

③.1 AutoreleasePoolPage分析

從最初的clang或者匯編分析我們了解了自動釋放池其底層調(diào)用的objc_autoreleasePoolPushobjc_autoreleasePoolPop這兩個方法摘悴,其源碼實現(xiàn)如下[圖片上傳失敗...(image-d024be-1615298790653)]
從源碼中我們可以發(fā)現(xiàn)峭梳,都是調(diào)用AutoreleasePoolPagepushpop實現(xiàn),以下是其定義蹂喻,從定義中可以看出葱椭,自動釋放池是一個頁,同時也是一個對象口四,并且AutoreleasePoolPage是繼承于AutoreleasePoolPageData的.

從上面可以做出以下判斷:

  • 1.自動釋放池是一個孵运,同時也是一個對象,這個頁的大小是4096字節(jié)
  • 2.從其定義中發(fā)現(xiàn)窃祝,AutoreleasePoolPage是繼承自AutoreleasePoolPageData,且該類的屬性也是來自父類掐松,以下是AutoreleasePoolPageData的定義

可以發(fā)現(xiàn):

  • 其中有AutoreleasePoolPage對象踱侣,所以有以下一個關(guān)系鏈AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage粪小,從這里可以說明自動釋放池除了是一個頁,還是一個雙向鏈表結(jié)構(gòu)
  • AutoreleasePoolPageData結(jié)構(gòu)體的內(nèi)存大小為56字節(jié)
    • 屬性magic的類型是magic_t結(jié)構(gòu)體抡句,所占內(nèi)存大小為m[4]其內(nèi)存(即4*4=16字節(jié)
    • 屬性next(指針)探膊、thread(對象)parent(對象)待榔、child(對象)均占8字節(jié)(即4*8=32字節(jié)
    • 屬性depth逞壁、hiwat類型為uint32_t流济,實際類型是unsigned int類型,均占4字節(jié)(即2*4=8字節(jié)
      通過上面可以知道一個空的AutoreleasePoolPage的結(jié)構(gòu)如下:
objc_autoreleasePoolPush 源碼分析

進(jìn)入push的源碼實現(xiàn):

有以下邏輯:

  • 首先進(jìn)行判斷是否存在pool
  • 如果沒有腌闯,則通過autoreleaseNewPage方法創(chuàng)建
  • 如果有绳瘟,則通過autoreleaseFast壓棧哨兵對象
autoreleaseNewPage創(chuàng)建新頁

先來看下autoreleaseNewPage創(chuàng)建新頁的實現(xiàn)過程

通過上面的代碼實現(xiàn)(autoreleaseFullPage后面會重點分析),我們可得到以下結(jié)論

  • 1.獲取當(dāng)前操作頁,
  • 2.如果當(dāng)前操作頁存在,則通過autoreleaseFullPage方法進(jìn)行壓棧對象
  • 3.如果當(dāng)前操作頁不存在姿骏,則通過autoreleaseNoPage方法創(chuàng)建頁
    • autoreleaseNoPage方法中可知當(dāng)前線程的自動釋放池是通過AutoreleasePoolPage創(chuàng)建
    • AutoreleasePoolPage構(gòu)造方法通過實現(xiàn)父類AutoreleasePoolPageData的初始化方法實現(xiàn)的.
AutoreleasePoolPage構(gòu)造方法

上面說了當(dāng)前線程的自動釋放池是通過AutoreleasePoolPage創(chuàng)建糖声,看下AutoreleasePoolPage構(gòu)造方法:

其中AutoreleasePoolPageData方法傳入的參數(shù)含義為:

  • begin()表示壓棧的位置(即下一個要釋放對象的壓棧地址).可以通過源碼調(diào)試begin,發(fā)現(xiàn)其具體實現(xiàn)等于頁首地址+56分瘦,其中的56就是結(jié)構(gòu)體AutoreleasePoolPageData的內(nèi)存大小.
    • 由于在ARC模式下蘸泻,是無法手動調(diào)用autorelease,所以將Demo切換至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting設(shè)置為NO

這個指針地址為什么要加上56呢嘲玫?這個56是哪里來的呢悦施?其實就是AutoreleasePoolPage中的固有屬性

分析:AutoreleasePoolPageData中的指針和對象都占8字節(jié)uint4字節(jié)去团,只有magic_t未知(因為不是個指針抡诞,所以需要看具體類型);magic_t是個指針土陪,由于靜態(tài)變量的存儲區(qū)域在全局段沐绒,所以magic_t占用4*4=16字節(jié),即AutoreleasePoolPageData結(jié)構(gòu)體的內(nèi)存大小為56字節(jié).

  • objc_thread_self()是表示當(dāng)前線程,而當(dāng)前線程是通過tls獲取

  • newParent表示父節(jié)點

  • 后續(xù)兩個參數(shù)是通過父節(jié)點的深度旺坠、最大入棧個數(shù)計算的depth以及hiwat

查看自動釋放池內(nèi)存結(jié)構(gòu)

接著我們使用_objc_autoreleasePoolPrint函數(shù)來打印一下自動釋放池的相關(guān)信息(記得切換為MRC模式調(diào)試,這里前面我們已經(jīng)切換了)

通過運行結(jié)果如下乔遮,我們發(fā)現(xiàn)release是6個,但是我們壓棧的對象其實只有5個取刃,其中的POOL表示哨兵對象蹋肮,即邊界,其目的是為了防止越界,我們再看下打印地址璧疗,發(fā)現(xiàn)頁的首地址(PAGE)和哨兵對象(POOL)相差0x38坯辩,轉(zhuǎn)成10進(jìn)制正好是56.也就是AutoreleasePoolPage自己本身的內(nèi)存大小.

那么是否可以無限往AutoreleasePool中添加對象呢?答案是不能崩侠!

將循環(huán)次數(shù)i的上限改為505漆魔,其內(nèi)存結(jié)構(gòu)如下,發(fā)現(xiàn)第一頁滿了却音,存儲了504個釋放的對象改抡,第二頁只存儲了一個

在將循環(huán)次數(shù)i據(jù)改為505+506,來驗證第二頁是否也是存儲504個對象?

通過運行發(fā)現(xiàn)系瓢,第一頁存儲504阿纤,第二頁存儲505,第三頁存儲2個.

通過上述測試夷陋,我們可以得出以下結(jié)論:

  • 第一頁可以存放504個對象,且只有第一頁有哨兵對象,當(dāng)一頁壓棧滿了博烂,就會開辟新的一頁
  • 第二頁開始吕粹,最多可以存放505個對象
  • 一頁的大小等于 505 * 8 = 4040

這個結(jié)論我們之前講AutoreleasePoolPage中的SIZE的時候就說了,一頁的大小是4096字節(jié),而在其構(gòu)造函數(shù)中對象的壓棧位置,是從首地址+56字節(jié)開始的,所以可以一頁中實際可以存儲4096-56 = 4040字節(jié)榛搔,轉(zhuǎn)換成對象是 4040 / 8 = 505個,即一頁最多可以存儲505個對象,其中第一頁有哨兵對象(由于自動釋放池在初始化時會POOL_BOUNDARY哨兵對象push到棧頂东揣,所以第一頁只能存放504個對象践惑,接下來每一頁都能存放505個對象)只能存儲504個.其結(jié)構(gòu)圖示如下

通過上面的結(jié)論,我有一個疑問:哨兵對象在一個自動釋放池有幾個?

  • 一個自動釋放池只有一個哨兵對象嘶卧,且哨兵在第一頁
  • 第一頁最多可以存504個對象尔觉,第二頁開始最多存505

③.2 哨兵對象 -- POOL_BOUNDARY

哨兵對象本質(zhì)上是個nil,它的作用主要在調(diào)用objc_autoreleasePoolPop時體現(xiàn):

  • 根據(jù)傳入的哨兵對象地址找到哨兵對象所在的page
  • 在當(dāng)前page中芥吟,將晚于哨兵對象插入的所有autorelese對象都發(fā)送一次release消息侦铜,并移動next指針到正確位置
  • 從最新加入的對象一直向前清理,可以向前跨越若干個page钟鸵,直到哨兵對象所在的page

③.3 壓棧對象autoreleaseFast

進(jìn)入autoreleaseFast源碼:

主要有以下幾步:

  • 1.獲取當(dāng)前操作頁钉稍,并判斷頁是否存在以及是否滿了
  • 2.如果頁存在且未滿棺耍,則通過add方法壓棧對象
  • 3.如果頁存在贡未,且滿了,則通過autoreleaseFullPage方法安排新的頁面
  • 4.如果頁不存在蒙袍,則通過autoreleaseNoPage方法創(chuàng)建新頁
autoreleaseFullPage方法

其源碼為:

這個方法主要是用于判斷當(dāng)前頁是否已經(jīng)存儲滿了俊卤,如果當(dāng)前頁已經(jīng)滿了,通過do-while循環(huán)查找子節(jié)點對應(yīng)的頁害幅,如果不存在開辟新的AutoreleasePoolPage并設(shè)為HotPage,然后壓棧對象.從上面AutoreleasePoolPage初始化方法中可以看出消恍,主要是通過操作child對象,將當(dāng)前頁的child指向新建頁面以现,由此可以得出是通過雙向鏈表連接.

add方法

查看源碼:

這個方法主要是添加釋放對象狠怨,其底層是實現(xiàn)是通過next指針存儲釋放對象,并將next指針遞增邑遏,表示下一個釋放對象存儲的位置.從這里可以看出是通過棧結(jié)構(gòu)存儲

③.4 autorelease 底層分析

demo中佣赖,我們通過autorelease方法,在MRC模式下无宿,將對象壓棧到自動釋放池茵汰,下面來分析其底層實現(xiàn):

  • 查看autorelease方法源碼
  • 進(jìn)入對象的autorelease實現(xiàn)

從這里看出枢里,無論是壓棧哨兵對象孽鸡,還是普通對象蹂午,都會來到autoreleaseFast方法,只是區(qū)別標(biāo)識不同而以.

③.5 objc_autoreleasePoolPop 源碼分析&出棧

objc_autoreleasePoolPop 源碼分析

objc_autoreleasePoolPop方法中有個參數(shù)彬碱,在clang分析時豆胸,發(fā)現(xiàn)傳入的參數(shù)是push壓棧后返回的哨兵對象,即ctxt巷疼,其目的是避免出椡砗混亂防止將別的對象出棧嚼沿,其內(nèi)部是調(diào)用AutoreleasePoolPagepop方法估盘,我們看下pop源碼:

pop源碼實現(xiàn),主要由以下幾步:

  • 1.空頁面的處理骡尽,并根據(jù)token獲取page
  • 2.容錯處理
  • 3.通過popPage出棧頁
出棧 -- popPage

查看popPage源碼:

進(jìn)入popPage源碼遣妥,其中傳入的allowDebugfalse,則通過releaseUntil出棧當(dāng)前頁stop位置之前的所有對象攀细,即向棧中的對象發(fā)送release消息箫踩,直到遇到傳入的哨兵對象.

releaseUntil方法

看源碼我們可以知道:

  • releaseUntil的實現(xiàn),主要是通過循環(huán)遍歷谭贪,判斷對象是否等于stop境钟,其目的是釋放stop之前的所有的對象
  • 首先通過獲取page的next釋放對象(即page的最后一個對象),并對next進(jìn)行遞減俭识,獲取上一個對象
  • 判斷是否是哨兵對象慨削,如果不是則自動調(diào)用objc_release釋放
kill方法

通過kill實現(xiàn)我們知道,主要是銷毀當(dāng)前頁套媚,將當(dāng)前頁賦值為父節(jié)點頁理盆,并將父節(jié)點頁的child對象指針置為nil

③.6 總結(jié)

  • 1.autoreleasepool其本質(zhì)是一個結(jié)構(gòu)體對象,一個自動釋放池對象就是凑阶,是棧結(jié)構(gòu)存儲猿规,符合先進(jìn)后出的原則
  • 2.頁的棧底是一個56字節(jié)大小的空占位符一頁總大小為4096字節(jié)
  • 3.只有第一頁有哨兵對象宙橱,最多存儲504個對象姨俩,從第二頁開始最多存儲505個對象
  • 4.autoreleasepool加入要釋放的對象時,底層調(diào)用的是objc_autoreleasePoolPush方法(push操作)
    • 當(dāng)沒有pool师郑,即只有空占位符(存儲在tls中)時环葵,則創(chuàng)建頁,壓棧哨兵對象
    • 在頁中壓棧普通對象主要是通過next指針遞增進(jìn)行的
    • 當(dāng)頁滿了時宝冕,需要設(shè)置頁的child對象為新建頁
    • objc_autoreleasePush的整體底層的流程圖如下
  • 5.autoreleasepool調(diào)用析構(gòu)函數(shù)釋放時张遭,內(nèi)部的實現(xiàn)是調(diào)用objc_autoreleasePoolPop方法(pop操作)
    • 在頁中出棧普通對象主要是通過next指針遞減進(jìn)行的
    • 當(dāng)頁空了時,需要賦值頁的parent對象為當(dāng)前頁
    • objc_autoreleasePoolPop出棧的流程圖如下

④ 提出疑問

④.1 臨時變量什么時候釋放?

  • 1.如果在正常情況下地梨,一般是超出其作用域就會立即釋放
  • 2.如果將臨時變量加入了自動釋放池菊卷,會延遲釋放缔恳,即在runloop休眠或者autoreleasepool作用域之后釋放

④.2 自動釋放池原理 即AutoreleasePool原理

  • 1.自動釋放池本質(zhì)是一個AutoreleasePoolPage結(jié)構(gòu)體對象,是一個棧結(jié)構(gòu)存儲的頁洁闰,每一個AutoreleasePoolPage都是以雙向鏈表的形式連接
  • 2.自動釋放池壓棧出棧主要是通過結(jié)構(gòu)體的構(gòu)造函數(shù)析構(gòu)函數(shù)調(diào)用底層的objc_autoreleasePoolPushobjc_autoreleasePoolPop歉甚,實際上是調(diào)用AutoreleasePoolPagepushpop兩個方法
  • 3.每次調(diào)用push操作其實就是創(chuàng)建一個新的AutoreleasePoolPage,而AutoreleasePoolPage的具體操作就是插入一個POOL_BOUNDARY扑眉,并返回插入POOL_BOUNDARY的內(nèi)存地址.而push內(nèi)部調(diào)用autoreleaseFast方法處理纸泄,主要有以下三種情況
    • 當(dāng)page存在,且不滿時腰素,調(diào)用add方法對象添加至page的next指針處聘裁,并next遞增
    • 當(dāng)page存在,且已滿時弓千,調(diào)用autoreleaseFullPage初始化一個新的page咧虎,然后調(diào)用add方法對象添加至page棧中
    • 當(dāng)page不存在時,調(diào)用autoreleaseNoPage創(chuàng)建一個hotPage计呈,然后調(diào)用add方法對象添加至page棧中
  • 4.當(dāng)執(zhí)行pop操作時砰诵,會傳入一個值,這個值就是push操作的返回值捌显,即POOL_BOUNDARY的內(nèi)存地址token.所以pop內(nèi)部的實現(xiàn)就是根據(jù)token找到哨兵對象所處的page中茁彭,然后使用 objc_release釋放token之前的對象,并把next指針到正確位置

④.3 自動釋放池能否嵌套使用扶歪?

  • 1.可以嵌套使用理肺,其目的是可以控制應(yīng)用程序的內(nèi)存峰值,使其不要太高
  • 2.可以嵌套的原因是因為自動釋放池是以棧為節(jié)點善镰,通過雙向鏈表的形式連接的妹萨,且是和線程一一對應(yīng)的
  • 3.自動釋放池的多層嵌套其實就是不停的push哨兵對象,在pop時炫欺,會先釋放里面的乎完,在釋放外面的

④.4 哪些對象可以加入AutoreleasePool?alloc創(chuàng)建可以嗎品洛?

  • 1.在MRC下使用new树姨、alloc、copy關(guān)鍵字生成的對象和retain了的對象需要手動釋放桥状,不會被添加到自動釋放池中
  • 2.在MRC下設(shè)置為autorelease的對象不需要手動釋放帽揪,會直接進(jìn)入自動釋放池
  • 3.所有autorelease的對象,在出了作用域之后辅斟,會被自動添加到最近創(chuàng)建的自動釋放池
  • 4.在ARC下只需要關(guān)注引用計數(shù)转晰,因為創(chuàng)建都是在主線程進(jìn)行的,系統(tǒng)會自動為主線程創(chuàng)建AutoreleasePool,所以創(chuàng)建的對象會自動放入自動釋放池

④.5 AutoreleasePool的釋放時機是什么時候查邢?

  • 1.App啟動后蔗崎,蘋果在主線程RunLoop里注冊了兩個Observer,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()
  • 2.第一個Observer監(jiān)視的事件是Entry(即將進(jìn)入 Loop)侠坎,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池.其order是-2147483647蚁趁,優(yōu)先級最高裙盾,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前
  • 3.第二個Observer監(jiān)視了兩個事件:BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時調(diào)用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池实胸;Exit(即 將退出Loop)時調(diào)用_objc_autoreleasePoolPop()來釋放自動釋放池.這個Observerorder是 2147483647,優(yōu)先級最低番官,保證其釋放池子發(fā)生在其他所有回調(diào)之后

④.6 thread和AutoreleasePool的關(guān)系

每個線程都有與之關(guān)聯(lián)的自動釋放池堆棧結(jié)構(gòu)庐完,新的pool創(chuàng)建時會被壓棧到棧頂pool銷毀時徘熔,會被出棧门躯,對于當(dāng)前線程來說,釋放對象會被壓棧到棧頂酷师,線程停止時讶凉,會自動釋放與之關(guān)聯(lián)的自動釋放池.

④.7 RunLoop和AutoreleasePool的關(guān)系

  • 1.主程序的RunLoop在每次事件循環(huán)之前,會自動創(chuàng)建一個autoreleasePool
  • 2.并且會在事件循環(huán)結(jié)束時山孔,執(zhí)行drain操作懂讯,釋放其中的對象

六、NSRunLoop

① RunLoop介紹

RunLoop事件接收分發(fā)機制的一個實現(xiàn)台颠,是線程相關(guān)的基礎(chǔ)框架的一部分褐望,一個RunLoop就是一個事件處理的循環(huán),用來不停的調(diào)度工作以及處理輸入事件.

RunLoop本質(zhì)是一個do-while循環(huán)串前,沒事做就休息瘫里,來活了就干活.與普通的while循環(huán)是有區(qū)別的,普通的while循環(huán)會導(dǎo)致CPU進(jìn)入忙等待狀態(tài)荡碾,即一直消耗cpu谨读,而RunLoop則不會,RunLoop是一種閑等待坛吁,即RunLoop具備休眠功能.

RunLoop的作用

  • 保持程序的持續(xù)運行
  • 處理App中的各種事件(觸摸漆腌、定時器performSelector
  • 節(jié)省cpu資源阶冈,提供程序的性能闷尿,該做事就做事,該休息就休息

RunLoop源碼的下載地址女坑,在其中找到最新版下載即可

② RunLoop和線程的關(guān)系

②.1 獲取RunLoop

一般在日常開發(fā)中填具,對于RunLoop的獲取主要有以下兩種方式

②.2 CFRunLoopGetMain源碼

②.3 _CFRunLoopGet0源碼

通過上面可以知道,Runloop只有兩種,一種是主線程的劳景,一個是其它線程的.即Runloop和線程一一對應(yīng)的.

③ RunLoop的創(chuàng)建

通過上面的_CFRunLoopGet0可以知道Runloop是通過__CFRunLoopCreate創(chuàng)建(系統(tǒng)創(chuàng)建誉简,開發(fā)者自己是無法創(chuàng)建的).我們查看下__CFRunLoopCreate源碼:

我們發(fā)現(xiàn)__CFRunLoopCreate主要是對runloop屬性的賦值操作.我們繼續(xù)看CFRunLoopRef的源碼

可以得出以下結(jié)論:

  • 1.根據(jù)定義得知,其實RunLoop也是一個對象.是__CFRunLoop結(jié)構(gòu)體的指針類型
  • 2.一個RunLoop依賴于多個Mode盟广,意味著一個RunLoop需要處理多個事務(wù)闷串,即一個Mode對應(yīng)多個Item,而一個item中筋量,包含了timer烹吵、sourceobserver桨武,可以用下圖說明

③.1 Mode類型

其中mode在蘋果文檔中提及的有個肋拔,而在iOS中公開暴露出來的只有 NSDefaultRunLoopModeNSRunLoopCommonModes. NSRunLoopCommonModes實際上是一個Mode的集合,默認(rèn)包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode.

  • NSDefaultRunLoopMode默認(rèn)的mode呀酸,正常情況下都是在這個model下運行(包括主線程)
  • NSEventTrackingRunLoopModecocoa):追蹤mode凉蜂,使用這個mode跟蹤來自用戶交互的事件(比如UITableView上下滑動流暢,為了不受其他mode影響)UITrackingRunLoopMode(iOS)
  • NSModalPanelRunLoopMode:處理modal panels事件
  • NSConnectionReplyMode:處理NSConnection對象相關(guān)事件性誉,系統(tǒng)內(nèi)部使用窿吩,用戶基本不會使用
  • NSRunLoopCommonModes:這是一個偽模式,其為一組runloop mode的集合错览,將輸入源加入此模式意味著在Common Modes中包含的所有模式下都可以處理.在Cocoa應(yīng)用程序中纫雁,默認(rèn)情況下Common Modes包含default modes,modal modes,event Tracking modes.可使用CFRunLoopAddCommonMode方法將Common Modes中添加自定義modes.

③.2 Source & Timer & Observer

  • Source表示可以喚醒RunLoop的一些事件,例如用戶點擊了屏幕蝗砾,就會創(chuàng)建一個RunLoop先较,主要分為Source0Source1
    • Source0表示非系統(tǒng)事件,即用戶自定義的事件
    • Source1表示系統(tǒng)事件悼粮,主要負(fù)責(zé)底層的通訊闲勺,具備喚醒能力
  • Timer就是常用NSTimer定時器這一類
  • Observer主要用于監(jiān)聽RunLoop的狀態(tài)變化,并作出一定響應(yīng)扣猫,主要有以下一些狀態(tài)

④ 測試驗證

④.1 驗證:RunLoop和mode是一對多

上面我們說過RunLoopmode一對多的關(guān)系菜循,下面我們通過運行代碼來實操證明. 我們先通過lldb命令獲取mainRunloopcurrentRunloopcurrentMode

運行結(jié)果表明runloop在運行時的mode只有一個.

下面我們獲取mainRunLoop所有的模型

從上面的打印結(jié)果可以驗證runloopCFRunloopMode具有一對多的關(guān)系.

④.2 驗證:mode和Item也是一對多

我們繼續(xù)在斷點處申尤,通過bt查看堆棧信息癌幕,從這里看出timer的item類型如下所示(截取部分)

RunLoop源碼中查看Item類型,有以下幾種:

  • block應(yīng)用:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
  • 調(diào)用timer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
  • 響應(yīng)source0: __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
  • 響應(yīng)source1:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
  • GCD主隊列:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
  • observer源: __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

我們下面以Timer為例昧穿,一般初始化timer時勺远,都會將timer通過addTimer:forMode:方法添加到Runloop中,于是在源碼中查找addTimer的相關(guān)方法时鸵,即CFRunLoopAddTimer方法胶逢,其源碼實現(xiàn)如下
  • 1.其實現(xiàn)主要判斷是否是kCFRunLoopCommonModes厅瞎,然后查找runloopmode進(jìn)行匹配處理
  • 2.其中kCFRunLoopCommonModes不是一種模式,是一種抽象的偽模式初坠,比defaultMode更加靈活
  • 3.通過CFSetAddValue(rl->_commonModeItems, rlt);可以得知和簸,runloopmode一對多的,同時可以得出modeitem也是一對多的.

⑤ RunLoop執(zhí)行

我們都知道碟刺,RunLoop的執(zhí)行依賴于run方法锁保,從下面的堆棧信息中可以看出,其底層執(zhí)行的是__CFRunLoopRun方法

進(jìn)入__CFRunLoopRun源碼:

通過__CFRunLoopRun源碼可知半沽,針對不同的對象爽柒,有不同的處理

  • 如果有observer,則調(diào)用__CFRunLoopDoObservers
  • 如果有block抄囚,則調(diào)用__CFRunLoopDoBlocks
  • 如果有timer霉赡,則調(diào)用__CFRunLoopDoTimers
  • 如果是source0橄务,則調(diào)用__CFRunLoopDoSources0
  • 如果是source1幔托,則調(diào)用__CFRunLoopDoSource1

_ _CFRunLoopDoTimers

查看下__CFRunLoopDoTimers源碼

主要是通過for循環(huán),對單個timer進(jìn)行處理蜂挪,下面繼續(xù)看__CFRunLoopDoTimer源碼:

通過源碼可知:主要邏輯就是timer執(zhí)行完畢后重挑,會主動調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__函數(shù),正好與timer堆棧調(diào)用中的一致.

timer執(zhí)行總結(jié)

  • 1.為自定義的timer棠涮,設(shè)置Mode谬哀,并將其加入RunLoop
  • 2.在RunLooprun方法執(zhí)行時,會調(diào)用__CFRunLoopDoTimers執(zhí)行所有timer
  • 3.在__CFRunLoopDoTimers方法中严肪,會通過for循環(huán)執(zhí)行單個timer的操作
  • 4.在__CFRunLoopDoTimer方法中史煎,timer執(zhí)行完畢后,會執(zhí)行對應(yīng)的timer回調(diào)函數(shù)

以上驳糯,是針對timer的執(zhí)行分析篇梭,對于observerblock酝枢、source0恬偷、source1,其執(zhí)行原理與timer是類似的帘睦,這里就不再重復(fù)說明以下是蘋果官方文檔針對RunLoop處理不同源的圖示

⑥ RunLoop底層原理

從上述的堆棧信息中可以看出袍患,run在底層的實現(xiàn)路徑為CFRunLoopRun -> CFRunLoopRun -> __CFRunLoopRun 進(jìn)入CFRunLoopRun源碼,其中傳入的參數(shù)1.0e10(科學(xué)計數(shù))等于1* e^10竣付,用于表示超時時間

進(jìn)入CFRunLoopRunSpecific源碼:
  • 首先根據(jù)modeName找到對應(yīng)的mode诡延,然后主要分為三種情況:
    • 如果是entry,則通知observer古胆,即將進(jìn)入runloop
    • 如果是exit肆良,則通過observer,即將退出runloop
    • 如果是其他中間狀態(tài),主要是通過runloop處理各種源

上面說到會調(diào)用__CFRunLoopRun妖滔,上面講了在這一步里面會根據(jù)不同的事件源進(jìn)行不同的處理敦间,當(dāng)RunLoop休眠時,可以通過相應(yīng)的事件喚醒RunLoop.

所以焊虏,綜上所述焰望,RunLoop的執(zhí)行流程,如下所示

⑦ 提出疑問

⑦.1 當(dāng)前有個子線程曲秉,子線程中有個timer采蚀。timer是否能夠執(zhí)行,并進(jìn)行持續(xù)的打映卸榆鼠?

不可以,因為子線程的runloop默認(rèn)不啟動亥鸠, 需要runloop run手動啟動.

⑦.2 RunLoop和線程的關(guān)系

1.每個線程都有一個與之對應(yīng)的RunLoop妆够,所以RunLoop線程一一對應(yīng)的,其綁定關(guān)系通過一個全局的Dictionary存儲负蚊,線程為key神妹,runloop為value.
2.線程中的RunLoop主要是用來管理線程的,當(dāng)線程的RunLoop開啟后家妆,會在執(zhí)行完任務(wù)后進(jìn)行休眠狀態(tài)鸵荠,當(dāng)有事件觸發(fā)喚醒時,又開始工作伤极,即有活時干活蛹找,沒活就休息
3.主線程RunLoop默認(rèn)開啟的,在程序啟動之后哨坪,會一直運行庸疾,不會退出
4.其他線程RunLoop默認(rèn)是不開啟的,如果需要齿税,則手動開啟

⑦.3 NSRunLoop和CFRunLoopRef區(qū)別

  • 1.NSRunLoop是基于CFRunLoopRef面向?qū)ο蟮?code>API彼硫,是不安全
  • 2.CFRunLoopRef是基于C語言,是線程安全的

⑦.4 Runloop的mode作用是什么凌箕?

mode主要是用于指定RunLoop中事件優(yōu)先級

⑦.5 以+scheduledTimerWithTimeInterval:的方式觸發(fā)的timer拧篮,在滑動頁面上的列表時,timer會暫颓2眨回調(diào)串绩,為什么?如何解決芜壁?

  • 1.timer停止的原因是因為滑動scrollView時礁凡,主線程的RunLoop會從NSDefaultRunLoopMode切換到UITrackingRunLoopMode高氮,而timer是添加在NSDefaultRunLoopMode。所以timer不會執(zhí)行
  • 2.將timer放入NSRunLoopCommonModes中執(zhí)行.

寫在后面

和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末顷牌,一起剝皮案震驚了整個濱河市剪芍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窟蓝,老刑警劉巖罪裹,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異运挫,居然都是意外死亡状共,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進(jìn)店門谁帕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峡继,“玉大人,你說我怎么就攤上這事匈挖∧肱疲” “怎么了?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵关划,是天一觀的道長小染。 經(jīng)常有香客問我翘瓮,道長贮折,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任资盅,我火速辦了婚禮调榄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呵扛。我一直安慰自己每庆,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布今穿。 她就那樣靜靜地躺著缤灵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蓝晒。 梳的紋絲不亂的頭發(fā)上腮出,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天,我揣著相機與錄音芝薇,去河邊找鬼胚嘲。 笑死,一個胖子當(dāng)著我的面吹牛洛二,可吹牛的內(nèi)容都是我干的馋劈。 我是一名探鬼主播攻锰,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妓雾!你這毒婦竟也來了娶吞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤械姻,失蹤者是張志新(化名)和其女友劉穎寝志,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體策添,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡材部,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唯竹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乐导。...
    茶點故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖浸颓,靈堂內(nèi)的尸體忽然破棺而出物臂,到底是詐尸還是另有隱情,我是刑警寧澤产上,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布棵磷,位于F島的核電站,受9級特大地震影響晋涣,放射性物質(zhì)發(fā)生泄漏仪媒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一谢鹊、第九天 我趴在偏房一處隱蔽的房頂上張望算吩。 院中可真熱鬧,春花似錦佃扼、人聲如沸偎巢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽压昼。三九已至,卻和暖如春瘤运,著一層夾襖步出監(jiān)牢的瞬間窍霞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工尽超, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留官撼,地道東北人。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓似谁,卻偏偏與公主長得像傲绣,于是被迫代替她去往敵國和親掠哥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,435評論 2 348

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