IOS優(yōu)化篇之啟動(dòng)速度優(yōu)化(一)

啟動(dòng)速度

用戶從點(diǎn)擊APP圖標(biāo)到完全看到APP內(nèi)容的過(guò)程稱為啟動(dòng),如果啟動(dòng)耗時(shí)較長(zhǎng)可能會(huì)影響用戶的體驗(yàn),所以啟動(dòng)速度優(yōu)化就顯得很有必要。

  • 最佳速度:400ms,這是剛好是啟動(dòng)動(dòng)畫(huà)的時(shí)間,這是app啟動(dòng)時(shí)間的最佳時(shí)間。業(yè)界建議啟動(dòng)時(shí)間保持在1.5s內(nèi)比較合適。

  • 最慢速度:超過(guò)20s,則會(huì)被系統(tǒng)殺掉。

啟動(dòng)的分類

1、冷啟動(dòng):系統(tǒng)里沒(méi)有APP的進(jìn)程緩存信息,例如重啟手機(jī)或者更新APP后的首次啟動(dòng)APP,APP長(zhǎng)時(shí)間不用系統(tǒng)清掉已有的進(jìn)程緩存

2、熱啟動(dòng):系統(tǒng)里有APP的進(jìn)程緩存信息,例如殺死APP后短時(shí)間內(nèi)重啟APP

3、回前臺(tái):APP退入后臺(tái)再進(jìn)入前臺(tái),APP進(jìn)程從掛起到激活狀態(tài)

一般只討論1、2兩種情況的啟動(dòng)優(yōu)化。如何從代碼層面計(jì)算啟動(dòng)速度?根據(jù)蘋果官方文檔的計(jì)算方式:進(jìn)程創(chuàng)建時(shí)間到第一個(gè)CA::Transaction::commit()

啟動(dòng)流程

如下引用自:抖音品質(zhì)建設(shè) - iOS啟動(dòng)優(yōu)化《原理篇》

Snip20211224_2.png

1、點(diǎn)擊APP圖標(biāo)后,內(nèi)核創(chuàng)建APP進(jìn)程

2、將APP的Mach-O可執(zhí)行文件mmap進(jìn)虛擬內(nèi)存,加載dyld程序,接下來(lái)調(diào)用_dyld_start函數(shù)開(kāi)始程序的初始化

3、重啟手機(jī)/更新APP會(huì)先創(chuàng)建啟動(dòng)閉包,然后根據(jù)啟動(dòng)閉包進(jìn)行相關(guān)的初始化

4、將動(dòng)態(tài)庫(kù)mmap進(jìn)虛擬內(nèi)存,動(dòng)態(tài)庫(kù)數(shù)量太多則這里耗時(shí)會(huì)增加

5、對(duì)動(dòng)態(tài)庫(kù)和APP的Mach-O可執(zhí)行文件做bind&rebase,主要耗時(shí)在 Page In,影響 Page In 數(shù)量的是 objc 的元數(shù)據(jù)

6、初始化 objc 的 runtime,如果有了閉包,由于閉包已經(jīng)初始化了大部分,這里只會(huì)注冊(cè) sel 和裝載 category

7、+load 和靜態(tài)初始化被調(diào)用,除了方法本身耗時(shí),這里還會(huì)引起大量 Page In

8、初始化 UIApplication,啟動(dòng) Main Runloop

9、執(zhí)行 will/didFinishLaunch,這里主要是業(yè)務(wù)代碼耗時(shí)

10、Layout,viewDidLoad 和 Layoutsubviews 會(huì)在這里調(diào)用,Autolayout 太多會(huì)影響這部分時(shí)間

11、Display,drawRect 會(huì)調(diào)用

12、Prepare,圖片解碼發(fā)生在這一步

13、Commit,首幀渲染數(shù)據(jù)打包發(fā)給 RenderServer,啟動(dòng)結(jié)束

啟動(dòng)速度優(yōu)化思路:

1、控制APP的可執(zhí)行文件大小

2、控制動(dòng)態(tài)庫(kù)數(shù)量

3、控制Page In 次數(shù)

4、控制首幀渲染前業(yè)務(wù)邏輯相關(guān)耗時(shí)

5、控制首幀視圖渲染耗時(shí),即上面流程中的步驟10-12

