玩轉 IOS 開發 - block 使用詳解

Block 是iOS在4.0之後新增的程式語法,在iOS SDK 4.0之後,block應用幾乎無處不在。

在其他語言中也有類似的概念稱做閉包(closure),比如object C的好兄弟swift 中閉包(swift 閉包詳解)的使用跟 OC的block一樣重要。總的來說:

Block是C語言的
Block是一個數據類型
Block 是一個提前準備好的代碼,在需要的時候執行

1. block作用:

Block用來封裝一段代碼,可以在任何時候執行;

  • Block可以作爲函數參數或者函數的返回值,而其本身又可以帶輸入參數或返回值。
  • 蘋果官方建議儘量多用block。在多線程、異步任務 、集合遍歷、集合排序、動畫轉場用的很多

在新的iOS API中block被大量用來取代傳統的delegate和callback,而新的API會大量使用block主要是基於以下兩個原因:

A. 可以直接在block代碼塊中寫等會要接着執行的代碼,直接把block變成函數的參數傳入函數中,這是新API最常使用block的地方。

B. 可以存取局部變量,在傳統的callback操作時,若想要存取局部變量得將變量封裝成結構體才能使用,而block則是可以很方便地直接存取局部變量。

2. Block的定義:

定義時,把block當成數據類型

特點:
1. 類型比函數定義多了一個 ^
2. 設置數值,有一個 ^,內容是 {} 括起的一段代碼

(1)基本定義方式

/*
*1.最簡單的定義方式:
*格式:void (^myBlock)() = ^ { // 代碼實現; }
*/
void (^myBlock)() = ^ {
    NSLog(@"hello");
};

// 執行時,把block當成函數
myBlock();

/*
*2.定義帶參數的block:
*格式:void (^block名稱)(參數列表) = ^ (參數列表) { // 代碼實現; }
*/
void (^sumBlock)(int, int) = ^ (int x, int y) {
    NSLog(@"%d", x + y);
};

sumBlock(10, 20);

/*
*3.定義帶返回值的block
*格式:返回類型 (^block名稱)(參數列表) = ^ 返回類型 (參數列表) { // 代碼實現; }
*/
int (^sumBlock2)(int, int) = ^ int (int a, int b) {
    return a + b;
};

NSLog(@"%d", sumBlock2(4, 8));

(2) block 指針

Block Pointer是這樣定義的:

回傳值 (^名字) (參數列);

//聲明一個名字爲square的Block Pointer,其所指向的Block有一個int輸入和int輸出  
int (^square)(int);  

//block 指針square的內容
square = ^(int a){ return a*a ; };  

//調用方法,感覺是是不是很像function的用法?  
int result = square(5);  
NSLog(@"%d", result);  

(3) 用typedef先聲明類型,再定義變量進行賦值

typedef int (^MySum)(int,int);
MySum sum = ^(int a,int b)
 { 
      return a + b;
};

(4) block 訪問外部變量

但是block使用有個特點,Block可以訪問局部變量,但是不能修改:

int sum = 10;

int (^MyBlock)(int) = ^(int num) 
{ 
     sum++;//編譯報錯 
     return num * sum; 
};

如果要修改就要加關鍵字 __block (下面詳細說明):

__block int sum =10;

(5) block 與函數指針

下面比較下函數指針與block異同:

  • 定義函數指針 int (*myFn)();
    調用函數指針 (*myFn)(10, 20);

  • 定義Block int (^MyBlocks)(int,int);
    調用Blocks MyBlocks(10, 20);

3. block訪問外部變量

block 訪問外部變量有幾個特點必須知道:

  • block內部可以訪問外部變量;
  • 默認情況下block內部不能修改外面的局部變量;
  • 給局部變量加上關鍵字_block,這個局部變量就可以在block內部修改;

block中可以訪問外部變量。但是不能修改它,否則編譯錯誤。但是可以改變全局變量、靜態變量(static)、全局靜態變量。

上面的特點是有原因滴:

A. 爲何不讓修改變量:這個是編譯器決定的。理論上當然可以修改變量了,只不過block捕獲的是外部變量的副本,名字一樣。爲了不給開發者迷惑,乾脆不讓賦值。道理有點像:函數參數,要用指針,不然傳遞的是副本(大家想起那個經典的兩個數調換值的問題了吧)。

