autorelease基本用法

1,對(duì)象執(zhí)行autorelease方法時(shí)會(huì)將對(duì)象添加到自動(dòng)釋放池中

2,當(dāng)自動(dòng)釋放池銷毀時(shí)自動(dòng)釋放池中所有對(duì)象作release操作

3,對(duì)象執(zhí)行autorelease方法后自身引用計(jì)數(shù)器不會(huì)改變,而且會(huì)返回對(duì)象本身

autoreleased 對(duì)象什么時(shí)候釋放

autorelease 本質(zhì)上就是延遲調(diào)用 release ,那 autoreleased 對(duì)象究竟會(huì)在什么時(shí)候釋放呢?為了弄清楚這個(gè)問題,我們先來做一個(gè)小實(shí)驗(yàn)。這個(gè)小實(shí)驗(yàn)分 3 種場景進(jìn)行,請(qǐng)你先自行思考在每種場景下的 console 輸出,以加深理解。注:本實(shí)驗(yàn)的源碼可以在這里AutoreleasePool找到。


__weak?NSString?*string_weak_?=?nil;

-?(void)viewDidLoad?{

[superviewDidLoad];

//?場景?1

NSString?*string?=?[NSString?stringWithFormat:@"leichunfeng"];

string_weak_?=?string;

//?場景?2

//????@autoreleasepool?{

//????????NSString?*string?=?[NSString?stringWithFormat:@"leichunfeng"];

//????????string_weak_?=?string;

//????}

//?場景?3

//????NSString?*string?=?nil;

//????@autoreleasepool?{

//????????string?=?[NSString?stringWithFormat:@"leichunfeng"];

//????????string_weak_?=?string;

//????}

NSLog(@"string:?%@",?string_weak_);

}

-?(void)viewWillAppear:(BOOL)animated?{

[superviewWillAppear:animated];

NSLog(@"string:?%@",?string_weak_);

}

-?(void)viewDidAppear:(BOOL)animated?{

[superviewDidAppear:animated];

NSLog(@"string:?%@",?string_weak_);

}

思考得怎么樣了?相信在你心中已經(jīng)有答案了。那么讓我們一起來看看 console 輸出:


//?場景?1

2015-05-30?10:32:20.837?AutoreleasePool[33876:1448343]?string:?leichunfeng

2015-05-30?10:32:20.838?AutoreleasePool[33876:1448343]?string:?leichunfeng

2015-05-30?10:32:20.845?AutoreleasePool[33876:1448343]?string:?(null)

//?場景?2

2015-05-30?10:32:50.548?AutoreleasePool[33915:1448912]?string:?(null)

2015-05-30?10:32:50.549?AutoreleasePool[33915:1448912]?string:?(null)

2015-05-30?10:32:50.555?AutoreleasePool[33915:1448912]?string:?(null)

//?場景?3

2015-05-30?10:33:07.075?AutoreleasePool[33984:1449418]?string:?leichunfeng

2015-05-30?10:33:07.075?AutoreleasePool[33984:1449418]?string:?(null)

2015-05-30?10:33:07.094?AutoreleasePool[33984:1449418]?string:?(null)

跟你預(yù)想的結(jié)果有出入嗎?Any way ,我們一起來分析下為什么會(huì)得到這樣的結(jié)果。

分析:3 種場景下,我們都通過 [NSString stringWithFormat:@"leichunfeng"] 創(chuàng)建了一個(gè) autoreleased 對(duì)象,這是我們實(shí)驗(yàn)的前提。并且,為了能夠在 viewWillAppear 和 viewDidAppear 中繼續(xù)訪問這個(gè)對(duì)象,我們使用了一個(gè)全局的 __weak 變量 string_weak_ 來指向它。因?yàn)?__weak 變量有一個(gè)特性就是它不會(huì)影響所指向?qū)ο蟮纳芷?,這里我們正是利用了這個(gè)特性。

