OC中block的底層實現(xiàn)原理

1. block的本質(zhì)

block本質(zhì)上是一個OC對象,它內(nèi)部也有isa指針,這個對象封裝了函數(shù)調(diào)用地址以及函數(shù)調(diào)用環(huán)境(函數(shù)參數(shù)、返回值、捕獲的外部變量等)。當我們定義一個block,在編譯后它的底層存儲結(jié)構是怎樣的呢?

下面我們來看一個例子,定義了一個block,并在block里面訪問量block外面的變量age,它底層存儲結(jié)構如下圖所示,block底層就是一個結(jié)構體__main_block_impl_0

block的本質(zhì)

  • impl->isa:就是isa指針,可見它就是一個OC對象。
  • impl->FuncPtr:是一個函數(shù)指針,也就是底層將block中要執(zhí)行的代碼封裝成了一個函數(shù),然后用這個指針指向那個函數(shù)。
  • Desc->Block_size:block占用的內(nèi)存大小。
  • age:捕獲的外部變量age,可見block會捕獲外部變量并將其存儲在block的底層結(jié)構體中。

當我們調(diào)用block時(block()),實際上就是通過函數(shù)指針FuncPtr找到封裝的函數(shù)并將block的地址作為參數(shù)傳給這個函數(shù)進行執(zhí)行,把block傳給函數(shù)是因為函數(shù)執(zhí)行中需要用到的某些數(shù)據(jù)是存在block的結(jié)構體中的(比如捕獲的外部變量)。如果定義的是帶參數(shù)的block,調(diào)用block時是將block地址和block的參數(shù)一起傳給封裝好的函數(shù)。

2. block的變量捕獲機制

block外部的變量是可以被block捕獲的,這樣就可以在block內(nèi)部使用外部的變量了。不同類型的變量的捕獲機制是不一樣的。下面我們來看一個示例:

int c = 1000; // 全局變量
static int d = 10000; // 靜態(tài)全局變量

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 10; // 局部變量
        static int b = 100; // 靜態(tài)局部變量
        void (^block)(void) = ^{
             NSLog(@"a = %d",a);
             NSLog(@"b = %d",b);
             NSLog(@"c = %d",c);
             NSLog(@"d = %d",d);
         };
         a = 20;
         b = 200;
         c = 2000;
         d = 20000;
         block();
    }
    return 0;
}

// ***************打印結(jié)果***************
2020-01-07 15:08:37.541840+0800 CommandLine[70672:7611766] a = 10
2020-01-07 15:08:37.542168+0800 CommandLine[70672:7611766] b = 200
2020-01-07 15:08:37.542201+0800 CommandLine[70672:7611766] c = 2000
2020-01-07 15:08:37.542222+0800 CommandLine[70672:7611766] d = 20000

關于運行結(jié)果我們后面再做講解,我們先來看一下定義的這個block的在編譯后底層存儲結(jié)構是怎么樣的呢?(可以在命令行運行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m將這個main.m文件轉(zhuǎn)成編譯后的c/c++文件,然后在這個文件搜索__main_block_impl_0就可以找到這個block的結(jié)構體)。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
};

我們可以發(fā)現(xiàn),我定義了4個變量,結(jié)果只有2個局部變量被捕獲了,而且2個局部變量的捕獲方式還不一樣。為什么會這樣呢?下面來一一解釋:

2.1 全局變量的捕獲

不管是普通全局變量還是靜態(tài)全局變量,block都不會捕獲。因為全局變量在哪里都可以訪問,所以block內(nèi)部不捕獲也是可以直接訪問全局變量的,所以外部更改全局變量的值時,block內(nèi)部打印的就是最新更改的值。

2.2 靜態(tài)局部變量的捕獲

我們發(fā)現(xiàn)定義的靜態(tài)局部變量b被block捕獲后,在block結(jié)構體里面是以int *b;的形式來存儲的,也就是說block其實是捕獲的變量b的地址,block內(nèi)部是通過b的地址去獲取或修改b的值,所以block外部更改b的值會影響block里面獲取的b的值,block里面更改b的值也會影響block外面b的值。所以上面會打印b = 200

2.3 普通局部變量的捕獲

所謂的普通局部變量就是在一個函數(shù)或代碼塊中定義的類似int a = 10;的變量,它其實是省略了auto關鍵字,等價于auto int a = 10,所以也叫auto變量。和靜態(tài)局部變量不同的是,普通局部變量被block捕獲后再block底層結(jié)構體中是以int a;的形式存儲,也就是說block捕獲的其實是a的值(也就是10),并且在block內(nèi)部重新定義了一個變量來存儲這個值,這個時候block外部和里面的a其實是2個不同的變量,所以外面更改a的值不會影響block里面的a。所以打印的結(jié)果是a = 10