Tips:iOS13之后系統(tǒng)采用dyld3加載器,才有啟動(dòng)閉包的機(jī)制。之前使用的是dyld2,無(wú)此機(jī)制

優(yōu)化啟動(dòng)速度

1、動(dòng)態(tài)庫(kù)數(shù)量

解決方案:將項(xiàng)目中的動(dòng)態(tài)庫(kù)全部替換成靜態(tài)庫(kù)。

目前項(xiàng)目中有32個(gè)三方庫(kù)采用動(dòng)態(tài)庫(kù)方式引入,蘋果推薦的是最多不要超過(guò)6個(gè)動(dòng)態(tài)庫(kù)。先看如下一組試驗(yàn)數(shù)據(jù):

APP進(jìn)程創(chuàng)建到首次+load調(diào)用時(shí)間

測(cè)試設(shè)備ipad pro 2,iOS13.4,冷啟動(dòng)5次;

動(dòng)態(tài)庫(kù)方式耗時(shí):721.7ms、619.6ms、782.8ms、761.0ms、724.8ms

靜態(tài)庫(kù)方式耗時(shí):143.9ms、133.1ms、139.7ms、145.8ms、151.4ms

測(cè)試設(shè)備ipad pro 2,iOS13.4,熱啟動(dòng)5次;

動(dòng)態(tài)庫(kù)方式耗時(shí):122.6ms、117.4ms、121.3ms、120.7ms、120.5ms

靜態(tài)庫(kù)方式耗時(shí):36.1ms、37.0ms、36.6ms、36.1ms、36.2ms

測(cè)試設(shè)備ipad air,iOS12.1.1,熱啟動(dòng)5次;

動(dòng)態(tài)庫(kù)方式耗時(shí):524.3ms、3435.9ms、3861.4ms、1927.8ms、474.9ms

靜態(tài)庫(kù)方式耗時(shí):3145.2ms、4393.3ms、3095.3ms、3126.5ms、2567.0ms

結(jié)論:如果項(xiàng)目中有很多動(dòng)態(tài)庫(kù),以本項(xiàng)目32個(gè)動(dòng)態(tài)庫(kù)為例,iPad Pro2 測(cè)試結(jié)果,冷啟動(dòng)耗時(shí)減少600ms+,熱啟動(dòng)耗時(shí)減少80ms+

疑問(wèn):

1、靜態(tài)庫(kù)增加了APP可執(zhí)行文件的體積,必然導(dǎo)致dyld加載可執(zhí)行文件耗時(shí)增加,TEXT解密耗時(shí)增加。是否靜態(tài)庫(kù)被鏈接的文件數(shù)量或者代碼量大于某個(gè)值后采用動(dòng)態(tài)庫(kù)更合適?

答案:暫未研究

2、采用靜態(tài)庫(kù)的方案是否會(huì)導(dǎo)致APP的體積大幅增加?

答案:本項(xiàng)目中并未增加APP體積,反而減小,如下為對(duì)比

Snip20211224_5.png

3、采用靜態(tài)庫(kù)方式可能導(dǎo)致的問(wèn)題?(之所以有這樣的思考,看到文章中提到了如下的觀點(diǎn):)

  1. 三方庫(kù)中使用了[NSBundle bundleForClass:[self class]]的行為會(huì)和[NSBundle mainBundle]一致。
  2. 由于上一個(gè)問(wèn)題可能導(dǎo)致Bundle找不到的問(wèn)題(目前正在嘗試能否處理)。

本文采用的靜態(tài)方式use_modular_headers!和上面文章中方式不一樣(很明顯更加簡(jiǎn)單,自動(dòng)實(shí)現(xiàn)了上文中資源拷貝的功能)

利用cocopods管理三方庫(kù)的資源還是比較智能的,以下兩個(gè)選項(xiàng)用于指定需要拷貝到主工程的資源文件和文件夾語(yǔ)法,默認(rèn)將這些資源拷貝到主工程的MainBundle中(但如果三方庫(kù)資源和主工程資源文件重名則會(huì)報(bào)錯(cuò)),不指定則不拷貝

  spec.resource  = "ic_loading_004.png"
  spec.resources = "Resources/*.png"

