OC底層-深入理解Block

基本使用

block常見的使用方式如下:

// 無參無返回值
void(^MyBlockOne)(void) = ^(void) {
    NSLog(@"無參數(shù), 無返回值");
};
MyBlockOne();

// 有參無返回值
void (^MyBlockTwo)(int a) = ^(int a) {
    NSLog(@"a = %d", a);
};
MyBlockTwo(10);

//有參有返回值
int (^MyBlockThree)(int, int) = ^(int a, int b) {
    
    NSLog(@"return %d", a + b);
    return 10;
};
MyBlockThree(10, 20);

// 無參有返回值
int (^MyBlockFour)(void) = ^(void) {
    NSLog(@"return 10");
    return 10;
};

// 聲明為某種類型
typedef int (^MyBlock) (int, int);
@property (nonatomic, copy) MyBlock block;

Block的本質(zhì) - OC對(duì)象

結(jié)論: block的內(nèi)部存在isa指針,其本質(zhì)就是封裝了函數(shù)調(diào)用函數(shù)調(diào)用環(huán)境OC對(duì)象。

證明方法一:底層結(jié)構(gòu)窺探

main函數(shù)中定義一個(gè)block,如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^(void) {
            NSLog(@"this is first block");
        };
        block();
    }
    return 0;
}

終端進(jìn)入項(xiàng)目所在目錄,通過xcrun 命令將OC代碼轉(zhuǎn)為C++代碼:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

轉(zhuǎn)換結(jié)果如下:

// 1. block 的結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block 內(nèi)部impl結(jié)構(gòu)體,存儲(chǔ)isa指針,block方法的地址。
struct __block_impl {
  void *isa;      
  int Flags;
  int Reserved;
  void *FuncPtr;  // 方法地址
};

// block 的描述信息,如:block的大小
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c441779a345740a98accb31ac195d61f~tplv-k3u1fbpfcp-zoom-1.image)static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// 2. block 的方法實(shí)現(xiàn)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_cf18a7_mi_0);
}

// 3. main方法的實(shí)現(xiàn)
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

將生成的main方法簡(jiǎn)化后得:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        block->FuncPtr(block); 
    }
    return 0;
}

看簡(jiǎn)化后的代碼,你是不是有疑問, 為什么block->FuncPtr(block) 這句話能調(diào)用成功,明明FuncPtr__block_impl 類型里的成員,為什么可以直接使用block調(diào)用

原因其實(shí)很簡(jiǎn)單,因?yàn)樵?code>block結(jié)構(gòu)體__main_block_impl_0內(nèi),__block_impl是第一個(gè)成員變量,因此block的地址和impl的地址是相同的。兩者可以進(jìn)行強(qiáng)制轉(zhuǎn)換。

根據(jù)轉(zhuǎn)換結(jié)果:

  1. OC中定義的block底層其實(shí)就是一個(gè)C++ 的結(jié)構(gòu)體__main_block_impl_0。結(jié)構(gòu)體有兩個(gè)成員變量implDesc,分別是結(jié)構(gòu)體類型 __block_impl、__main_block_desc_0
  2. 結(jié)構(gòu)體__block_impl內(nèi)包含了isa指針和指向函數(shù)實(shí)現(xiàn)的指針FuncPtr。
  3. 結(jié)構(gòu)體__main_block_desc_0內(nèi) Block_size成員存儲(chǔ)著Block的大小
image

由上可知,block內(nèi)部有一個(gè)isa指針,因此,block本質(zhì)其實(shí)就是一個(gè)OC對(duì)象

證明方法二:代碼層面

如果block是一個(gè)OC對(duì)象,那它最終肯定繼承自NSObject類(NSProxy除外),因此我們可以直接打印出block繼承鏈看一下就知道了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^(void) {
            NSLog(@"this is first block");
        };
        
        NSLog(@"class = %@", [block class]); 
        NSLog(@"superclass = %@", [block superclass]); 
        NSLog(@"superclass superclass = %@", [[block superclass] superclass]);
        NSLog(@"superclass superclass superclass = %@", [[[block superclass] superclass] superclass]);
    }
    return 0;
}
輸出結(jié)果:
2020-07-28 19:25:24.475317+0800 LearningBlock[39445:591948] class = __NSGlobalBlock__
2020-07-28 19:25:24.475707+0800 LearningBlock[39445:591948] superclass = __NSGlobalBlock
2020-07-28 19:25:24.475762+0800 LearningBlock[39445:591948] superclass superclass= NSBlock
2020-07-28 19:25:24.475808+0800 LearningBlock[39445:591948] superclass superclass superclass= NSObject