B. 可以修改靜態變量的值。靜態變量屬於類的,不是某一個變量。所以block內部不用調用cself指針。所以block可以調用。

(1) __block存儲類型

通過__block存儲類型修飾符, 變量在block中可被修改。__block存儲跟register、auto和static存儲類型相似(但是之間互斥),用於局部變量。__block變量存儲在堆區,因此,這個block使用的外部變量,將會在棧結束被留下來。

從優化角度考慮,block存儲在棧上,如果block被拷貝(通過Block_copy或者copy),變量被拷貝到堆。因此__block變量的地址就會改變。

__block變量還有兩個限制,他們不能是可變數組(NSMutableArray),不能是結構體(structure)。

__block 變量的內部實現要複雜許多,__block 變量其實是一個結構體對象,拷貝的是指向該結構體對象的指針

(2) block訪問外部變量

上面已經說過,默認block 訪問的外部變量是隻讀屬性的,若要對外部變量進行讀寫,需要在定義外部變量時加一個 __block, 示例如下:

//示例1:block訪問外部變量
void demoBlock1() 
{
    int x = 10;
    NSLog(@"定義前 %p", &x);// 局部變量在棧區

    // 在定義block的時候,如果引用了外部變量,默認是把外部變量當做是常量編碼到block當中,並且把外部變量copy到堆中,外部變量值爲定義block時變量的數值
    // 如果後續再修改x的值,默認不會影響block內部的數值變化!
    // 在默認情況下,不允許block內部修改外部變量的數值!因爲會破壞代碼的可讀性,不易於維護!
    void(^myBlock)() = ^ {

        NSLog(@"%d", x);
        NSLog(@"in block %p", &x); // 堆中的地址
    };
    //輸出是10,因爲block copy了一份x到堆中

    NSLog(@"定義後 %p", &x);  // 棧區
    x = 20;

    myBlock();
}
//示例2:在block中修改外部變量
void demoBlock2() 
{
    // 使用 __block,說明不在關心x數值的具體變化
    __block int x = 10;
    NSLog(@"定義前 %p", &x);                 // 棧區

    // !定義block時,如果引用了外部使用__block的變量,在block定義之後, block外部的x和block內部的x指向了同一個值,內存地址相同
    void (^myBlock)() = ^ {
        x = 80;
        NSLog(@"in block %p", &x);          // 堆區
    };
    NSLog(@"定義後 %p", &x);                 // 堆區

    myBlock();
    NSLog(@"%d", x);
    //打印x的值爲8,且地址在堆區中
}

下面的例子就有點難度了,讓我們看下block對指針變量的訪問

//例子3:block對指針變量的訪問
void demoBlock3() 
{    
    // !指針記錄的是地址
    NSMutableString *strM = [NSMutableString stringWithString:@"zhangsan"];
    //strM是指針,其在堆中存儲的是zhangsan這個string在內存中的的地址值
    //&strM是指針strM在堆中的地址
    NSLog(@"定義前 %p %p", strM, &strM); 

    void (^myBlock)() = ^ {
        /*首先調用block會對strM(指針)進行一份copy,這份copy會在堆中創建
        另一個指針,這個指針存儲的值同strM,都是zhangsan的地址,
        即新copy的指針指向的內容沒有變
        */

        // 注意下面的操作是修改strM指針指向的內容
        [strM setString:@"lisi"];
        NSLog(@"inblock %p %p", strM, &strM);
        //輸出:strM沒有變,因爲存儲的都是zhangsan的地址,&strM爲堆中新地址

        /*
        *這句代碼是修改指針strM,因爲strM copy過來後是隻讀的,所以同例子2編譯會報錯,需要在定義strM時加__block
        strM = [NSMutableString stringWithString:@"wangwu"];
        NSLog(@"inblock %p %p", strM, &strM);
        */
    };

    //大家想想使用__block輸出會是什麼呢
    NSLog(@"定義後 %p %p", strM, &strM);

    myBlock();
    NSLog(@"%@", strM);
}

上面的例子搞定了,來讓我們看下各種類型的變量與block之間的互動:

//示例4:各種類型的變量和block之間的互動
  extern NSInteger CounterGlobal;
  static NSInteger CounterStatic;

  NSInteger localCounter = 42 ;
  __block char localCharacter;
  void (^aBlock)( void ) = ^( void )
  {
      ++ CounterGlobal ; //可以存取。
      ++ CounterStatic ; //可以存取。 

      CounterGlobal = localCounter; //localCounter在block 建立時就不可變了。
      localCharacter = 'a' ; //設定外面定義的localCharacter 變數。
  };

  ++localCounter; //不會影響的block 中的值。
  localCharacter = 'b' ;
  aBlock(); //執行block 的內容。

  //執行完後,localCharachter 會變成'a'

(3) block 引用成員變量

OC對象,不同於基本類型,Block會引起對象的引用計數變化。若我們在block中引用到oc的對象,則對象的引用計數器會加1, 不過在對象前 加__block修飾,則參考計數不變。

- 若直接存取實例變量(instance variable),self的參考計數將被加1。
- 若透過變量存取實例變量的值,則變量的參考計數將被加1。
- 在對象前加 __block 則參考計數不會自動加1。
//例子1:定義一個變量間接給block調用,成員變量引用計數不變
dispatch_async (queue, ^{
   // 因爲直接存取實例變量instanceVariable ,所以self 的retain count 會加1
   doSomethingWithObject (instanceVariable);
});

//通過
id localVaribale = instanceVariable;
dispatch_async (queue, ^{
   //localVariable 是存取值,所以這時只有localVariable 的retain count 加1
   //self 的 return count  並不會增加。
  doSomethingWithObject (localVaribale);
});

上面只是簡單演示下block引用成員變量,下面我們研究下block引用成員變量時出現的一個經典問題:循環引用

在block內部使用成員變量,如下:

@interface ViewController : UIViewController 
{ 
    NSString *_string; 
} 
@end

在block創建中:

_block = ^(){ 
    NSLog(@"string %@", self.string); 
};

上面代碼中block是會對內部的成員變量進行一次retain, 即self會被retain一次。

對於block 使用 成員變量self.string來說,block內部是直接強引用self的。也就是block持有了self,在這裏bock又作爲self的一個成員被持有,就會導致循環引用和內存泄露

修改方案很簡單:

新建一個__block scope的局部變量,並把self賦值給它,而在block內部則使用這個局部變量來進行取值,上面說過:__block標記的變量是不會被自動retain的。

__block ViewController *controller = self; 

_block = ^(){ 
    NSLog(@"string %@", controller.string); 
}; 

4. block 基本使用

當block定義完成後,我們除了可以像使用一般函數的方式來直接調用它以外,還可以有其他妙用,這些靈活的應用纔是block最爲強大的地方。

(1) block 作爲函數參數

我們可以像使用一般函數使用參數的方式將block以函數參數的型式傳入函數中,在這種情況下,大多數我們使用block的方式將不會傾向定義一個block,而是直接以內嵌的方式來將block傳入,這也是目前新版SDK中主流的做法

下面的例子中,block本身就是函數參數的一部分

char *myCharacters[ 3 ] = { "TomJohn" , "George" , "Charles Condomine" };

qsort_b (myCharacters, 3 , sizeof ( char *), ^( const void *l, const void *r)
{
    //需要類型強轉下
    char *left = *( char **)l;
    char *right = *( char **)r;
    return strncmp (left, right, 1 );
} // 這裏是block 的終點
);
// 最後的結果爲:{"Charles Condomine", "George", "TomJohn"}

(2) Block當作方法的參數

// 所有的資料
NSArray *array = [ NSArray arrayWithObjects : @"A" , @"B" , @"C" , @"A" , @"B" , @"Z" , @"G" , @"are" , @" Q" ,nil ];   

// 我們只要這個集合內的資料
NSSet *filterSet = [ NSSet setWithObjects : @"A" , @"B" , @"Z" , @"Q" , nil ];
BOOL (^test)( id obj, NSUInteger idx, BOOL *stop);
test = ^ ( id obj, NSUInteger idx, BOOL *stop) {
    // 只對前5 筆資料做檢查
    if (idx < 5 ) 
    {
        if ([filterSet containsObject : obj]) 
        {
            return YES ;
        }
    }

    return NO ;
};