那有人可能就有疑問了,為什么普通局部變量要捕獲值,跟靜態(tài)局部變量一樣捕獲地址不行嗎?是的,不行。因為普通局部變量a在出了大括號后就會被釋放掉了,這個時候如果我們在大括號外面調(diào)用這個block,block內(nèi)部通過a的指針去訪問a的值就會拋出異常,因為a已經(jīng)被釋放了。而靜態(tài)局部變量的生命周期是和整個程序的生命周期是一樣的,也就是說在整個程序運行過程中都不會釋放b,所以不會出現(xiàn)這種情況。

那有人又有疑問了,既然靜態(tài)局部變量一直都不會被釋放,那block為什么還要捕獲它,直接拿來用不就可以了嗎?這是因為靜態(tài)局部變量作用域只限制在這個大括號類,出了這個大括號,雖然它還存在,但是外面無法訪問它。而前面已經(jīng)介紹過,block里面的代碼在底層是被封裝成了一個函數(shù),那這個函數(shù)肯定是在b所在的大括號外面,所以這個函數(shù)是無法直接訪問到b的,所以block必須將其捕獲。

2.4 block捕獲變量小結(jié)

  • 全局變量--不會捕獲,是直接訪問。
  • 靜態(tài)局部變量--是捕獲變量地址。
  • 普通局部變量--是捕獲變量的值。

所以我們判斷一個變量是否會被block捕獲關鍵就在于這個變量是局部變量還是全局變量。那我們來看一下以下幾種情況中block是否會捕獲self:

- (void)blockTest{
    
    // 第一種
    void (^block1)(void) = ^{
        NSLog(@"%p",self);
    };
    
    // 第二種
    void (^block2)(void) = ^{
        self.name = @"Jack";
    };
    
    // 第三種
    void (^block3)(void) = ^{
        NSLog(@"%@",_name);
    };
    
    // 第四種
    void (^block4)(void) = ^{
        [self name];
    };
}

要搞清楚這個問題,我們主要是要搞清楚self是局部變量還是全局變量。可能很多人以為它是全局變量,其實他是一個局部變量。再強調(diào)一遍,self是局部變量。那有人就疑惑了,它為什么是局部變量?它是在哪里定義的。要搞清楚這個問題,我們需要對OC的方法調(diào)用機制有一定了解。

OC中一個對象調(diào)用方法,其實就是給這個這個對象發(fā)送消息。比如我調(diào)用[self blockTest],它轉(zhuǎn)成C語言后就變成了objc_msgSend(self, @selector(blockTest))。OC的blockTest方法是沒有參數(shù)的,但是轉(zhuǎn)成objc_msgSend后就多出來了2個參數(shù),一個就是self,是指向函數(shù)調(diào)用者的,另一個參數(shù)就是要調(diào)用的這個方法。所以對于所有的OC方法來說,它們都有這2個默認的參數(shù),第一個參數(shù)就是self,所以self就是這么通過參數(shù)的形式傳進來的,它的確是一個局部變量。

這個問題解決了,那上面幾種情況就簡單了,這4中情況下block都會捕獲self。對第一、二、四這3種情況都好理解,第三種情況可能有人會有疑惑,block里面都沒有用到self為什么還會捕獲self呢。其實_name這種寫法并不是沒有self,它只是將self給省略了,它其實等同于self->_name,所以這種情況要格外注意。

3. block的類型

block既然是一個OC對象,那它就有類,那它的類時什么呢?我們可以通過調(diào)用block的class方法或object_getClass()函數(shù)來得到block的類。

- (void)test{
    int age = 10;
    
    void (^block1)(void) = ^{
        NSLog(@"-----");
    };
    NSLog(@"block1的類:%@",[block1 class]);
    
    NSLog(@"block2的類:%@",[^{
        NSLog(@"----%d",age);
    } class]);
    
   NSLog(@"block3的類:%@",[[^{
       NSLog(@"----%d",age);
   } copy] class]);
}

// ***************打印結(jié)果***************
2020-01-08 09:07:46.253895+0800 AppTest[72445:7921459] block1的類:__NSGlobalBlock__
2020-01-08 09:07:46.254027+0800 AppTest[72445:7921459] block2的類:__NSStackBlock__
2020-01-08 09:07:46.254145+0800 AppTest[72445:7921459] block3的類:__NSMallocBlock__

block的類有三種,就是上面打印的結(jié)果:__NSGlobalBlock____NSStackBlock____NSMallocBlock__。有人可能會疑惑,打印第2和3這兩個block時為不像block1那樣先定義一個block1然后再打印block1。這是因為在ARC模式下,如果一個__NSStackBlock__類型的block被一個強指針指著,那系統(tǒng)會自動對這個block進行一次copy操作將這個block變成__NSMallocBlock__類型,這樣會影響運行的結(jié)果。