block 的繼承鏈: __NSGlobalBlock -> NSBlock -> NSObject

可以看出block最終繼承自NSObject的。isa指針其實(shí)就是由NSObject來的。 因此block本質(zhì)就是一個(gè)OC對(duì)象。

Block 的變量捕獲(Capture)

為了保證block內(nèi)部能夠正常訪問外部的值,block有個(gè)變量捕獲的機(jī)制。下面來一起來探索一下block的變量捕獲機(jī)制。

代碼:

int a = 10;   // 全局變量, 程序運(yùn)行過程一直存在內(nèi)存。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int b = 20;       // 局部變量,默認(rèn)是auto修飾,一般可以不寫auto,所在作用域結(jié)束后會(huì)被銷毀。
        static int c = 30;     // 靜態(tài)變量,程序運(yùn)行過程中一直存在內(nèi)存。
        
        void(^block)(void) = ^(void) {
            NSLog(@"a = %d, b = %d, c = %d", a, b, c);
        };
        
        // 觀察調(diào)用block時(shí),a,b,c 的值是多少呢?
        a = 11;
        b = 21;
        c = 31;   
        
        block();  // 調(diào)用block
    }
    return 0;
}

打印輸出:
2020-07-28 19:43:41.729849+0800 LearningBlock[39648:603167] a = 11, b = 20, c = 31

由打印結(jié)果來看,b沒有改變, 而ac 的值都發(fā)生了變化。 原因是什么呢?下面一起看下

運(yùn)行下面的轉(zhuǎn)換語句,將當(dāng)前的OC代碼轉(zhuǎn)換C++, 方便我們看到更本質(zhì)的東西:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

轉(zhuǎn)換后的代碼如下:


int a = 10;  // 全局變量

struct __main_block_impl_0 {   // block的結(jié)構(gòu)體
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;    // 新生成的成員變量b,用于存放外部局部變量b的值
  int *c;   // 新生成的成員變量c,指針類型, 用于存儲(chǔ)外部靜態(tài)局部變量c的引用。
  
  // 構(gòu)造函數(shù)
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int *_c, int flags=0) : b(_b), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int b = __cself->b;  // 通過cself進(jìn)行訪問內(nèi)部的成員變量b
  int *c = __cself->c;   // 通過cself獲取靜態(tài)局部變量c的引用
  
  // 直接訪問全局變量a
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_256a11_mi_0, a , b, (*c)); 
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int b = 20;
        static int c = 30;

        void (*Myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b, &c));

        a = 11;
        b = 21;
        c = 31;

        ((void (*)(__block_impl *))((__block_impl *)Myblock)->FuncPtr)((__block_impl *)Myblock);

    }
    return 0;
}

有上可以觀察到:

block結(jié)構(gòu)體__main_block_impl_0內(nèi)部生成了新的成員變量b*c, 分別用于存放外部傳進(jìn)來的bc的地址,這就是我們所說的捕獲。而對(duì)于全局變量a則沒有進(jìn)行捕獲,在使用時(shí)是直接訪問的。

由此可得出:

  1. block 內(nèi)部對(duì)autostatic類型的變量進(jìn)行了捕獲,但是不會(huì)捕獲全局變量。
  2. 雖然block對(duì)autostatic變量都進(jìn)行了捕獲,但是不同的是,auto 變量是值傳遞,而static變量則是地址傳遞。因此當(dāng)外部的static變量值發(fā)生變化時(shí),block內(nèi)部也跟著會(huì)改變,而外部的auto變量值發(fā)生變化,block內(nèi)部的值不會(huì)發(fā)生改變。

[圖片上傳失敗...(image-3381f6-1601373145542)]

思考??