如下則將資源拷貝到指定的Bundle中,盡量避免和主工程沖突

  spec.resource_bundles = {
    'resources' => ['Resources']
  }

2、無(wú)用代碼

思考:無(wú)用代碼會(huì)增加APP可執(zhí)行文件的大小嗎?

無(wú)用代碼包括第三方庫(kù)中的無(wú)用代碼和自身工程中未使用到的無(wú)用代碼。現(xiàn)在分別做試驗(yàn)(設(shè)備iPad Pro2 iOS13.4),隨便創(chuàng)建一個(gè)工程,打release包,可執(zhí)行文件最終大小109k,APP最終大小為133k

1、自身工程中未使用到的無(wú)用代碼

隨便拖入一堆無(wú)用代碼后(工程中不使用)打出來(lái)的APP包大小201k,可執(zhí)行文件大小變?yōu)?76k。

2、未使用到的無(wú)用第三方靜態(tài)庫(kù)

拖入ffmpeg靜態(tài)庫(kù),打出來(lái)的APP包仍然為133k,可執(zhí)行文件大小仍然為109k

3、未使用到的無(wú)用第三方動(dòng)態(tài)庫(kù)

拖入Alamofire.framework動(dòng)態(tài)庫(kù),打出來(lái)的APP包大小2.1M,可執(zhí)行文件大小仍然為109k。APP包增加的大小為Alamofire.framework庫(kù)本身的大小

答案:項(xiàng)目工程中未使用到的無(wú)用代碼最終會(huì)編譯到APP的可執(zhí)行文件中區(qū),所以會(huì)導(dǎo)致APP可執(zhí)行文件體積增加。當(dāng)APP可執(zhí)行文件變大時(shí),會(huì)導(dǎo)致dyld加載可執(zhí)行文件的耗時(shí)增加,即增加了啟動(dòng)時(shí)間

Tips:統(tǒng)計(jì)項(xiàng)目中代碼行數(shù)(包括注釋和空格)

cd 項(xiàng)目目錄
find . ! -path "./*.framework/*" ! -path "./tttt/*" "(" -name "*.m" -or -name "*.mm" -or -name "*.cpp" -or -name "*.h" -or -name "*.swift" ")" -print | xargs wc -l

解決方案:

目前業(yè)界給出了兩種方案:1、自定義Pass、在函數(shù)頭部插樁,在編譯期間確定代碼是否被使用,優(yōu)點(diǎn)是準(zhǔn)確,缺點(diǎn)需要查看llvm源碼,實(shí)現(xiàn)難度大,目前也只是停留在理論階段,并無(wú)具體的解決方案;2、基于Mach-O可執(zhí)行文件格式中的字段來(lái)確定類或者函數(shù)是否被使用,優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,缺點(diǎn)是不夠準(zhǔn)確,只能做靜態(tài)分析,動(dòng)態(tài)調(diào)用代碼無(wú)法查出來(lái),所以刪代碼前需要二次確認(rèn),目前也有很多不錯(cuò)的實(shí)現(xiàn)方案。

采用業(yè)界開(kāi)源方案:WBBlades 分析項(xiàng)目中的無(wú)用類,該工具支持OC和Swift的檢測(cè),且使用簡(jiǎn)單。安裝該工具后執(zhí)行如下命令:

blades -unused /Users/ws/Library/Developer/Xcode/DerivedData/FilmoraGo-blntnxezkthnqjdamaarjzspcgmz/Build/Products/Debug-iphoneos/FilmoraGo.app

最終輸出項(xiàng)目中的無(wú)用類到桌面UnusedClass.plist文件中

Snip20211228_2.png

結(jié)論:

經(jīng)過(guò)對(duì)比,最終減少安裝包0.3M左右,對(duì)啟動(dòng)時(shí)間基本沒(méi)有幫助,此外再刪除無(wú)用類的過(guò)程中發(fā)現(xiàn)一下現(xiàn)象,予以記錄

如下情況類仍然被該工具標(biāo)記為未使用:

1、Swift中一個(gè)類聲明為Private

2、collectionView.register()

疑問(wèn):