下面我們一一介紹一下這三種類型的區(qū)別:

3.1 __NSGlobalBlock__

如果一個block里面沒有訪問普通局部變量(也就是說block里面沒有訪問任何外部變量或者訪問的是靜態(tài)局部變量或者訪問的是全局變量),那這個block就是__NSGlobalBlock____NSGlobalBlock__類型的block在內(nèi)存中是存在數(shù)據(jù)區(qū)的(也叫全局區(qū)或靜態(tài)區(qū),全局變量和靜態(tài)變量是存在這個區(qū)域的)。__NSGlobalBlock__類型的block調(diào)用copy方法的話什么都不會做

下面我們再來看下__NSGlobalBlock__的繼承鏈:

- (void)test{
    void (^block)(void) = ^{
        NSLog(@"-----");
    };
    NSLog(@"--- %@",[block class]);
    NSLog(@"--- %@",[[block class] superclass]);
    NSLog(@"--- %@",[[[block class] superclass] superclass]);
    NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}

// ***************打印結(jié)果***************
2020-01-08 11:03:34.331652+0800 AppTest[72667:7957820] --- __NSGlobalBlock__
2020-01-08 11:03:34.331777+0800 AppTest[72667:7957820] --- __NSGlobalBlock
2020-01-08 11:03:34.331883+0800 AppTest[72667:7957820] --- NSBlock
2020-01-08 11:03:34.331950+0800 AppTest[72667:7957820] --- NSObject

看了block最終也是繼承自NSObject,__NSGlobalBlock__的繼承鏈為:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

3.2 __NSStackBlock__

如果一個block里面訪問了普通的局部變量,那它就是一個__NSStackBlock__,它在內(nèi)存中存儲在棧區(qū),棧區(qū)的特點就是其釋放不受開發(fā)者控制,都是由系統(tǒng)管理釋放操作的,所以在調(diào)用__NSStackBlock__類型block時要注意,一定要確保它還沒被釋放。如果對一個__NSStackBlock__類型block做copy操作,那會將這個block從棧復制到堆上。

__NSStackBlock__的繼承鏈是:__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject

3.3 __NSMallocBlock__

一個__NSStackBlock__類型block做調(diào)用copy,那會將這個block從棧復制到堆上,堆上的這個block類型就是__NSMallocBlock__,所以__NSMallocBlock__類型的block是存儲在堆區(qū)。如果對一個__NSMallocBlock__類型block做copy操作,那這個block的引用計數(shù)+1。

__NSMallocBlock__的繼承鏈是:__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject

ARC環(huán)境下,編譯器會根據(jù)情況,自動將棧上的block復制到堆上。有一下4種情況會將棧block復制到堆上:

a. block作為函數(shù)返回值時:

typedef void (^MyBlock)(void);

- (MyBlock)createBlock{
    int a = 10;
    return ^{
        NSLog(@"******%d",a);
    };
}

b. 將block賦值給強指針時:

將定義的棧上的block賦值給強指針myBlock,就變成了堆block。

- (void)test{
    int a = 10; // 局部變量

    void (^myBlock)(void) = ^{
        NSLog(@"a = %d",a);
    };
    block();
}

c. 當block作為參數(shù)傳給Cocoa API時:

比如:

[UIView animateWithDuration:1.0f animations:^{
        
}];

d. block作為GCD的API的參數(shù)時:

比如:

dispatch_async(dispatch_get_main_queue(), ^{
        
});

另外在MRC環(huán)境下,定義block屬性建議使用copy關鍵字,這樣會將棧區(qū)的block復制到堆區(qū)。

@property (copy,nonatomic) void(^block)void;

ARC環(huán)境下,定義block屬性用copystrong關鍵字都會將棧區(qū)block復制到堆上,所以這兩種寫法都可以。

@property (strong, nonatomic) void (^block)(void); 
@property (copy, nonatomic) void (^block)(void);

4. block對對象型的局部變量的捕獲

block對對象類型和對基本數(shù)據(jù)類型變量的捕獲是不一樣的,對象類型的變量涉及到強引用和弱引用的問題,強引用和弱引用在block底層是怎么處理的呢?

如果block是在棧上,不管捕獲的對象時強指針還是弱指針,block內(nèi)部都不會對這個對象產(chǎn)生強引用。所以我們主要來看下block在堆上的情況。

首先來看下強引用的對象被block捕獲后在底層結(jié)構體中是如何存儲的。這里用下面這條命令來將OC代碼轉(zhuǎn)成c/c++代碼:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
// OC代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        void (^block)(void) = ^{
            NSLog(@"age--- %ld",person.age);
         };
        block();

    }
    return 0;
}

// 底層結(jié)構體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
};

可以看到和基本數(shù)據(jù)類型不同的是,person對象被block捕獲后,在結(jié)構體中多了一個修飾關鍵字__strong

