iOS Block捕獲外部變量和ARC自動拷貝block

Block內(nèi)引用外部變量的問題

#define BLog(prefix,obj) {NSLog(@"位置和指針變量名:%@ ,指針內(nèi)存地址:%p, 指針值:%p ,指向的對象:%@ ",prefix,&obj,obj,obj);}

// 強引用
- (void)blockVariableStrongReferenceTest
{
    NSLog(@"\n");
    NSObject *obj = [[NSObject alloc] init];
    BLog(@"StrongRef obj",obj);
    void(^testBlock)()= ^(){
        BLog(@"StrongRef in block",obj);
    };
    testBlock();
    // Block外部嘗試將obj置為nil
    obj = nil;
    testBlock();  // 第二次調(diào)用block
}

運行結(jié)果

 位置和指針變量名:StrongRef obj ,指針內(nèi)存地址:0x7fff543d0c98, 指針值:0x7fcb1bd22390 ,指向的對象:<NSObject: 0x7fcb1bd22390> 
 
 位置和指針變量名:StrongRef in block ,指針內(nèi)存地址:0x7fcb1c903fb0, 指針值:0x7fcb1bd22390 ,指向的對象:<NSObject: 0x7fcb1bd22390> 
 
 位置和指針變量名:StrongRef in block ,指針內(nèi)存地址:0x7fcb1c903fb0, 指針值:0x7fcb1bd22390 ,指向的對象:<NSObject: 0x7fcb1bd22390

分析

  • 方法內(nèi)部的obj變量在棧中偿渡,變量內(nèi)存地址0x7fff543d0c98,所指向的對象<NSObject: 0x7fcb1bd22390>在堆中伪朽,內(nèi)存地址0x7fcb1bd22390,它的引用計數(shù)+1汛蝙。

  • Block中obj指針已經(jīng)不是外部的obj指針了,它是外部變量obj的拷貝烈涮,它的內(nèi)存地址是0x7fcb1c903fb0,跟外部obj不一樣,但是所指向的對象也是0x7fcb1bd22390窖剑,0x7fcb1bd22390對象引用計數(shù)再+1=2坚洽。

  • block中的obj指針(內(nèi)存地址是0x7fcb1c903fb0)對對象(<NSObject: 0x7fcb1bd22390>)的引用是強引用,在外部將obj(地址0x7fff543d0c98)置為nil后西土,外部的obj不再指向<NSObject: 0x7fcb1bd22390>對象讶舰,0x7fcb1bd22390對象的引用計數(shù)-1,引用計數(shù)為1需了,0x7fcb1bd22390對象內(nèi)存不被自動回收跳昼,所以第二次調(diào)用block,0x7fcb1bd22390對象還在內(nèi)存中肋乍。

結(jié)論
block內(nèi)部的obj 指針是外部obj指針的拷貝鹅颊,有2個指針指向同一個NSObject對象,但只將外部的obj指針置為nil,NSObject對象的引用計數(shù)不為0墓造,無法回收堪伍。

// 弱引用
- (void)blockVariableWeakReferenceTest
{
    NSLog(@"\n");
    NSObject *obj = [[NSObject alloc] init];
    BLog(@"StrongRef obj",obj);  
    __weak NSObject *weakObj = obj;
    BLog(@"WeakRef weakObj", weakObj);
    void(^testBlock)()= ^(){
        BLog(@"weakObj in block",weakObj);
    };
    testBlock();
    obj = nil; 
    testBlock();
}

運行結(jié)果

 位置和指針變量名:StrongRef obj ,指針內(nèi)存地址:0x7fff543d0c98, 指針值:0x7fcb1bd2fee0 ,指向的對象:<NSObject: 0x7fcb1bd2fee0> 
 
 位置和指針變量名:WeakRef weakObj ,指針內(nèi)存地址:0x7fff543d0c90, 指針值:0x7fcb1bd2fee0 ,指向的對象:<NSObject: 0x7fcb1bd2fee0> 
 
位置和指針變量名:weakObj in block ,指針內(nèi)存地址:0x7fcb1bc072b0, 指針值:0x7fcb1bd2fee0 ,指向的對象:<NSObject: 0x7fcb1bd2fee0> 

 位置和指針變量名:weakObj in block ,指針內(nèi)存地址:0x7fcb1bc072b0, 指針值:0x0 ,指向的對象:(null) 