相信你會(huì)有這樣的疑問,為什么block會(huì)捕獲autostatic類型的局部變量,而不會(huì)捕獲全局變量呢?(全局變量表示不服,block你怎么搞區(qū)別對(duì)待呢?), 那么block的變量捕獲究竟有什么講究呢?

其實(shí)是這樣的

  • 首先對(duì)于auto類型的局部變量,其生命周期太短了,離開了其所在的作用域后,auto變量的內(nèi)存就會(huì)被系統(tǒng)回收了,而block的調(diào)用時(shí)機(jī)是不確定的,如果block不對(duì)它進(jìn)行捕獲,那么當(dāng)block運(yùn)行時(shí)再訪問auto變量時(shí),因?yàn)樽兞恳驯幌到y(tǒng)回收,那么就會(huì)出現(xiàn)壞內(nèi)存訪問或者得到不正確的值。
  • 對(duì)于局部的static變量,因?yàn)槠涑跏蓟?,在程序運(yùn)行過程中就會(huì)一直存在內(nèi)存中,而不會(huì)被系統(tǒng)回收,但是由于因?yàn)槭蔷植孔兞康脑颍湓L問的作用域有限,block想訪問它就要知道去哪里訪問,所以block才需要對(duì)其進(jìn)行捕獲,但與auto變量不同的是,block只需捕獲static變量的地址即可。
  • 對(duì)于全局變量,因?yàn)槠湓诔绦蜻\(yùn)行過程一直都在,并且其訪問作用域也是全局的,所以block可以直接找到它,而不需要對(duì)它進(jìn)行捕獲。

所以,block的變量捕獲原則其實(shí)很簡(jiǎn)單,如果block內(nèi)部能直接訪問到的變量,那就不捕獲(捕獲也是浪費(fèi)空間), 如果block內(nèi)部不能直接訪問到變量,那就需要進(jìn)行捕獲(不捕獲就沒得用)。

Block的類型

block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型.

  • NSGlobalBlock
  • NSStackBlock
  • NSMallocBlock

為了準(zhǔn)確分析block的類型,先把ARC給關(guān)閉,使用MRC。
[圖片上傳失敗...(image-dd67c0-1601373145542)]

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int age = 10;       // 局部變量,默認(rèn)是auto,一般可以不寫auto,所處作用域結(jié)束后會(huì)被銷毀。
        static int height = 20;  // 靜態(tài)變量,程序運(yùn)行過程中一直存在內(nèi)存。

        void(^block1)(void) = ^(void) {
            NSLog(@"1111111111");  // 沒有捕獲了auto變量
        };
        
        void(^block2)(void) = ^(void) {
            NSLog(@"age = %d", age);   // 捕獲了auto變量
        };
        
        void(^block3)(void) = ^(void) {
            NSLog(@"height = %d", height);   // 捕獲了static變量
        };
        
        NSLog(@"block1 class: %@", [block1 class]);             // __NSGlobalBlock__
        NSLog(@"block2 class: %@", [block2 class]);             // __NSStackBlock__
        NSLog(@"block2 copy class: %@", [[block2 copy] class]); //__NSMallocBlock__
        NSLog(@"block3 class: %@", [block3 class]);             //__NSGlobalBlock__
    }
    return 0;
}

// 輸出結(jié)果:
2020-07-28 20:41:43.283331+0800 LearningBlock[40390:637401] block1 class: __NSGlobalBlock__
2020-07-28 20:41:43.283755+0800 LearningBlock[40390:637401] block2 class: __NSStackBlock__
2020-07-28 20:41:43.283877+0800 LearningBlock[40390:637401] block2 copy class: __NSMallocBlock__
2020-07-28 20:41:43.283924+0800 LearningBlock[40390:637401] block3 class: __NSGlobalBlock__

由上可知:

  1. block類型取值如下:

    • 沒有捕獲auto變量,那么block的為__NSGlobalBlock__類型。
    • 捕獲了auto變量,那么block__NSStackBlock__類型。
    • 對(duì)__NSStackBlock__類型的block進(jìn)行copy操作,則block就會(huì)變成__NSMallocBlock__ 類型.
      image
  2. block 這幾種類型的主要區(qū)別是:在內(nèi)存中的存放區(qū)域不同。(即生命的周期不同)

    • __NSGlobalBlock__ 存在數(shù)據(jù)段。
    • __NSStackBlock__ 存放在??臻g。
    • __NSMallocBlock__ 存放在堆空間。