場景 1:當(dāng)使用 [NSString stringWithFormat:@"leichunfeng"] 創(chuàng)建一個(gè)對(duì)象時(shí),這個(gè)對(duì)象的引用計(jì)數(shù)為 1 ,并且這個(gè)對(duì)象被系統(tǒng)自動(dòng)添加到了當(dāng)前的 autoreleasepool 中。當(dāng)使用局部變量 string 指向這個(gè)對(duì)象時(shí),這個(gè)對(duì)象的引用計(jì)數(shù) +1 ,變成了 2 。因?yàn)樵?ARC 下 NSString *string 本質(zhì)上就是 __strong NSString *string 。所以在 viewDidLoad 方法返回前,這個(gè)對(duì)象是一直存在的,且引用計(jì)數(shù)為 2 。而當(dāng) viewDidLoad 方法返回時(shí),局部變量 string 被回收,指向了 nil 。因此,其所指向?qū)ο蟮囊糜?jì)數(shù) -1 ,變成了 1 。

而在 viewWillAppear 方法中,我們?nèi)匀豢梢源蛴〕鲞@個(gè)對(duì)象的值,說明這個(gè)對(duì)象并沒有被釋放。咦,這不科學(xué)吧?我讀書少,你表騙我。不是一直都說當(dāng)函數(shù)返回的時(shí)候,函數(shù)內(nèi)部產(chǎn)生的對(duì)象就會(huì)被釋放的嗎?如果你這樣想的話,那我只能說:騷年你太年經(jīng)了。開個(gè)玩笑,我們繼續(xù)。前面我們提到了,這個(gè)對(duì)象是一個(gè) autoreleased 對(duì)象,autoreleased 對(duì)象是被添加到了當(dāng)前最近的 autoreleasepool 中的,只有當(dāng)這個(gè) autoreleasepool 自身 drain 的時(shí)候,autoreleasepool 中的 autoreleased 對(duì)象才會(huì)被 release 。

另外,我們注意到當(dāng)在 viewDidAppear 中再打印這個(gè)對(duì)象的時(shí)候,對(duì)象的值變成了 nil ,說明此時(shí)對(duì)象已經(jīng)被釋放了。因此,我們可以大膽地猜測一下,這個(gè)對(duì)象一定是在 viewWillAppear 和 viewDidAppear 方法之間的某個(gè)時(shí)候被釋放了,并且是由于它所在的 autoreleasepool 被 drain 的時(shí)候釋放的。

你說什么就是什么咯?有本事你就證明給我看你媽是你媽。額,這個(gè)我真證明不了,不過上面的猜測我還是可以證明的,不信,你看!

在開始前,我先簡單地說明一下原理,我們可以通過使用 lldb 的 watchpoint 命令來設(shè)置觀察點(diǎn),觀察全局變量 string_weak_ 的值的變化,string_weak_ 變量保存的就是我們創(chuàng)建的 autoreleased 對(duì)象的地址。在這里,我們?cè)俅卫昧?__weak 變量的另外一個(gè)特性,就是當(dāng)它所指向的對(duì)象被釋放時(shí),__weak 變量的值會(huì)被置為 nil 。了解了基本原理后,我們開始驗(yàn)證上面的猜測。

我們先在第 35 行打一個(gè)斷點(diǎn),當(dāng)程序運(yùn)行到這個(gè)斷點(diǎn)時(shí),我們通過 lldb 命令 watchpoint set v string_weak_ 設(shè)置觀察點(diǎn),觀察 string_weak_ 變量的值的變化。如下圖所示,我們將在 console 中看到類似的輸出,說明我們已經(jīng)成功地設(shè)置了一個(gè)觀察點(diǎn):

設(shè)置好觀察點(diǎn)后,點(diǎn)擊 Continue program execution 按鈕,繼續(xù)運(yùn)行程序,我們將看到如下圖所示的界面:

我們先看console中的輸出,注意到 string_weak_ 變量的值由 0x00007f9b886567d0 變成了 0x0000000000000000 ,也就是 nil 。說明此時(shí)它所指向的對(duì)象被釋放了。另外,我們也可以注意到一個(gè)細(xì)節(jié),那就是 console 中打印了兩次對(duì)象的值,說明此時(shí) viewWillAppear 也已經(jīng)被調(diào)用了,而 viewDidAppear 還沒有被調(diào)用。