我們再來看下弱引用對象被捕獲后是什么樣的:

// OC代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        __weak Person *weakPerson = person;
        void (^block)(void) = ^{
            NSLog(@"age--- %ld",weakPerson.age);
         };
        block();

    }
    return 0;
}

// 底層block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
};

可見此時block中weakPerson的關鍵字變成了__weak

在block中修飾被捕獲的對象類型變量的關鍵字除了__strong__weak外還有一個__unsafe_unretained。那這結(jié)果關鍵字起什么作用呢?

當block被拷貝到堆上時是調(diào)用的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)就會根據(jù)這3個關鍵字來進行操作。

  • 如果關鍵字是__strong,那block內(nèi)部就會對這個對象進行一次retain操作,引用計數(shù)+1,也就是block會強引用這個對象。也正是這個原因,導致在使用block時很容易造成循環(huán)引用。
  • 如果關鍵字是__weak__unsafe_unretained,那block對這個對象是弱引用,不會造成循環(huán)引用。所以我們通常在block外面定義一個__weak__unsafe_unretained修飾的弱指針指向?qū)ο螅缓笤赽lock內(nèi)部使用這個弱指針來解決循環(huán)引用的問題。

block從堆上移除時,則會調(diào)用block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部調(diào)用_Block_object_dispose函數(shù)會自動釋放強引用的變量。

4. __block修飾符的作用

在介紹__block之前,我們先來看下下面這段代碼:

- (void)test{
    int age = 10;
    void (^block)(void) = ^{
        age = 20;
    };
}

這段代碼有什么問題嗎?編譯器會直接報錯,在block中不可以修改這個age的值。為什么呢?

因為age是一個局部變量,它的作用域和生命周期就僅限在是test方法里面,而前面也介紹過了,block底層會將大括號中的代碼封裝成一個函數(shù),也就相當于現(xiàn)在是要在另外一個函數(shù)中訪問test方法中的局部變量,這樣肯定是不行的,所以會報錯。

如果我想在block里面更改age的值要怎么做呢?我們可以將age定義成靜態(tài)局部變量static int age = 10;。雖然靜態(tài)局部變量的作用域也是在test方法里面,但是它的生命周期是和程序一樣的,而且block捕獲靜態(tài)局部變量實際是捕獲的age的地址,所以block里面也是通過age的地址去更改age的值,所以是沒有問題的。

但我們并不推薦這樣做,因為靜態(tài)局部變量在程序運行過程中是不會被釋放的,所以還是要盡量少用。那還有什么別的方法來實現(xiàn)這個需求呢?這就是我們要講的__block關鍵字。

- (void)test1{
    __block int age = 10;
    void (^block)(void) = ^{
        age = 20;
    };
    block();
    NSLog(@"%d",age);
}

當我們用__block關鍵字修飾后,底層到底做了什么讓我們能在block里面訪問age呢?下面我們來看下上面代碼轉(zhuǎn)成c++代碼后block的存儲結(jié)構是什么樣的。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
};

struct __Block_byref_age_0 {
  void *__isa; // isa指針
__Block_byref_age_0 *__forwarding; // 如果這block是在堆上那么這個指針就是指向它自己,如果這個block是在棧上,那這個指針是指向它拷貝到堆上后的那個block
 int __flags;
 int __size; // 結(jié)構體大小
 int age; // 真正捕獲到的age
};

我們可以看到,age__block修飾后,在block的結(jié)構體中變成了__Block_byref_age_0 *age;,而__Block_byref_age_0是個結(jié)構體,里面有個成員int age;,這個才是真正捕獲到的外部變量age,實際上外部的age的地址也是指向這里的,所以不管是外面還是block里面修改age時其實都是通過地址找到這里來修改的。

所以age__block修飾后它就不再是一個test1方法內(nèi)部的局部變量了,而是被包裝成了一個對象,age就被存儲在這個對象中。之所以說是包裝成一個對象,是因為__Block_byref_age_0這個結(jié)構體的第一個成員就是isa指針。

__block修飾變量的內(nèi)存管理:

__block不管是修飾基礎數(shù)據(jù)類型還是修飾對象數(shù)據(jù)類型,底層都是將它包裝成一個對象(我這里取個名字叫__blockObj),然后block結(jié)構體中有個指針指向__blockObj。既然是一個對象,那block內(nèi)部如何對它進行內(nèi)存管理呢?

當block在棧上時,block內(nèi)部并不會對__blockObj產(chǎn)生強引用。

當block調(diào)用copy函數(shù)從棧拷貝到堆中時,它同時會將__blockObj也拷貝到堆上,并對__blockObj產(chǎn)生強引用。

當block從堆中移除時,會調(diào)用block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部又會調(diào)用_Block_object_dispose函數(shù)來釋放__blockObj

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

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