image

檢驗(yàn)題:

新建一個(gè)Person類, 如下:

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

- (void)test;

@end

@implementation Person

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"person name = %@", _name);
    };
}
@end

問題: 在Person.mtest方法中的block對(duì)self有沒有進(jìn)行捕獲呢?

答案是有,block會(huì)捕獲self. 分析如下:

首先將Person.m 通過xcrun命令轉(zhuǎn)換為C++, 得到如下內(nèi)容:

//test 方法內(nèi)的block方法
struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//test 方法
static void _I_Person_test(Person * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_Person_14871d_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));
}

觀察轉(zhuǎn)換后的代碼可以看到:

  1. 我們平常寫的OC方法,其實(shí)默認(rèn)就有隱藏的兩個(gè)參數(shù),(Person *self, SEL _cmd), 分別是方法的調(diào)用者 self方法選擇器 sel。
  2. 方法的參數(shù)一般是局部變量,block會(huì)對(duì)局部變量進(jìn)行捕獲的。

Block的copy操作

我們?nèi)粘J褂玫腷lock一般是__NSMallocBlock__類型的,原因有如下:

  • 對(duì)于__NSGlobalBlock__類型的block, 因?yàn)闆]有捕獲auto變量, 所以正常一般都是直接使用函數(shù)實(shí)現(xiàn)。
  • 對(duì)于__NSStackBlock__類型的block, 因?yàn)槠浯娣旁跅I?,其?nèi)部使用變量容易被系統(tǒng)回收掉,從而導(dǎo)致一些異常的情況。比如下面:(要先將項(xiàng)目切成MRC,因?yàn)锳RC下編譯器會(huì)根據(jù)情況做copy操作,會(huì)影響分析)
typedef void (^MJBlock)(void);
MJBlock block;
void test() {
    int a = 10;  // test方法結(jié)束后,a的內(nèi)存就被回收了。
    block = ^(void) {
        NSLog(@"a = %d", a);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();   // block里打印的是被回收了的a
    }
    return 0;
}
輸出結(jié)果:
2020-09-27 10:05:28.616920+0800 Interview01-block的copy[7134:29679] a = -272632776
  • 對(duì)于__NSMallocBlock__類型的block, 因?yàn)樗谴鎯?chǔ)在堆上,所以就不存在__NSStackBlock__類型block的問題。

上面演示的是在MRC環(huán)境下的, 那么在ARC環(huán)境下又是如何的呢?

ARC環(huán)境下,編譯器會(huì)自動(dòng)根據(jù)情況將棧上的block復(fù)制到堆上。比如一下情況:

  • block作為函數(shù)返回值時(shí)。
  • block賦值給__strong指針時(shí)。
  • block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)。
  • block作為GCD API方法參數(shù)時(shí)。
typedef void (^MJBlock)(void);
MJBlock myblock()
{
    int a = 10;
    return ^{
        NSLog(@"--------- %d", a);   // 1. 作為方法返回值時(shí)。會(huì)自動(dòng)copy
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        MJBlock block = ^{    // 2.賦值給strong指針時(shí),會(huì)自動(dòng)copy
            NSLog(@"---------%d", age);
        };
        
        NSArray *arr = @[@10, @20];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 3. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)。會(huì)自動(dòng)copy
        }];

        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            // 4. block作為GCD API方法參數(shù)時(shí)。會(huì)自動(dòng)copy
        });
    }
    return 0;
}

根據(jù)上面的情況,在MRCARCblock屬性的寫法可以有差異:

MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);  // 賦值時(shí)會(huì)自動(dòng)copy到堆上

ARC下block屬性的建議寫法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

對(duì)象類型的auto變量

基本數(shù)據(jù)類型auto變量我們已經(jīng)分析了,那么對(duì)象類型auto變量是不是和基本數(shù)據(jù)類型的一樣還是有什么特別之處呢?下面我們一起來分析下:(記得先將工程切回ARC模式)

如下代碼:

@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@end