接著,我們來看看左側(cè)的線程堆棧。我們看到了一個(gè)非常敏感的方法調(diào)用 -[NSAutoreleasePool release] ,這個(gè)方法最終通過調(diào)用 AutoreleasePoolPage::pop(void *) 函數(shù)來負(fù)責(zé)對(duì) autoreleasepool 中的 autoreleased 對(duì)象執(zhí)行 release 操作。結(jié)合前面的分析,我們知道在 viewDidLoad 中創(chuàng)建的 autoreleased 對(duì)象在方法返回后引用計(jì)數(shù)為 1 ,所以經(jīng)過這里的 release 操作后,這個(gè)對(duì)象的引用計(jì)數(shù) -1 ,變成了 0 ,該 autoreleased 對(duì)象最終被釋放,猜測得證。

另外,值得一提的是,我們?cè)诖a中并沒有手動(dòng)添加 autoreleasepool ,那這個(gè) autoreleasepool 究竟是哪里來的呢?看完后面的章節(jié)你就明白了。

場景 2:同理,當(dāng)通過 [NSString stringWithFormat:@"leichunfeng"] 創(chuàng)建一個(gè)對(duì)象時(shí),這個(gè)對(duì)象的引用計(jì)數(shù)為 1 。而當(dāng)使用局部變量 string 指向這個(gè)對(duì)象時(shí),這個(gè)對(duì)象的引用計(jì)數(shù) +1 ,變成了 2 。而出了當(dāng)前作用域時(shí),局部變量 string 變成了 nil ,所以其所指向?qū)ο蟮囊糜?jì)數(shù)變成 1 。另外,我們知道當(dāng)出了 @autoreleasepool {} 的作用域時(shí),當(dāng)前 autoreleasepool 被 drain ,其中的 autoreleased 對(duì)象被 release 。所以這個(gè)對(duì)象的引用計(jì)數(shù)變成了 0 ,對(duì)象最終被釋放。

場景 3:同理,當(dāng)出了 @autoreleasepool {} 的作用域時(shí),其中的 autoreleased 對(duì)象被 release ,對(duì)象的引用計(jì)數(shù)變成 1 。當(dāng)出了局部變量 string 的作用域,即 viewDidLoad 方法返回時(shí),string 指向了 nil ,其所指向?qū)ο蟮囊糜?jì)數(shù)變成 0 ,對(duì)象最終被釋放。

理解在這 3 種場景下,autoreleased 對(duì)象什么時(shí)候釋放對(duì)我們理解 Objective-C 的內(nèi)存管理機(jī)制非常有幫助。其中,場景 1 出現(xiàn)得最多,就是不需要我們手動(dòng)添加 @autoreleasepool {} 的情況,直接使用系統(tǒng)維護(hù)的 autoreleasepool ;場景 2 就是需要我們手動(dòng)添加 @autoreleasepool {} 的情況,手動(dòng)干預(yù) autoreleased 對(duì)象的釋放時(shí)機(jī);場景 3 是為了區(qū)別場景 2 而引入的,在這種場景下并不能達(dá)到出了 @autoreleasepool {} 的作用域時(shí) autoreleased 對(duì)象被釋放的目的。

PS:請(qǐng)讀者參考場景 1 的分析過程,使用 lldb 命令 watchpoint 自行驗(yàn)證下在場景 2 和場景 3 下 autoreleased 對(duì)象的釋放時(shí)機(jī),you should give it a try yourself 。

AutoreleasePoolPage

細(xì)心的讀者應(yīng)該已經(jīng)有所察覺,我們?cè)谏厦嬉呀?jīng)提到了 -[NSAutoreleasePool release] 方法最終是通過調(diào)用 AutoreleasePoolPage::pop(void *) 函數(shù)來負(fù)責(zé)對(duì) autoreleasepool 中的 autoreleased 對(duì)象執(zhí)行 release 操作的。

那這里的 AutoreleasePoolPage 是什么東西呢?其實(shí),autoreleasepool 是沒有單獨(dú)的內(nèi)存結(jié)構(gòu)的,它是通過以 AutoreleasePoolPage 為結(jié)點(diǎn)的雙向鏈表來實(shí)現(xiàn)的。我們打開 runtime 的源碼工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的實(shí)現(xiàn)源碼。通過閱讀源碼,我們可以知道:

每一個(gè)線程的 autoreleasepool 其實(shí)就是一個(gè)指針的堆棧;

每一個(gè)指針代表一個(gè)需要 release 的對(duì)象或者 POOL_SENTINEL(哨兵對(duì)象,代表一個(gè) autoreleasepool 的邊界);

一個(gè) pool token 就是這個(gè) pool 所對(duì)應(yīng)的 POOL_SENTINEL 的內(nèi)存地址。當(dāng)這個(gè) pool 被 pop 的時(shí)候,所有內(nèi)存地址在 pool token 之后的對(duì)象都會(huì)被 release ;

這個(gè)堆棧被劃分成了一個(gè)以 page 為結(jié)點(diǎn)的雙向鏈表。pages 會(huì)在必要的時(shí)候動(dòng)態(tài)地增加或刪除;

Thread-local storage(線程局部存儲(chǔ))指向 hot page ,即最新添加的 autoreleased 對(duì)象所在的那個(gè) page 。

一個(gè)空的 AutoreleasePoolPage 的內(nèi)存結(jié)構(gòu)如下圖所示:

magic 用來校驗(yàn) AutoreleasePoolPage 的結(jié)構(gòu)是否完整;

next 指向最新添加的 autoreleased 對(duì)象的下一個(gè)位置,初始化時(shí)指向 begin() ;

thread 指向當(dāng)前線程;

parent 指向父結(jié)點(diǎn),第一個(gè)結(jié)點(diǎn)的 parent 值為 nil ;

child 指向子結(jié)點(diǎn),最后一個(gè)結(jié)點(diǎn)的 child 值為 nil ;

depth 代表深度,從 0 開始,往后遞增 1;

hiwat 代表 high water mark 。

另外,當(dāng) next == begin() 時(shí),表示 AutoreleasePoolPage 為空;當(dāng) next == end() 時(shí),表示 AutoreleasePoolPage 已滿。

Autorelease Pool Blocks

我們使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼:


@autoreleasepool?{

}

將會(huì)得到以下輸出結(jié)果(只保留了相關(guān)代碼):


extern"C"__declspec(dllimport)?void?*?objc_autoreleasePoolPush(void);

extern"C"__declspec(dllimport)?void?objc_autoreleasePoolPop(void?*);

struct?__AtAutoreleasePool?{

__AtAutoreleasePool()?{atautoreleasepoolobj?=?objc_autoreleasePoolPush();}

~__AtAutoreleasePool()?{objc_autoreleasePoolPop(atautoreleasepoolobj);}

void?*?atautoreleasepoolobj;

};

/*?@autoreleasepool?*/{?__AtAutoreleasePool?__autoreleasepool;

}

不得不說,蘋果對(duì) @autoreleasepool {} 的實(shí)現(xiàn)真的是非常巧妙,真正可以稱得上是代碼的藝術(shù)。蘋果通過聲明一個(gè) __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實(shí)現(xiàn) @autoreleasepool {} 。當(dāng)聲明 __autoreleasepool 變量時(shí),構(gòu)造函數(shù) __AtAutoreleasePool() 被調(diào)用,即執(zhí)行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當(dāng)出了當(dāng)前作用域時(shí),析構(gòu)函數(shù) ~__AtAutoreleasePool() 被調(diào)用,即執(zhí)行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是說 @autoreleasepool {} 的實(shí)現(xiàn)代碼可以進(jìn)一步簡化如下:


/*?@autoreleasepool?*/{

void?*atautoreleasepoolobj?=?objc_autoreleasePoolPush();

//?用戶代碼,所有接收到?autorelease?消息的對(duì)象會(huì)被添加到這個(gè)?autoreleasepool?中

objc_autoreleasePoolPop(atautoreleasepoolobj);

}

因此,單個(gè) autoreleasepool 的運(yùn)行過程可以簡單地理解為 objc_autoreleasePoolPush()、[對(duì)象 autorelease] 和 objc_autoreleasePoolPop(void *) 三個(gè)過程。