結(jié)論

分析

  • 方法內(nèi)部的weakObj變量在棧中,變量內(nèi)存地址0x7fff543d0c98觅闽,所指向的對象<NSObject: 0x7fcb1bd2fee0>在堆中帝雇,內(nèi)存地址0x7fcb1bd2fee0,它的引用計數(shù)+1谱煤。
  • weakObj變量也在棧中摊求,內(nèi)存為0x7fff543d0c90,所指向的對象也是<NSObject: 0x7fcb1bd2fee0>,弱引用禽拔,所以0x7fcb1bd2fee0對象的引用計數(shù)不增加刘离,仍然為1.
  • Block中weakObj它的內(nèi)存地址是0x7fcb1bc072b0室叉,跟外部的weakObj不同,但是所指向的對象也是0x7fcb1bd2fee0硫惕,弱引用茧痕,0x7fcb1bd2fee0對象引用計數(shù)還是不增加,仍然是1恼除。
    -在外部將obj(地址0x7fff543d0c98)置為nil后踪旷,外部的obj不再指向<NSObject: 0x7fcb1bd2fee0>對象,0x7fcb1bd2fee0對象的引用計數(shù)-1豁辉,引用計數(shù)為0令野,ARC回收0x7fcb1bd2fee0對象內(nèi)存,并將指向它的弱引用指針賦值為nil,所以第二次調(diào)用block徽级,0x7fcb1bd2fee0對象不在在內(nèi)存中气破。

Block生命周期內(nèi)的對象安全

在block中__weak聲明的指針去引用對象 可以避免循環(huán)引用的問題,但是當(dāng)外部對象被釋放了餐抢,block 內(nèi)部會訪問不到這個對象. 這種問題如何解決呢现使?先來看一段代碼:

//多線程時Block生命周期內(nèi)對象安全
- (void)blockVariableMutiThreadTest
{
    NSObject *obj = [[NSObject alloc]init]; //obj強引用,<NSObject: 0x7f9413c1c040>對象引用計數(shù)+1,=1
    BLog(@"obj", obj);
    __weak NSObject *weakObj = obj;//weakObj弱引用,<NSObject: 0x7f9413c1c040>對象引用計數(shù)不變旷痕,=1
    BLog(@"weakObj-0", weakObj);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        __strong NSObject *strongObj = weakObj; //strongObj強引用,<NSObject: 0x7f9413c1c040>對象引用計數(shù)+1碳锈,=2
        sleep(3);
        BLog(@"weakObj - block", weakObj);
        BLog(@"strongObj - block", strongObj);
    });
    sleep(1);
    obj = nil; //obj被置為nil,<NSObject: 0x7f9413c1c040>對象引用計數(shù)-1欺抗,=1
    BLog(@"weakObj-1", weakObj);  //沒被釋放
    sleep(4); //block在異步線程中執(zhí)行完畢(在另一塊內(nèi)存中執(zhí)行)售碳,block內(nèi)存被釋放,<NSObject: 0x7f9413c1c040>對象引用計數(shù)-1绞呈,=0团滥;ARC開始把0x7f9413c1c040對象內(nèi)存回收,把弱引用weakObj置為nil
    BLog(@"weakObj-2", weakObj);
}

執(zhí)行結(jié)果如下:

位置和指針變量名:obj ,指針內(nèi)存地址:0x7fff51888c98, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040> 
位置和指針變量名:weakObj-0 ,指針內(nèi)存地址:0x7fff51888c90, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040> 
位置和指針變量名:weakObj-1 ,指針內(nèi)存地址:0x7fff51888c90, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040> 
位置和指針變量名:weakObj - block ,指針內(nèi)存地址:0x7f9413d9a880, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040> 
位置和指針變量名:strongObj - block ,指針內(nèi)存地址:0x1187e2e08, 指針值:0x7f9413c1c040 ,指向的對象:<NSObject: 0x7f9413c1c040> 
位置和指針變量名:weakObj-2 ,指針內(nèi)存地址:0x7fff51888c90, 指針值:0x0 ,指向的對象:(null) 