1、目前項(xiàng)目中無(wú)用類比較少,刪除無(wú)用類文件后,對(duì)安裝包和啟動(dòng)時(shí)間影響不大,故減少無(wú)用類對(duì)啟動(dòng)時(shí)間的幫助量化目前還未得知

2、通過(guò)Pod方式引入的庫(kù)也檢查出來(lái)了很多無(wú)用類,有沒(méi)有辦法過(guò)濾掉Pods庫(kù)中的無(wú)用類?

參考文章:

Mach-O 文件結(jié)構(gòu)

從Mach-O角度談?wù)凷wift和OC的存儲(chǔ)差異

MachOView

58同城iOS混編項(xiàng)目無(wú)用代碼檢測(cè)方案介紹

3、二進(jìn)制重排

思考:

二進(jìn)制重排為什么會(huì)加快啟動(dòng)速度?

當(dāng)APP進(jìn)程訪問(wèn)一頁(yè)虛擬內(nèi)存page,而對(duì)應(yīng)的物理內(nèi)存不存在時(shí),先觸發(fā)缺頁(yè)中斷(Page Fault)阻塞當(dāng)前進(jìn)程,然后加載數(shù)據(jù)到對(duì)應(yīng)物理內(nèi)存(Release版本還要對(duì)加載的數(shù)據(jù)進(jìn)行簽名),所以缺頁(yè)中斷還是比較耗時(shí)的。假設(shè)APP啟動(dòng)時(shí)調(diào)用100個(gè)函數(shù),這100個(gè)函數(shù)如果分布在100個(gè)不同的內(nèi)存頁(yè),那會(huì)產(chǎn)生100次缺頁(yè)中斷。如通過(guò)二進(jìn)制重排將這100函數(shù)分布到50個(gè)或者更少的內(nèi)存頁(yè)中,缺頁(yè)中斷的次數(shù)減半,啟動(dòng)速度就提升了 。

獲取啟動(dòng)階段Page Fault的次數(shù)

打開(kāi)Instruments,選擇System Trace工具

Snip20220113_3.png

重啟手機(jī)(熱啟動(dòng)情況下系統(tǒng)已經(jīng)做了加載緩存,產(chǎn)生缺頁(yè)中斷大幅減少,所以最好重啟手機(jī)),然后點(diǎn)擊啟動(dòng),待首屏出現(xiàn)后停止,如下圖:

Snip20220113_6.png

以iPad Pro2代為測(cè)試設(shè)備,優(yōu)化前啟動(dòng)APP時(shí)產(chǎn)生缺頁(yè)中斷3969次,耗時(shí)303.82ms

解決方案

獲取啟動(dòng)階段調(diào)用的函數(shù)符號(hào)然后編寫order_file編譯順序文件然后在Build Settings -> Order File中配置一個(gè)后綴為order的文件路徑是實(shí)現(xiàn)二進(jìn)制重排的核心思路。目前業(yè)界獲取啟動(dòng)階段調(diào)用的函數(shù)符號(hào)主要有三種:

1、抖音通過(guò)靜態(tài)掃描和運(yùn)行時(shí) Trace 等方法確定 order_file。該方案實(shí)現(xiàn)難度大(需要匯編、反匯編等知識(shí)),且只能覆蓋部分符號(hào)(無(wú)法覆蓋純Swift、initialize、部分block 和 C++ 通過(guò)寄存器的間接函數(shù)調(diào)用)

2、手淘通過(guò)修改 .o 目標(biāo)文件實(shí)現(xiàn)靜態(tài)插樁,需要對(duì)目標(biāo)代碼較為熟悉,通用性不高

3、clang編譯器靜態(tài)插樁,目前業(yè)界已有成熟的庫(kù)和方案

靜態(tài)插樁:在 build settings->"Other C Flags"中添加"-fsanitize-coverage=func,trace-pc-guard"。如過(guò)項(xiàng)目中有 Swift 代碼,還需要在 "Other Swift Flags" 中加入"-sanitize-coverage=fun"和"-sanitize=undefined",如下:

Snip20220118_2.png

Snip20220118_1.png

靜態(tài)插樁腳本 獲取啟動(dòng)階段符號(hào)表的使用步驟:

1、在Podfile中添加如下代碼:

pod 'YCSymbolTracker'
post_install do |installer|
    require './Pods/YCSymbolTracker/YCSymbolTracker/symbol_tracker.rb'
    symbol_tracker(installer)