@implementation LCPerson
- (void)dealloc {
    NSLog(@"%s", __func__);   // 銷毀代碼
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
        }
        
        NSLog(@"22222222");
    }
    return 0;
}

// 輸出結(jié)果:
2020-09-27 10:36:43.856070+0800 LearningBlock[16016:56873] 11111111
2020-09-27 10:36:43.856442+0800 LearningBlock[16016:56873] -[LCPerson dealloc]
2020-09-27 10:36:43.856474+0800 LearningBlock[16016:56873] 22222222

我們定義了一個(gè)LCPerson類,在main.m中做測(cè)試,由輸出結(jié)果可以看出,person對(duì)象的釋放是在111111122222222之間, 這我們應(yīng)該都可以理解。(局部作用域)

我們繼續(xù)~

加入Block之后,我們?cè)儆^察一下。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            block = ^(void){
                NSLog(@"person age = %d", person.age);
            };
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}

輸出結(jié)果:

2020-09-27 10:52:27.578241+0800 LearningBlock[20478:70040] 11111111
2020-09-27 10:52:27.578627+0800 LearningBlock[20478:70040] 22222222
2020-09-27 10:52:27.578688+0800 LearningBlock[20478:70040] -[LCPerson dealloc]
2020-09-27 10:52:27.578729+0800 LearningBlock[20478:70040] 3333333

根據(jù)結(jié)果,我們可以發(fā)現(xiàn)加入了block之后,person的銷毀是在222222之后發(fā)生的,即person所在的作用域結(jié)束后,person對(duì)象沒有立即釋放。 那么block究竟對(duì)person干了什么,導(dǎo)致person對(duì)象沒能及時(shí)釋放呢? 為了分析,我們將上面的代碼先簡(jiǎn)化一下。簡(jiǎn)化如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        void (^block)(void) = ^(void){
            NSLog(@"person age = %d", person.age);
        };
        
        block();
    }
    return 0;
}

將上面OC代碼轉(zhuǎn)換為C++代碼:(支持ARC、指定運(yùn)行時(shí)系統(tǒng)版本)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

轉(zhuǎn)換后的C++代碼如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  LCPerson *__strong person; // strong類型的指針
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LCPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  LCPerson *__strong person = __cself->person; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_5882d6_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        LCPerson *person = ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344)); 

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

通過觀察可以發(fā)現(xiàn),block內(nèi)部對(duì)person進(jìn)行了捕獲。并且與捕獲基本數(shù)據(jù)類型的auto變量不同的是,捕獲對(duì)象類型時(shí)__main_block_desc_0結(jié)構(gòu)體多了兩個(gè)函數(shù),分別是copydispose,這兩個(gè)函數(shù)與被捕獲對(duì)象的引用計(jì)數(shù)的處理有關(guān)。

  • 當(dāng)block上拷貝到上時(shí),copy函數(shù)被調(diào)用,接著它會(huì)調(diào)用_Block_object_assign函數(shù),處理被捕獲對(duì)象的引用計(jì)數(shù),如果捕獲變量時(shí)是使用__strong修飾,那么對(duì)象的引用計(jì)數(shù)就會(huì)+1. 如果捕獲時(shí)是__weak修飾,則引用計(jì)數(shù)不變。(下面會(huì)驗(yàn)證)
  • 當(dāng)block被回收,即釋放時(shí),dispose函數(shù)被調(diào)用,接著它會(huì)調(diào)用_Block_object_dispose函數(shù),如果捕獲變量時(shí)是使用__strong修飾,那么對(duì)象的引用計(jì)數(shù)就會(huì)-1. 如果捕獲變量時(shí)是__weak修飾,則引用計(jì)數(shù)不變。(下面會(huì)驗(yàn)證)

我們知道,在ARC環(huán)境下,將block賦值給__strong指針,block會(huì)自動(dòng)調(diào)用copy函數(shù)。所以 person對(duì)象離開了局部作用域后沒有釋放的原因就很明確了,是因?yàn)?code>block調(diào)用copy函數(shù)時(shí),將person對(duì)象的引用計(jì)數(shù)增加了1,所以當(dāng)局部作用域結(jié)束時(shí),person對(duì)象的引用計(jì)數(shù)并不為0,因此不會(huì)釋放。 而當(dāng)block的作用域結(jié)束,block調(diào)用dispose函數(shù),將person的引用計(jì)數(shù)減為0,然后person才會(huì)釋放。