push 操作

上面提到的 objc_autoreleasePoolPush() 函數(shù)本質(zhì)上就是調(diào)用的 AutoreleasePoolPage 的 push 函數(shù)。


void?*

objc_autoreleasePoolPush(void)

{

if(UseGC)returnnil;

returnAutoreleasePoolPage::push();

}

因此,我們接下來看看 AutoreleasePoolPage 的 push 函數(shù)的作用和執(zhí)行過程。一個(gè) push 操作其實(shí)就是創(chuàng)建一個(gè)新的 autoreleasepool ,對(duì)應(yīng) AutoreleasePoolPage 的具體實(shí)現(xiàn)就是往 AutoreleasePoolPage 中的 next 位置插入一個(gè) POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的內(nèi)存地址。這個(gè)地址也就是我們前面提到的 pool token ,在執(zhí)行 pop 操作的時(shí)候作為函數(shù)的入?yún)ⅰ?/p>

1

2

3

4

5

6

static?inline?void?*push()

{

id?*dest?=?autoreleaseFast(POOL_SENTINEL);

assert(*dest?==?POOL_SENTINEL);

returndest;

}

push 函數(shù)通過調(diào)用 autoreleaseFast 函數(shù)來執(zhí)行具體的插入操作。

1

2

3

4

5

6

7

8

9

10

11

static?inline?id?*autoreleaseFast(id?obj)

{

AutoreleasePoolPage?*page?=?hotPage();

if(page?&&?!page->full())?{

returnpage->add(obj);

}elseif(page)?{

returnautoreleaseFullPage(obj,?page);

}else{

returnautoreleaseNoPage(obj);

}

}

autoreleaseFast 函數(shù)在執(zhí)行一個(gè)具體的插入操作時(shí),分別對(duì)三種情況進(jìn)行了不同的處理:

當(dāng)前 page 存在且沒有滿時(shí),直接將對(duì)象添加到當(dāng)前 page 中,即 next 指向的位置;

當(dāng)前 page 存在且已滿時(shí),創(chuàng)建一個(gè)新的 page ,并將對(duì)象添加到新創(chuàng)建的 page 中;

當(dāng)前 page 不存在時(shí),即還沒有 page 時(shí),創(chuàng)建第一個(gè) page ,并將對(duì)象添加到新創(chuàng)建的 page 中。

每調(diào)用一次 push 操作就會(huì)創(chuàng)建一個(gè)新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個(gè) POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的內(nèi)存地址。

autorelease 操作

通過 NSObject.mm 源文件,我們可以找到 -autorelease 方法的實(shí)現(xiàn):

1

2

3

-?(id)autorelease?{

return((id)self)->rootAutorelease();

}

通過查看 ((id)self)->rootAutorelease() 的方法調(diào)用,我們發(fā)現(xiàn)最終調(diào)用的就是 AutoreleasePoolPage 的 autorelease 函數(shù)。

1

2

3

4

5

6

7

__attribute__((noinline,used))

id

objc_object::rootAutorelease2()

{

assert(!isTaggedPointer());

returnAutoreleasePoolPage::autorelease((id)this);

}

AutoreleasePoolPage 的 autorelease 函數(shù)的實(shí)現(xiàn)對(duì)我們來說就比較容量理解了,它跟 push 操作的實(shí)現(xiàn)非常相似。只不過 push 操作插入的是一個(gè) POOL_SENTINEL ,而 autorelease 操作插入的是一個(gè)具體的 autoreleased 對(duì)象。


static?inline?id?autorelease(id?obj)

{

assert(obj);

assert(!obj->isTaggedPointer());

id?*dest?__unused?=?autoreleaseFast(obj);

assert(!dest??||??*dest?==?obj);

returnobj;

}

pop 操作

同理,前面提到的 objc_autoreleasePoolPop(void *) 函數(shù)本質(zhì)上也是調(diào)用的 AutoreleasePoolPage 的 pop 函數(shù)。


void

objc_autoreleasePoolPop(void?*ctxt)