NSIndexSet *indexes = [array indexesOfObjectsPassingTest :test];
NSLog ( @"indexes: %@" , indexes);   
// 結果:indexes: <NSIndexSet: 0x6101ff0>[number of indexes: 4 (in 2 ranges), indexes: (0-1 3-4)]
// 前5筆資料中,有4筆符合條件,它們的索引值分別是0-1, 3-4

(3)OC方法中block實例

A. sortedArrayUsingComparator:

//這裏面block代碼塊直接內嵌作爲方法的參數
NSArray *sortedArray = [array sortedArrayUsingComparator: ^(id obj1, id obj2) {
    //左邊大於右邊,降序
    if ([obj1 integerValue] > [obj2 integerValue]) 
    {
        return (NSComparisonResult)NSOrderedDescending;
    }

    //右邊大於左邊,升序
    if ([obj1 integerValue] < [obj2 integerValue]) 
    {
        return (NSComparisonResult)NSOrderedAscending;
    }

    //相同
    return (NSComparisonResult)NSOrderedSame;
}];

B. enumerateObjectsUsingBlock

通常enumerateObjectsUsingBlock: 和 (for(… in …)在效率上基本一致,有時會快些。主要是因爲它們都是基於 NSFastEnumeration實現的。快速迭代在處理的過程中需要多一次轉換,當然也會消耗掉一些時間. 基於Block的迭代可以達到本機存儲一樣快的遍歷集合. 對於字典同樣適用。

  • 注意”enumerateObjectsUsingBlock” 修改局部變量時, 你需要聲明局部變量爲 __block 類型.

  • enumerateObjectsWithOptions:usingBlock: 支持併發迭代或反向迭代,併發迭代時效率也非常高.

  • 對於字典而言, enumerateObjectsWithOptions:usingBlock 也是唯一的方式可以併發實現恢復Key-Value值.

示例代碼:

//定義一個可變數組
NSMutableArray *test = [NSMutableArray array];

//向數組中添加元素
for (int i= 0; i < 10000; i++) 
{
    [test addObject:@"i"];
}

//迭代數組輸出
[test enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSLog(@"%@",obj);
}];

5. block內存管理

(1)堆(Stack)和棧(Heap)

heap和stack是內存管理的兩個重要概念。這裏指的是內存的分配區域。

  1. stack的空間由操作系統進⾏行分配。
    在現代操作系統中,一個線程會分配⼀個stack. 當一個函數被調用,一個stack frame(棧幀)就會被壓到stack裏。裏包含這個函數涉及的參數,局部變量,返回地址等相關信息。當函數返回後,這個棧幀就會被銷燬。⽽這一切都是自動的,由系統幫我們進行分配與銷燬。對於程序員是透明的,我們不需要手動調度。
    .

  2. heap的空間需要手動分配。 heap與動態內存分配相關,內存可以隨時在堆中分配和銷燬。我們需要明確請求內存分配與內存銷燬。 簡單來說,就
    是malloc與free.

(2)Objective-C中的Stack和Heap

首先所有的Objective-C對象都是分配在heap的。 在OC經典的內存分配與初始化:

 NSObject *obj = [[NSObject alloc] init];

一個對象在alloc的時候,就在Heap分配了內存空間。 stack對象通常有速度的優勢,⽽且不會發生內存泄露問題。那麼爲什麼OC的對象都是分配在heap的呢? 原因在於:

  1. stack對象的⽣生命週期所導致的問題。例如一旦函數返回,則所在的stack frame就會被摧毀。那麼此時返回的對象也 會一併摧毀。這個時候我們去retain這個對象是無效的。因爲整個stack frame都已經被摧毀了。簡單⽽言就是stack 對象的⽣命週期不適合Objective-C的引用計數內存管理⽅方法。
    .
  2. stack對象不夠靈活,不具備足夠的擴展性。創建時⻓度已經是固定的,⽽stack對象的擁有者也就是所在的stack frame

我們知道block 在使用@property定義時,官方建議我們使⽤用copy修飾符

// 定義一個塊代碼的屬性,block屬性需要用 copy
@property (nonatomic, copy) void (^completion)(NSString *text);