如上面所說,那如果是在MRC環(huán)境下,person對(duì)象離開局部作用域后就會(huì)銷毀了, 因?yàn)樵?code>MRC環(huán)境下,將block賦值給__strong指針是不會(huì)觸發(fā)copy函數(shù)的,所以person對(duì)象應(yīng)該可以正常釋放。

驗(yàn)證一: 將工程切換到MRC模式下,測(cè)試剛才的代碼,如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            block = ^(void){
                NSLog(@"person age = %d", person.age);
            };
            
            [person release]; // MRC下需要手動(dòng)管理內(nèi)存
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}

// 輸出結(jié)果:
2020-09-27 11:39:05.493388+0800 LearningBlock[33422:105156] 11111111
2020-09-27 11:39:05.493800+0800 LearningBlock[33422:105156] -[LCPerson dealloc]
2020-09-27 11:39:05.493833+0800 LearningBlock[33422:105156] 22222222
2020-09-27 11:39:05.493857+0800 LearningBlock[33422:105156] 3333333

觀察輸出結(jié)果,和預(yù)料中的一樣。person對(duì)象離開局部作用域后正常釋放。

驗(yàn)證二:weak修飾的對(duì)象類型的auto變量. (記得切回ARC環(huán)境)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            // 弱指針
            __weak LCPerson *weakPerson = person;
            block = ^(void){
                NSLog(@"person age = %d", weakPerson.age);
            };
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}
// 輸出結(jié)果:
2020-09-27 12:00:20.461929+0800 LearningBlock[39325:122309] 11111111
2020-09-27 12:00:20.462321+0800 LearningBlock[39325:122309] -[LCPerson dealloc]
2020-09-27 12:00:20.462361+0800 LearningBlock[39325:122309] 22222222
2020-09-27 12:00:20.462391+0800 LearningBlock[39325:122309] 3333333

觀察輸出結(jié)果,和預(yù)料中的一樣。person對(duì)象離開局部作用域后正常釋放。

總結(jié):

  • 當(dāng)block內(nèi)部訪問了對(duì)象類型的auto變量時(shí)

    • 如果block是在棧上,將不會(huì)對(duì)auto變量產(chǎn)生強(qiáng)引用
  • 如果block被拷貝到堆上

    • 會(huì)調(diào)用block內(nèi)部的copy函數(shù)
    • copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù)
    • _Block_object_assign函數(shù)會(huì)根據(jù)auto變量的修飾符(__strong、__weak__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用
  • 如果block從堆上移除

    • 會(huì)調(diào)用block內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù)
    • _Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的auto變量(release

__block修飾符

  • __block可以用于解決block內(nèi)部無法修改auto變量值的問題
  • __block不能修飾全局變量靜態(tài)變量static
  • 編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象.

下面一起驗(yàn)證一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        void (^block)(void) = ^{
            a = 20;
            NSLog(@"a = %d", a);
        };
        block();
    }
    return 0;
}
// 輸出結(jié)果:
a = 20

將上面OC代碼轉(zhuǎn)換為C++代碼:(支持ARC、指定運(yùn)行時(shí)系統(tǒng)版本)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

得到轉(zhuǎn)換后結(jié)果:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref   這就捕獲到的a的引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 20;     // 修改值a的值。
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_ca9eb0_mi_0, (a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};  // 這就是__block 修飾的a變量。

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));  // 傳入的是a變量的地址。

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

由上面可以看到,OC代碼 __block int a = 10 轉(zhuǎn)換為C++之后變?yōu)榱耍?/p>

    __Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10};

__Block_byref_a_0是一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
}

所以在OC中用__block修飾一個(gè)變量, 編譯器會(huì)自動(dòng)生成一個(gè)全新的OC對(duì)象。

image

__block的內(nèi)存管理