{

if(UseGC)return;

//?fixmerdar://9167170

if(!ctxt)return;

AutoreleasePoolPage::pop(ctxt);

}

pop 函數(shù)的入?yún)⒕褪?push 函數(shù)的返回值,也就是 POOL_SENTINEL 的內(nèi)存地址,即 pool token 。當(dāng)執(zhí)行 pop 操作時(shí),內(nèi)存地址在 pool token 之后的所有 autoreleased 對(duì)象都會(huì)被 release 。直到 pool token 所在 page 的 next 指向 pool token 為止。

下面是某個(gè)線程的 autoreleasepool 堆棧的內(nèi)存結(jié)構(gòu)圖,在這個(gè) autoreleasepool 堆棧中總共有兩個(gè) POOL_SENTINEL ,即有兩個(gè) autoreleasepool 。該堆棧由三個(gè) AutoreleasePoolPage 結(jié)點(diǎn)組成,第一個(gè) AutoreleasePoolPage 結(jié)點(diǎn)為 coldPage() ,最后一個(gè) AutoreleasePoolPage 結(jié)點(diǎn)為 hotPage() 。其中,前兩個(gè)結(jié)點(diǎn)已經(jīng)滿了,最后一個(gè)結(jié)點(diǎn)中保存了最新添加的 autoreleased 對(duì)象 objr3 的內(nèi)存地址。

此時(shí),如果執(zhí)行 pop(token1) 操作,那么該 autoreleasepool 堆棧的內(nèi)存結(jié)構(gòu)將會(huì)變成如下圖所示:

NSThread、NSRunLoop 和 NSAutoreleasePool

根據(jù)蘋果官方文檔中對(duì)NSRunLoop的描述,我們可以知道每一個(gè)線程,包括主線程,都會(huì)擁有一個(gè)專屬的 NSRunLoop 對(duì)象,并且會(huì)在有需要的時(shí)候自動(dòng)創(chuàng)建。

Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.

同樣的,根據(jù)蘋果官方文檔中對(duì)NSAutoreleasePool的描述,我們可知,在主線程的 NSRunLoop 對(duì)象(在系統(tǒng)級(jí)別的其他線程中應(yīng)該也是如此,比如通過 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 獲取到的線程)的每個(gè) event loop 開始前,系統(tǒng)會(huì)自動(dòng)創(chuàng)建一個(gè) autoreleasepool ,并在 event loop 結(jié)束時(shí) drain 。我們上面提到的場景 1 中創(chuàng)建的 autoreleased 對(duì)象就是被系統(tǒng)添加到了這個(gè)自動(dòng)創(chuàng)建的 autoreleasepool 中,并在這個(gè) autoreleasepool 被 drain 時(shí)得到釋放。

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

另外,NSAutoreleasePool 中還提到,每一個(gè)線程都會(huì)維護(hù)自己的 autoreleasepool 堆棧。換句話說 autoreleasepool 是與線程緊密相關(guān)的,每一個(gè) autoreleasepool 只對(duì)應(yīng)一個(gè)線程。

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

弄清楚 NSThread、NSRunLoop 和 NSAutoreleasePool 三者之間的關(guān)系可以幫助我們從整體上了解 Objective-C 的內(nèi)存管理機(jī)制,清楚系統(tǒng)在背后到底為我們做了些什么,理解整個(gè)運(yùn)行機(jī)制等。

總結(jié)

看到這里,相信你應(yīng)該對(duì) Objective-C 的內(nèi)存管理機(jī)制有了更進(jìn)一步的認(rèn)識(shí)。通常情況下,我們是不需要手動(dòng)添加 autoreleasepool 的,使用線程自動(dòng)維護(hù)的 autoreleasepool 就好了。根據(jù)蘋果官方文檔中對(duì)Using Autorelease Pool Blocks的描述,我們知道在下面三種情況下是需要我們手動(dòng)添加 autoreleasepool 的:

如果你編寫的程序不是基于 UI 框架的,比如說命令行工具;

如果你編寫的循環(huán)中創(chuàng)建了大量的臨時(shí)對(duì)象;

如果你創(chuàng)建了一個(gè)輔助線程。

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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