總結(jié)
多線程的時候报强,在 block 外部__weak聲明的變量指向一個對象, 通過把__weak聲明的變量值賦值給block內(nèi)部__strong變量```灸姊,實現(xiàn)在block內(nèi)對該對象進行強引用,這樣可以在block生命周期內(nèi)保留該對象不被釋放秉溉,在block生命周期結(jié)束后力惯,對象內(nèi)存被釋放。

block修改外部變量 __block變量

- (void)blockVariable
{
   // 使用__block
   NSObject *obj = [[NSObject alloc]init];
   BLog(@"obj",obj); // 1 
   __block NSObject *blockObj = obj;
   obj = nil;
   BLog(@"外部blockObj -1",blockObj); // 2
   void(^testBlock)() = ^(){
       BLog(@"內(nèi)部blockObj - block",blockObj); // 5
       NSObject *obj2 = [[NSObject alloc]init];
       BLog(@"內(nèi)部obj2",obj2);   // 6
       blockObj = obj2;
       BLog(@"blockObj - block",blockObj); // 7
   };
   NSLog(@"%@",testBlock);   // 3
   
   BLog(@"外部blockObj -2",blockObj); // 4
   testBlock();
   BLog(@"外部blockObj -3",blockObj);   // 8
?
}
?

運行結(jié)果

 位置和指針變量名:obj ,指針內(nèi)存地址:0x7fff5dd2ec78, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00> 
 位置和指針變量名:外部blockObj -1 ,指針內(nèi)存地址:0x7fff5dd2ec70, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00>
 
 <__NSMallocBlock__: 0x7fa084906fa0>      ------ 這是block地址和類型

 位置和指針變量名:外部blockObj -2 ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00> 
 
 位置和指針變量名:內(nèi)部blockObj - block ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa082661a00 ,指向的對象:<NSObject: 0x7fa082661a00>        
 位置和指針變量名:內(nèi)部obj2 ,指針內(nèi)存地址:0x7fff5dd2eba8, 指針值:0x7fa084916660 ,指向的對象:<NSObject: 0x7fa084916660> 
 位置和指針變量名:blockObj - block ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa084916660 ,指向的對象:<NSObject: 0x7fa084916660> 
 
 位置和指針變量名:外部blockObj -3 ,指針內(nèi)存地址:0x7fa084905838, 指針值:0x7fa084916660 ,指向的對象:<NSObject: 0x7fa084916660> 

分析

  • 第3處日志打印了一個testBlock對象召嘶,blockObj的地址發(fā)生變化父晶。此時,block對象 從椗拷貝到堆上甲喝,__block變量blockObj,也被拷貝到堆上铛只。block對象擁有blockObj指針指向的對象埠胖。注意:這是個強引用哦糠溜。
  • 關(guān)注4到8 處日志,用__block關(guān)鍵字聲明blockObj指針后直撤,block內(nèi)外的變量blockObj都是0x7fa084905838非竿,也就是block內(nèi)外的blockObj指針是同一個指針。
  • block內(nèi)部改變 blockObj指針指向的對象谋竖,改動在 block外部可見红柱。

關(guān)于 block訪問外部變量原理

block實現(xiàn)原理(一)

block和變量的內(nèi)存管理(二)

在oc,在block中直接訪問外部變量蓖乘,訪問的是外部變量的copy锤悄。用clang后將 .m翻譯為.cpp文件后發(fā)現(xiàn),外部函數(shù)是通過傳值方式將變量值傳給block(block結(jié)構(gòu)體嘉抒、block最終要執(zhí)行的函數(shù)代碼).
使用了__block后铁蹈,外部函數(shù)是通過指針傳遞,將變量傳遞到 block 內(nèi)众眨,所以可以修改變量值.

Block在內(nèi)存中的位置

Block作為C語言的擴展握牧,并不是高新技術(shù),和其他語言的閉包或lambda表達式是一回事娩梨。需要注意的是由于Objective-C在iOS中不支持GC機制沿腰,使用Block必須自己管理內(nèi)存,而內(nèi)存管理正是使用Block坑最多的地方狈定,錯誤的內(nèi)存管理 要么導(dǎo)致return cycle內(nèi)存泄漏要么內(nèi)存被提前釋放導(dǎo)致crash颂龙。 Block的使用很像函數(shù)指針,不過與函數(shù)最大的不同是:Block可以訪問函數(shù)以外纽什、詞法作用域以內(nèi)的外部變量的值措嵌。換句話說,Block不僅 實現(xiàn)函數(shù)的功能芦缰,還能攜帶函數(shù)的執(zhí)行環(huán)境企巢。

可以這樣理解,Block其實包含兩個部分內(nèi)容:

  1. Block執(zhí)行的代碼让蕾,這是在編譯的時候已經(jīng)生成好的浪规;
  2. 一個包含Block執(zhí)行時需要的所有外部變量值的數(shù)據(jù)結(jié)構(gòu)。 Block將使用到的探孝、作用域附近的變量建立一份快照拷貝

根據(jù)Block在內(nèi)存中的位置分為三種類型:_NSConcreteGlobalBlock笋婿,_NSConcreteMallocBlock, _NSConcreteStackBlock。

  • _NSConcreteGlobalBlock:類似函數(shù)顿颅,位于text段缸濒;
  • _NSConcreteStackBlock:位于棧內(nèi)存,函數(shù)返回后Block將無效;
  • _NSConcreteMallocBlock:位于堆內(nèi)存庇配。
_NSConcreteGlobalBlock:

Blocks that don't capture any variables are global blocks. Since all instances of the block are the same, the compiler can just allocate one copy statically for the life of the program斩跌。

_NSConcreteStackBlock 和 _NSConcreteMallocBlock:

Blocks that capture variables (closures) are either stack or heap (malloc) blocks. Blocks start out on the stack, as stack blocks. When a stack block is copied for the first time, it is moved to the heap. Copying a heap block does not create another copy; but simply retains it.
在開啟 ARC 時,大部分情況下編譯器通常會將創(chuàng)建在棧上的 block 自動拷貝到堆上讨永。

  • 當(dāng) block 作為函數(shù)返回值返回時,編譯器自動將 block 作為 _Block_copy 函數(shù)遇革,效果等同于 block 直接調(diào)用 copy 方法卿闹;
  • 當(dāng) block 被賦值給 __strong id 類型的對象或 block 的成員變量時,編譯器自動將 block 作為 _Block_copy 函數(shù)萝快,效果等同于 block 直接調(diào)用 copy 方法锻霎;
  • 當(dāng) block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時。這些方法會在內(nèi)部對傳遞進來的 block 調(diào)用 copy 或 _Block_copy 拷貝;

ARC中自動copy block的例子

- (void)blockObjectInMemory
{
   // global block
   void (^globalBlockInMemory)(int number) = ^(int number){
       printf("%d \n",number);
   };
   globalBlockInMemory(90);
   BLog(@"global block %@", globalBlockInMemory);
   
   // malloc block
   int outVariable = 100;
   void (^mallocBlockInMemory)(int number) = ^(int number){
       printf("%d \n",outVariable+number);
   };
   BLog(@"stackBlock block %@", mallocBlockInMemory);     // ARC 自動將棧中block拷貝到堆上
}

 位置和指針變量名:global block %@ ,指針內(nèi)存地址:0x7fff5b422c78, 指針值:0x1047e01f0 ,指向的對象:<__NSGlobalBlock__: 0x1047e01f0> 
 位置和指針變量名:stackBlock block %@ ,指針內(nèi)存地址:0x7fff5b422c68, 指針值:0x7fe6849085f0 ,指向的對象:<__NSMallocBlock__: 0x7fe6849085f0> 

- (id)returnBlock
{
    int outVariable = 100;
    void (^mallocBlockInMamory)(void) = ^(void){
            NSLog(@"in block");
    };
    BLog(@" block  ", mallocBlockInMamory);
    return mallocBlockInMamory;
}

- (void)blockInmemory
{
    id block = [self returnBlock];
    BLog(@"a block %@", block);
}


位置和指針變量名: block   ,指針內(nèi)存地址:0x7fff516a9c30, 指針值:0x10e559250 ,指向的對象:<__NSGlobalBlock__: 0x10e559250> 
位置和指針變量名:a block %@ ,指針內(nèi)存地址:0x7fff516a9c78, 指針值:0x10e559250 ,指向的對象:<__NSGlobalBlock__: 0x10e559250> 

在沒有ARC之前揪漩,由于ARC 自動將棧中block拷貝到堆上旋恼,所以當(dāng)returnBlock函數(shù)退出,在棧中內(nèi)存釋放后奄容,仍然可以訪問到block對象冰更。

ARC 中需要手動拷貝Block的例子

在以下情形中, block 會從棸豪眨拷貝到堆:

  • 當(dāng) block 調(diào)用 copy 方法時蜀细,如果 block 在棧上,會被拷貝到堆上戈盈;
  • 當(dāng) block 作為函數(shù)返回值返回時奠衔,編譯器自動將 block 作為 _Block_copy 函數(shù),效果等同于 block 直接調(diào)用 copy 方法塘娶;
  • 當(dāng) block 被賦值給 __strong id 類型的對象或 block 的成員變量時归斤,編譯器自動將 block 作為 _Block_copy 函數(shù),效果等同于 block 直接調(diào)用 copy 方法刁岸;
  • 當(dāng) block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時脏里。這些方法會在內(nèi)部對傳遞進來的 block 調(diào)用 copy 或 _Block_copy 進行拷貝;

其他情況需要手動拷貝。

- (void)stackBlockInMemory
{
    NSArray *array = [self getBlockArray];
    id block = array[0];
    BLog(@"block %@", block);
}

- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"value:%d", val);},
            ^{NSLog(@"value:%d", val);}, nil];
}

程序會報EXC_BAD_ACCESS ,getBlockArray返回的數(shù)組里面的 block 是不可訪問的虹曙。

手動copy后膝宁,block拷貝到堆上,getBlockArray函數(shù)返回的棧幀被銷毀后根吁,仍可以訪問堆中的block拷貝员淫。

- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"value:%d", val);} copy],
            [^{NSLog(@"value:%d", val);} copy], nil];
}

Block中造成內(nèi)存泄漏的一些場景

推薦文章:

http://www.tanhao.me/pieces/310.html/

block 內(nèi)存管理

block 實現(xiàn)原理

Block-ABI-Apple

正確使用Block避免Cycle Retain和Crash

__weak與__block區(qū)別

block實現(xiàn)原理(一)

block和變量的內(nèi)存管理(二)

How blocks are implemented (and the consequences

block實現(xiàn)原理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市击敌,隨后出現(xiàn)的幾起案子介返,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圣蝎,死亡現(xiàn)場離奇詭異刃宵,居然都是意外死亡,警方通過查閱死者的電腦和手機徘公,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門牲证,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人关面,你說我怎么就攤上這事坦袍。” “怎么了等太?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵捂齐,是天一觀的道長。 經(jīng)常有香客問我缩抡,道長奠宜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任瞻想,我火速辦了婚禮压真,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蘑险。我一直安慰自己榴都,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布漠其。 她就那樣靜靜地躺著嘴高,像睡著了一般。 火紅的嫁衣襯著肌膚如雪和屎。 梳的紋絲不亂的頭發(fā)上拴驮,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音柴信,去河邊找鬼套啤。 笑死,一個胖子當(dāng)著我的面吹牛随常,可吹牛的內(nèi)容都是我干的潜沦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼绪氛,長吁一口氣:“原來是場噩夢啊……” “哼唆鸡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起枣察,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤争占,失蹤者是張志新(化名)和其女友劉穎燃逻,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臂痕,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡伯襟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了握童。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姆怪。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖澡绩,靈堂內(nèi)的尸體忽然破棺而出稽揭,到底是詐尸還是另有隱情,我是刑警寧澤英古,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布淀衣,位于F島的核電站昙读,受9級特大地震影響召调,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蛮浑,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一唠叛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沮稚,春花似錦艺沼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盛杰,卻和暖如春挽荡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背即供。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工定拟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逗嫡。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓青自,卻偏偏與公主長得像,于是被迫代替她去往敵國和親延窜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

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