__block 的在block中的內(nèi)存管理和對(duì)象類型的auto變量類似(但也有區(qū)別)。

  • 當(dāng)block在棧上時(shí),并不會(huì)對(duì)__block變量產(chǎn)生強(qiáng)引用

  • 當(dāng)blockcopy到堆時(shí)

    • 會(huì)調(diào)用block內(nèi)部的copy函數(shù)
    • copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù)
    • _Block_object_assign函數(shù)會(huì)對(duì)__block變量形成強(qiáng)引用(retain)。(這點(diǎn)就是和對(duì)象類型的auto變量有區(qū)別的地方,對(duì)于對(duì)象類型的auto變量, _Block_object_assign函數(shù)會(huì)根據(jù)auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作, 而__block則是直接強(qiáng)引用 )
      image
  • 當(dāng)block從堆中移除時(shí)

    • 會(huì)調(diào)用block內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù)
    • _Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的__block變量(release
image

__block的__forwarding指針

image

被__block修飾的對(duì)象類型

通過上面我們知道了用__block修飾的基本數(shù)據(jù)類型的處理。那用__block修飾的對(duì)象類型的處理是不是一樣的呢? 下面我們一起看下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        void(^block)(void) = ^(void) {
            NSLog(@"person age %d", person.age);
        };
        block();
    }
    return 0;
}

通過xcrun命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

轉(zhuǎn)換成C++后,得到結(jié)果如下:

struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);  // 管理person的內(nèi)存
 void (*__Block_byref_id_object_dispose)(void*);      // 管理person的內(nèi)存
 LCPerson *__strong person;   //arc環(huán)境下, copy 和 dispose函數(shù),會(huì)根據(jù)person的修飾類型(__strong、__weak)來對(duì)person做相應(yīng)的內(nèi)存管理。
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_person_0 *person = __cself->person; // bound by ref  // 這里就是強(qiáng)引用

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_213c56_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(person->__forwarding->person), sel_registerName("age")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_person_0 person = {(void*)0,(__Block_byref_person_0 *)&person, 33554432, sizeof(__Block_byref_person_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"))};
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(person.__forwarding->person), sel_registerName("setAge:"), 10);

        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

當(dāng)block拷貝到上時(shí),會(huì)調(diào)用blockcopy方法,同時(shí)還會(huì)調(diào)用__Block_byref_person_0結(jié)構(gòu)體里的__Block_byref_id_object_copy方法,__Block_byref_id_object_copy內(nèi)部會(huì)調(diào)用_Block_object_assign方法,處理結(jié)構(gòu)體__Block_byref_person_0內(nèi)部的person指針?biāo)笇?duì)象的引用計(jì)數(shù)。

總結(jié)如下:

  • 當(dāng)__block變量在棧上時(shí),不會(huì)對(duì)指向的對(duì)象產(chǎn)生強(qiáng)引用

  • 當(dāng)__block變量被copy到堆時(shí)

    • 會(huì)調(diào)用__block變量內(nèi)部的copy函數(shù)
    • copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù)
    • _Block_object_assign函數(shù)會(huì)根據(jù)所指向?qū)ο蟮男揎椃?code>__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于ARC時(shí)會(huì)retain,MRC時(shí)不會(huì)retain
  • 如果__block變量從堆上移除

    • 會(huì)調(diào)用__block變量內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù)
    • _Block_object_dispose函數(shù)會(huì)自動(dòng)釋放指向的對(duì)象(release

對(duì)象類型的 auto變量 和 __block變量處理的異同:

  • 當(dāng)block在棧上時(shí),對(duì)它們都不會(huì)產(chǎn)生強(qiáng)引用

  • 當(dāng)block拷貝到堆上時(shí),都會(huì)通過copy函數(shù)來處理它們

    • __block變量(假設(shè)變量名叫做a)
    • _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
  • 對(duì)象類型的auto變量(假設(shè)變量名叫做p)

    • _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
  • 當(dāng)block從堆上移除時(shí),都會(huì)通過dispose函數(shù)來釋放它們

    • __block變量(假設(shè)變量名叫做a)
    • _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
  • 對(duì)象類型的auto變量(假設(shè)變量名叫做p)

    • _Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);

循環(huán)引用問題

在開發(fā)過程中我們經(jīng)常會(huì)遇到block循環(huán)引用的問題, 如下:

typedef void (^MyBlock)(void);