end

然后執(zhí)行pod install

2、再首幀完成渲染前調(diào)用如下代碼:

    // 首幀渲染完成后調(diào)用此方法,一般在跟控制器的viewDidAppear方法中調(diào)用即可
    static func runAfterFirstFrameRendered(){
        ....省略的業(yè)務(wù)代碼
        
        // 首幀渲染前調(diào)用監(jiān)控代碼
        let filePath = NSTemporaryDirectory().appending("/demo.order")
        YCSymbolTracker.exportSymbols(filePath: filePath)
    }

3、關(guān)機(jī)然后打開(kāi)APP,獲取啟動(dòng)階段的符號(hào)表

Snip20220208_1.png

結(jié)論:
最后獲得150個(gè)page in fault的提升,大概50ms左右的速度提升

參考文章

[抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案,APP 啟動(dòng)速度提升超 15%](抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案 APP啟動(dòng)速度提升超15%)

手淘架構(gòu)組最新實(shí)踐 | iOS基于靜態(tài)庫(kù)插樁的?進(jìn)制重排啟動(dòng)優(yōu)化

深入探索 iOS 啟動(dòng)速度優(yōu)化(二進(jìn)制重排)

Clang 靜態(tài)插樁代碼

懶人版二進(jìn)制重排

4、首幀渲染前的業(yè)務(wù)邏輯優(yōu)化

這部分代表了main()函數(shù)之后的時(shí)間,即從didFinishLaunchingWithOptions()開(kāi)始到根控制器viewDidAppear函數(shù)結(jié)束主線程耗時(shí)的優(yōu)化

解決方案:

利用Xcode自帶的Instruments工具APP Launch分析啟動(dòng)耗時(shí),找出耗時(shí)嚴(yán)重的函數(shù)調(diào)用然后進(jìn)行優(yōu)化。該工具會(huì)追蹤應(yīng)用啟動(dòng)后5秒內(nèi)的所有線程的耗時(shí),自帶Time Profiler和應(yīng)用進(jìn)程的System Trace兩個(gè)看板,如下:

![Snip20211229_7.png](https://upload-images.jianshu.io/upload_images/7776337-9d11b033c4895fec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可以看到APP的啟動(dòng)時(shí)間為1.7秒(備注:通過(guò)這個(gè)工具是APP的冷啟動(dòng)時(shí)間),和之前通過(guò)檢測(cè)工具測(cè)出的APP冷啟動(dòng)時(shí)間還是比較吻合的。

接下來(lái)利用System Trace功能對(duì)APP啟動(dòng)后的業(yè)務(wù)邏輯進(jìn)行耗時(shí)分析,點(diǎn)擊System Trace看板(也就是上面FilmoraGo)左邊的右箭頭,選擇Main Thread,接下來(lái)就可以看到啟動(dòng)階段各個(gè)函數(shù)的耗時(shí)了

Snip20211230_1.png

可以看到這里面didFinishiLanchingWithOptions耗時(shí)197ms,加載解析首屏Main文件及創(chuàng)建相關(guān)視圖耗時(shí)20ms,首次Runloop接手了GCD主線程的代碼塊執(zhí)行耗時(shí)121ms。接著依次點(diǎn)開(kāi)這些耗時(shí)點(diǎn)逐個(gè)分析進(jìn)行優(yōu)化

1、在didFinishiLanchingWithOptions中將字體文件加載到內(nèi)存中(方法configure.fontPath = .. 的耗時(shí)),該方法耗時(shí)144ms,是大頭。經(jīng)過(guò)和業(yè)務(wù)同學(xué)溝通,可以將此動(dòng)作延后加載。可以看到這個(gè)方法是通過(guò)source0消息觸發(fā)的,在__CFRunLoopDoSources0代碼塊中調(diào)用

2、加載解析首屏Main文件及創(chuàng)建相關(guān)視圖耗時(shí)20ms,通過(guò)分析解析XML耗時(shí)3ms左右,基本可以忽略,主要時(shí)間消耗在一個(gè)視圖的創(chuàng)建上(大概10ms)。這段代碼也是在__CFRunLoopDoSources0中執(zhí)行

3、

結(jié)論:

具體改進(jìn)思路為:設(shè)計(jì)啟動(dòng)器,改啟動(dòng)器統(tǒng)一管理啟動(dòng)任務(wù),包括首屏選日前必須要加載的任務(wù)和可以在渲染完成后延后加載的任務(wù);目的是優(yōu)化啟動(dòng)速度。啟動(dòng)速度減少大概200ms

疑問(wèn):

這種方式是否可以全量覆蓋APP啟動(dòng)階段的耗時(shí)?如果不能,解決方案?

答案:不能全量檢測(cè),例如兩個(gè)方法A和B,滿足某個(gè)條件調(diào)用了A,滿足另外一個(gè)條件調(diào)用了B,而B(niǎo)非常耗時(shí),通過(guò)這種方式只是測(cè)試到了A的耗時(shí)。一種是線上監(jiān)控,不過(guò)任務(wù)過(guò)于艱巨。二是仔細(xì)排查代碼將這種AB邏輯相關(guān)代碼揪出來(lái)單獨(dú)進(jìn)行測(cè)試可能更加容易實(shí)現(xiàn)一些。

5、首幀視圖的渲染優(yōu)化

思考:

首幀視圖耗時(shí)即首次執(zhí)行CA::Transaction::commit()到將最終的渲染樹(shù)提交給渲染進(jìn)程結(jié)束所消耗的時(shí)間,同樣利用APP Lauch查看它的耗時(shí)如下:

Snip20211230_3.png

可以看到視圖的渲染是在代碼塊__CFRunLoopDoBlocks中調(diào)用的,整個(gè)耗時(shí)20ms,基本沒(méi)有優(yōu)化的空間。展開(kāi)這個(gè)調(diào)用棧,可以非常清楚的看到視圖渲染的整個(gè)流程。

優(yōu)化效果

以設(shè)備iPad Pro 2代為例

優(yōu)化前:冷啟動(dòng)5次,時(shí)間為:1449.2ms、1426.4ms、1300.1ms、1441.1ms、1351.1ms;熱啟動(dòng)5次,時(shí)間為:314.2ms、315.2ms、342.1ms、321.4ms、310.3ms

優(yōu)化后:冷啟動(dòng)5次,時(shí)間為:709.3ms、610.3ms、679.3ms、646.7ms、667.4.0ms;熱啟動(dòng)5次,時(shí)間為:

293.8ms、299.1ms、299.5ms、299.9ms、293.6ms

可以看到冷啟動(dòng)的優(yōu)化效果還是很明顯的,基本上在50%左右。熱啟動(dòng)也有一定的提升

思考

此為線下優(yōu)化啟動(dòng)速度的一次實(shí)踐,可以看到經(jīng)過(guò)這次優(yōu)化啟動(dòng)速度提升不少,那如何做到在業(yè)務(wù)快速迭代中啟動(dòng)速度不會(huì)持續(xù)增加呢?或者說(shuō)增加了能立馬知道,研發(fā)階段就快速優(yōu)化,不要等到累積到一定程度再來(lái)優(yōu)化。

1、首先研發(fā)階段針對(duì)啟動(dòng)相關(guān)代碼做code review,主要包括:

新增動(dòng)態(tài)庫(kù)

新增啟動(dòng)相關(guān)業(yè)務(wù)邏輯

2、將啟動(dòng)速度監(jiān)控自動(dòng)化

設(shè)計(jì)一套啟動(dòng)速度監(jiān)控自動(dòng)化系統(tǒng),當(dāng)發(fā)現(xiàn)啟動(dòng)速度增加了某個(gè)閾值給予開(kāi)發(fā)人員警告,并將耗時(shí)增加的方法打印出來(lái),相關(guān)業(yè)務(wù)人員針對(duì)性的優(yōu)化。

參考文章

抖音品質(zhì)建設(shè) - iOS啟動(dòng)優(yōu)化《原理篇》

抖音品質(zhì)建設(shè) - iOS啟動(dòng)優(yōu)化《實(shí)戰(zhàn)篇》

基于mach-o+反匯編的無(wú)用類檢測(cè)

iOS靜態(tài)庫(kù)&動(dòng)態(tài)庫(kù)依賴探索

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

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

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