雖然在ARC時代已經不需要再顯式聲明瞭,使用strong是沒有問題的,但是仍然建 議我們使⽤copy以顯示相關拷貝⾏爲。

(3)爲什麼要使用copy?!

其實Objective-C是有它的Stack object的。那就是block。

在Objective-C語⾔言中,⼀一共有3種類型的block:

  • _NSConcreteGlobalBlock 全局的靜態block,不會訪問任何外部變量。
  • _NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷燬。
  • _NSConcreteMallocBlock 保存在堆中的block,當引⽤用計數爲0時會被銷燬。

這⾥我們主要基於內存管理的角度對它們進行分類。

NSConcreteGlobalBlock,這種不捕捉外界變量的block是不需要內存管理的,這種block不存在於Heap或是Stack⽽是作爲代碼片段存在,類似於C函數。

NSConcreteStackBlock,需要涉及到外界變量的block在創建的時候是在stack上⾯分配空間的,也就是⼀旦所在函數返回,執行彈棧,則會被摧毀。這就導致內存管理的問題,如果我們希望保存這個block或者是返回它,如果沒有做進⼀步的copy處理,則必然會出現問題。

舉個栗子,在手動管理引⽤計數時,如果在exampleD_getBlock方法返回block 時沒有執行[[block copy] autorelease]的操作,則方法執行完畢後,block就會被銷燬, 返回block是無效的。

//定義了一個block
typedef void (^dBlock)();

dBlock exampleD_getBlock() {
    char d = 'D';
    return ^{
        printf("%c\n", d);
    };
}

void exampleD() 
{
    exampleD_getBlock();
}
NSConcreteMallocBlock,因此爲了解決block作爲Stack object的這個問題,我們最終需要把它拷⻉到堆上來。

拷貝到堆後,block的⽣命週期就與⼀般的OC對象⼀樣了,我們通過引用計數來對其進行內存管理。

現在我們知道爲麼麼要Copy了吧-_-

block在創建時是stack對象,如果我們需要在離開當前函數仍能夠使用我們創建的block。我們就需要把它 拷⻉到堆上以便進行以引用計數爲基礎的內存管理。

在ARC模式下,系統幫助我們完成了copy的⼯作。在ARC下,即使你聲明的修飾符是strong,實際上效果是與聲明爲copy一樣的。 因此在ARC情況下,創建的block仍然是NSConcreteStackBlock類型,只不過當block被引用或返回時,ARC幫助我們完成了copy和內存管理的工作。

總結

在ARC下,我們可以將block看做⼀一個正常的OC對象,與其他對象的內存管理沒什麼不同。MRC下要使用 Block_copy()和 Block_release 來管理內存。


(4)再來一個栗子

上面講到ARC下, block在被引用或返回時類型會由NSConcreteStackBlock轉換爲 NSConcreteHeapBlock,那在MRC環境下該怎麼辦呢。

block在創建的時候,它的內存是分配在棧(stack)上,而不是在堆(heap)上。

我們在viewDidLoad中創建一個_block:

- (void)viewDidLoad 
{ 
    [superviewDidLoad]; 

    int number = 1; 
    _block = ^(){ 

    NSLog(@"number %d", number); 
    }; 
} 

並且在一個按鈕的事件中調用了這個block:

- (IBAction)testDidClick:(id)sender { 
    _block(); 
} 

此時如果按了按鈕之後就會導致程序崩潰,解決這個問題的方法很簡單

在創建完block的時候需要調用 Block_copy函數。它會把block從棧上移動到堆上,那麼就可以在其他地方使用這個block了。Block_copy實際上是一個宏,如下:

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))

使用後,使用 Block_release,從堆中釋放掉

修改代碼如下:

_block = ^(){ 
    NSLog(@"number %d", number); 
}; 

_block = Block_copy(_block); 

同理,特別需要注意的地方就是在把block放到集合類當中去的時候,如果直接把生成的block放入到集合類中,是無法在其他地方使用block,必須要對block進行copy。示例如下:

[array addObject:[[^{ NSLog(@"hello!"); }  copy]  autorelease]]; 

Q:爲什麼不使用簡單的copy方法 而是 Blockcopy呢?

因爲blcok是複雜的匿名函數,簡單的copy在有些時候不能實現準確的copy,詳細就要看各自的C源碼了