@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block;
@end

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"person age %d", person.age);
        };
        
        NSLog(@"211212121122");
    }
    return 0;
}

// 輸出結(jié)果:
2020-09-28 20:01:48.358822+0800 LearningBlock[41115:298402] 211212121122

由打印結(jié)果可以看出,person并沒有釋放(沒有調(diào)用person的dealloc方法)。那是什么原因?qū)е碌哪兀渴茄h(huán)引用。 下面我們來分析一下:

  • @property (nonatomic, copy) MyBlock block;從這句話可以看出,person 強(qiáng)引用著block.
  • block內(nèi)部訪問了person對(duì)象的age屬性,根據(jù)上面所學(xué),我們知道block會(huì)對(duì)person進(jìn)行捕獲,并且在arc環(huán)境下,block賦值給__strong指針時(shí)會(huì)自動(dòng)調(diào)用copy方法,將block從??截惖蕉焉? 這樣會(huì)導(dǎo)致person的引用計(jì)數(shù)加1,即block強(qiáng)引用著person。
image

所以personblock相互強(qiáng)引用著,出現(xiàn)了循環(huán)引用,所以person對(duì)象不會(huì)釋放。

那么該如何解決呢? 下面說下在ARC環(huán)境和MRC環(huán)境分別如何處理?

解決循環(huán)引用問題 - ARC

ARC環(huán)境下,我們可以通過使用關(guān)鍵字__weak__unsafe_unretained來解決。如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;

        __weak LCPerson *weakPerson = person;
        // 或者 __unsafe_unretained LCPerson *weakPerson = person;
        person.block = ^{
            NSLog(@"person age %d", weakPerson.age);
        };
        
        NSLog(@"211212121122");
    }
    return 0;
}
// 打印結(jié)果:
2020-09-28 20:30:19.659679+0800 LearningBlock[41212:307877] 211212121122
2020-09-28 20:30:19.660256+0800 LearningBlock[41212:307877] -[LCPerson dealloc]

示意圖如下:


image

還可以使用__block解決, 如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"person age %d", person.age);
            person = nil;
        };
        
        person.block();  // 必須調(diào)用
        NSLog(@"211212121122");
    }
    return 0;
}

// 打印結(jié)果:
2020-09-28 20:35:32.531704+0800 LearningBlock[41256:310297] person age 10
2020-09-28 20:35:32.532221+0800 LearningBlock[41256:310297] -[LCPerson dealloc]
2020-09-28 20:35:32.532310+0800 LearningBlock[41256:310297] 211212121122

使用__block解決,必須調(diào)用block,不然無法將循環(huán)引用打破。

image

疑問: __weak__unsafe_unretained關(guān)鍵字有什么區(qū)別呢?

使用__weak__unsafe_unretained關(guān)鍵字都能達(dá)到弱引用的效果。這兩者主要的區(qū)別在于,使用__weak關(guān)鍵字修飾的指針,在所指的對(duì)象銷毀時(shí),指針存儲(chǔ)的地址會(huì)被清空(即置為nil), 而__unsafe_unretained則不會(huì)。

解決循環(huán)引用問題 - MRC

  • MRC環(huán)境是沒有__weak關(guān)鍵字的,所以可以使用__unsafe_unretained關(guān)鍵字解決。(與ARC差不多,這里就不演示了)
  • 同樣也可以是__block關(guān)鍵字解決。如下:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"person age %d", person.age);
            person = nil;
        };
        
        [person release];  // MRC需要手動(dòng)添加內(nèi)存管理代碼
        NSLog(@"211212121122");
    }
    return 0;
}

ARC不同的是,MRC下使用__block解決循環(huán)引用問題,不要求一定要調(diào)用block。原因上面__block修飾的對(duì)象類型里有說到:

_Block_object_assign函數(shù)會(huì)根據(jù)所指向?qū)ο蟮男揎椃?code>__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于ARC時(shí)會(huì)retain,MRC時(shí)不會(huì)retain

后話

這篇文章有點(diǎn)亂,還有待改進(jìn)。寫博客真的費(fèi)時(shí)間,不過能加深印象,也不錯(cuò)。

參考

  1. MJ 底層原理
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容