6. 視圖控制器反向傳值

使用Block的地方很多,其中傳值只是其中的一小部分,下面介紹Block在兩個界面之間的傳值:

先說一下思想:

首先,創建兩個視圖控制器,在第一個視圖控制器中創建一個UILabel和一個UIButton,其中UILabel是爲了顯示第二個視圖控制器傳過來的字符串,UIButton是爲了push到第二個界面。

第二個界面的只有一個UITextField,是爲了輸入文字,當輸入文字,並且返回第一個界面的時候,當第二個視圖將要消失的時候,就將第二個界面上TextFiled中的文字傳給第一個界面,並且顯示在UILabel上。

其實核心代碼就幾行代碼:

在第二個視圖控制器的.h文件中定義聲明Block屬性

typedef void (^ReturnTextBlock)(NSString *showText);

@interface TextFieldViewController : UIViewController

@property (nonatomic, copy) ReturnTextBlock returnTextBlock;

- (void)returnText:(ReturnTextBlock)block;

@end
    第一行代碼是爲要聲明的Block重新定義了一個名字

ReturnTextBlock
    這樣,下面在使用的時候就會很方便。

    第三行是定義的一個Block屬性

    第四行是一個在第一個界面傳進來一個Block語句塊的函數,不用也可以,不過加上會減少代碼的書寫量

實現第二個視圖控制器的方法

- (void)returnText:(ReturnTextBlock)block {
  self.returnTextBlock = block;
}
- (void)viewWillDisappear:(BOOL)animated {

  if (self.returnTextBlock != nil) {
    self.returnTextBlock(self.inputTF.text);
  }
}

其中inputTF是視圖中的UITextField。

第一個方法就是定義的那個方法,把傳進來的Block語句塊保存到本類的實例變量returnTextBlock(.h中定義的屬性)中,然後尋找一個時機調用,而這個時機就是上面說到的,當視圖將要消失的時候,需要重寫:

- (void)viewWillDisappear:(BOOL)animated;
方法。

在第一個視圖中獲得第二個視圖控制器,並且用第二個視圖控制器來調用定義的屬性

如下:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
  // Get the new view controller using [segue destinationViewController].
  // Pass the selected object to the new view controller.
  TextFieldViewController *tfVC = segue.destinationViewController;

  [tfVC returnText:^(NSString *showText) {
    self.showLabel.text = showText;
  }];
}

可以看到代碼中的註釋,系統告訴我們可以用

[segue destinationViewController]

來獲得新的視圖控制器,也就是我們說的第二個視圖控制器。

這時候上面(第一步中)定義的那個方法起作用了,如果你寫一個[tfVC return Text按回車 ,系統會自動提示出來一個:

tfVC returnText:<#^(NSString *showText)block#>

的東西,我們只要在焦點上回車,就可以快速創建一個代碼塊了,大家可以試試。這在寫代碼的時候是非常方便的。

面試題:

  1. __block什麼時候用

當需要在block 中修改外部變量時使用,當需要訪問內部成員變量時。

2.在block裏面, 對數組執行添加操作, 這個數組需要聲明成 __block嗎?

當然不需要,因爲數組可以理解爲指針,在block中對數組進行添加操作,只是改變了指針指向的值,而沒有修改外部數組地址,詳細參見block訪問成員變量示例3

3.在block裏面, 對NSInteger進行修改, 這個NSInteger是否需要聲明成__blcok

必須需要,NSInteger -> typedef long NSInteger; 這貨披着OC的外衣,其實就是一個基本類型,基本類型在沒有static 等的保護下,當然需要__block

悄悄告訴你哦,block在iOS的面試中是非常重要的,如果你能把上面講解的內容理解了,那麼就仰天長嘯出門去了。 

參考

  1. http://www.cocoachina.com/ios/20120514/4247.html
    使用block開發遇到的問題
  2. http://blog.csdn.net/hherima/article/details/3858610
    這篇block 博客系列從C源碼的角度詳細分析了blcok原理

  3. http://mobile.51cto.com/iphone-446829.htm

  4. http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/
    巧叔的談Objective-C Block的實現
發佈了42 篇原創文章 · 獲贊 9 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章