iOS Crash及其符號化

符號

維基百科關(guān)于符號(Symbol)定義:

A symbol in computer programming is a primitive data type whose instances have a unique human-readable form.

在計算機編程中符號是一種原始的數(shù)據(jù)類型,其實例具有獨一無二的人類可讀形式。也就是說符號是一個數(shù)據(jù)結(jié)構(gòu),它包含了名稱和類型等元數(shù)據(jù),符號對應(yīng)一個函數(shù)或者數(shù)據(jù)的地址。

符號表(Symbol Table)

符號表是內(nèi)存地址與符號的映射表,存儲了當(dāng)前文件的符號信息,靜態(tài)鏈接器ld和動態(tài)鏈接器dyld在鏈接的過程中都會讀取符號表。

打包上線的時候(Release模式)會把調(diào)試符號等裁剪掉,但是線上一旦出現(xiàn)問題上報了Crash log我們?nèi)绾沃缹?yīng)的報錯代碼位置和調(diào)用堆棧呢,這就需要把符號寫到另外一個單獨的文件里,也就是dSYM文件。dSYM(debug symbols)是iOS的符號表文件,存儲著16進制地址信息和符號的映射文件,可以幫我們將Crash堆棧信息中的地址信息符號化。

文件名樣式:MyApp.app.dSYM,它可以使我們將堆棧信息中的地址信息還原成對應(yīng)的符號,以便于問題的定位和修復(fù)。

如何生成dSYM文件

crash_03.png

符號相關(guān)配置

Deployment Postprocessing

Deployment Postprocessing是編譯生成目標文件后是否要進行后續(xù)處理的配置項

  • 配置為Yes,編譯生成目標文件后要進行后續(xù)處理,比如符號裁剪

  • 配置為No,不會進行后續(xù)處理

Strip Linked Product

當(dāng)Deployment Postprocessing為Yes時,Strip Linked Product的設(shè)置才會有效。

  • 配置為Yes,進行符號裁剪

  • 配置為No,不進行符號裁剪

crash_02.png

日常開發(fā)過程中,我們是需要符號信息存在的,通常Debug模式下會將Deployment Postprocessing設(shè)置為No,Release模式下設(shè)置為Yes,這樣Debug模式下一旦有問題可以及時暴露并修復(fù)。

Crash分析

一般來說發(fā)現(xiàn)Crash有開發(fā)階段和發(fā)布線上階段兩種情況。

開發(fā)階段

開發(fā)階段debug模式下,在Xcode中碰到Crash時控制臺會打印出崩潰信息,幫助我們排查問題。此時log信息大致如下

2021-06-18 20:52:01.931022+0800 MyApp[2706:35625050] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber length]: unrecognized selector sent to instance 0xbed641d619417a05'
*** First throw call stack:
(
    0   CoreFoundation         0x00007fff20421af6 __exceptionPreprocess + 242
    1   libobjc.A.dylib        0x00007fff20177e78 objc_exception_throw + 48
    2   CoreFoundation         0x00007fff204306f7 +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
    3   CoreFoundation         0x00007fff20426036 ___forwarding___ + 1489
    4   CoreFoundation         0x00007fff20428068 _CF_forwarding_prep_0 + 120
    5   MyApp                  0x0000000101a2cfb0 -[MIHomeINViewController loadWebPageRequestWithUrl:] + 208
    6   MyApp                  0x0000000101a2ce9b -[MIHomeINViewController loadWebPageRequestWithUrl:moduleDict:type:frameIndex:] + 379
    7   CoreFoundation         0x00007fff204282fc __invoking___ + 140
    8   CoreFoundation         0x00007fff204257b6 -[NSInvocation invoke] + 303
    9   MyApp                  0x0000000101d7d56f __ASPECTS_ARE_BEING_CALLED__ + 4111
    10  CoreFoundation         0x00007fff20425dc0 ___forwarding___ + 859
    
    ...
    
)

這樣根據(jù)崩潰log信息我們能夠很快定位到崩潰問題所在的類文件、方法。

日常開發(fā)中還可以利用斷點來快速定位代碼崩潰位置。

crash_01.png

發(fā)布線上階段

前面講過了,一旦到了發(fā)布線上(Release)階段,符號會被裁剪掉,為什么要裁剪呢

  • 減少包體積大小

  • 避免被逆向分析(符號裁剪不能保證不被逆向分析,符號化就是逆向工程的研究重點研究內(nèi)容之一)

這就意味著Release包發(fā)生Crash后,拿到的Crash log是未經(jīng)符號化的.

一般Crash日志來源有以下兩種:

  • 蘋果收集的Crash日志

    • Xcode -> Window -> Organize -> Crashes中查看

    • 用戶手機設(shè)置 -> 隱私 -> 分析與改進 -> 分析數(shù)據(jù)里查看

  • 應(yīng)用內(nèi)收集

    • Crash收集SDK,比如項目中所用的OMGCrashReportsSDK,第三方的KSCrash等,上報到自建分析平臺

    • 接入APM產(chǎn)品,比如Bugly、Fabric等

這個階段拿到的Crash日志大部分都是以下樣式:

Incident Identifier: *******
CrashReporter Key: *******
Hardware Model: iPhone6,2
Process: MyApp [1148]
Path: /var/containers/Bundle/Application/*******/MyApp.app/MyApp
Identifier: *******
Code Type: ARM-64
Parent Process: ? [1]

Date/Time: 2021-05-11T11:24:28Z
Launch Time: 2021-05-11T11:21:59Z
OS Version: iOS 12.5.2 (16H30)
Report Version: 104

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000001
Crashed Thread: 0

Thread 0 Crashed:
0 libGPUSupportMercury.dylib 0x23c67efe4 0x23c67d000 + 8164
1 libGPUSupportMercury.dylib 0x23c67ffac 0x23c67d000 + 12204
2 xxxxxxxx 0x240ce8404 0x240cc2000 + 156676
3 GLEngine 0x241d92234 0x241cb1000 + 922164
4 OpenGLES 0x22393eaa4 0x223937000 + 31396
5 MyApp 0x10369c584 0x101044000 + 40207748     
6 MyApp 0x10368f9a0 0x101044000 + 40155552
7 MyApp 0x10370e7e0 0x101044000 + 40675296
8 MyApp 0x1036f4364 0x101044000 + 40567652
9 MyApp 0x10373aa64 0x101044000 + 40856164
10 MyApp 0x1036f2a54 0x101044000 + 40561236
11 MyApp 0x1036f1b78 0x101044000 + 40557432
12 QuartzCore 0x224b43ff0 0x224b32000 + 73712

...

Crash log分析

既然線上發(fā)布階段Crash log是未經(jīng)符號化的,那么一旦發(fā)生Crash,如何進行Crash分析呢?

先來看下Crash log的結(jié)構(gòu)以及每個字段包含的信息是什么,這些內(nèi)容可以幫助我們診斷崩潰來源的信息。

crash_04.png

一份Crash log打開后結(jié)構(gòu)劃分大致如圖所示。對我們來說需要重點關(guān)注以下三個部分:

  1. Header(標題),用來描述發(fā)生崩潰的環(huán)境

    Incident Identifier: // 報告唯一標識符
    CrashReporter Key:   // 匿名的每個設(shè)備的標識符
    Hardware Model:      // 運行應(yīng)用程序的設(shè)備型號
    Process:             // 崩潰進程的可執(zhí)行文件名
    Path:                // 可執(zhí)行文件在磁盤上的位置
    Identifier:          // 崩潰的進程
    Code Type:           // 崩潰進程的CPU架構(gòu)
    Parent Process:      // 啟動崩潰進程的名稱和進程ID
    Date/Time:           // 崩潰的日期和時間
    Launch Time:         // 應(yīng)用程序啟動的日期和時間
    OS Version:          // 發(fā)生崩潰的系統(tǒng)版本號
    
  2. Exception Information

    Exception Type:      // 終止進程的異常的名稱
    Exception Codes:     // 異常編碼信息
    

    這塊信息會告訴我們進程終止的原因是什么,但是它無法完全解釋應(yīng)用程序終止的原因,因為這塊提供的信息是有限的。

  3. Exception Backtrace

    Crashed Thread: 0
    
    Thread 0 Crashed:
    0 libGPUSupportMercury.dylib 0x23c67efe4 0x23c67d000 + 8164
    1 libGPUSupportMercury.dylib 0x23c67ffac 0x23c67d000 + 12204
    2 xxxxxxxx 0x240ce8404 0x240cc2000 + 156676
    3 GLEngine 0x241d92234 0x241cb1000 + 922164
    4 OpenGLES 0x22393eaa4 0x223937000 + 31396
    5 MyApp 0x10369c584 0x101044000 + 40207748     
    6 MyApp 0x10368f9a0 0x101044000 + 40155552
    7 MyApp 0x10370e7e0 0x101044000 + 40675296
    8 MyApp 0x1036f4364 0x101044000 + 40567652
    9 MyApp 0x10373aa64 0x101044000 + 40856164
    10 MyApp 0x1036f2a54 0x101044000 + 40561236
    11 MyApp 0x1036f1b78 0x101044000 + 40557432
    12 QuartzCore 0x224b43ff0 0x224b32000 + 73712
    

    Exception Backtrace記錄了程序終止時在線程上運行的代碼,回溯的代碼調(diào)用堆棧和Xcode調(diào)試暫時或崩潰時看到的類似,區(qū)別在于調(diào)用信息都是16進制地址。

    • 第一列數(shù)字序號代表堆棧幀號,堆棧幀調(diào)用順序排列,第0幀是最后在執(zhí)行的方法,第1幀是調(diào)用第0幀方法的方法,以此類推,也就是反向調(diào)用的順序。

    • 第二列(MyApp)代表正在執(zhí)行方法的二進制文件名

    • 第三列(0x10369c584)是正在執(zhí)行的機器指令的地址

    • 第四列(0x101044000)是要二進制鏡像入口地址,在符號化后會顯示為要執(zhí)行的方法名(SEL)

    • 第五列(+ 40207748)是從方法入口點到方法中當(dāng)前指令的字節(jié)偏移量

Crash符號化

  1. Xcode本身也提供了工具來幫助開發(fā)者完成符號化的工作。

    • symbolicatecrash,這是一個將堆棧地址符號化的腳本,執(zhí)行命令:./symbolicatecrash MyApp.crash MyApp.app.dSYM > MyApp.log。

      這種方式局限性較大

      • 只能分析官方格式的Crash log,Crash log需要從具體設(shè)備中取出,存在一定的限制性

      • 會出現(xiàn)符號化失敗的情況

    • atos,這個命令的特點是可以對單行堆棧進行符號化操作

      // 命令格式
      atos -arch <BinaryArchitecture> -o <PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName>  -l <LoadAddress> <AddressesToSymbolicate>
      
      // 示例
      atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x104a10000 0x00000001066f56f4
      // 輸出
      -[HomeViewController configureViewTabBarViewWhenApplicationActive] (in MyApp) (HomeViewController.m:370)
      

      atos命令可以把地址里的16進制信息替換成等價的符號。如果調(diào)試符號信息是完備的,則atos的輸出信息將會包含文件名和對應(yīng)的資源行數(shù)。atos命令可以被用來單獨符號化那些未符號化或者部分符號化過的crash log中的堆棧信息里的地址。

  2. Crash收集SDK收集Crash log并上傳自建分析平臺,打包時腳本上傳dSYM文件,在自建平臺完成log解析

    crash_05.png

    如圖所示,平臺在完成Crash log解析后,調(diào)用堆棧、崩潰發(fā)生App的信息、用戶信息等都完整的解析出來,對于開發(fā)同學(xué)來說是最友好的,能夠更加全面的對問題進行分析和解決。

特殊需求

上面的Crash分析說的都是針對普通情況下的,面對業(yè)務(wù)中的特殊場景和特殊需求,仍然有著特殊的Crash分析和解決方案。

比如在我們的業(yè)務(wù)中,App啟動時會根據(jù)業(yè)務(wù)的不同需求來加載AB兩套服務(wù),當(dāng)然A、B服務(wù)加載哪一個都能正常的保證App業(yè)務(wù)流程的正常使用。如果在App的啟動過程中A服務(wù)發(fā)生崩潰,不管是A服務(wù)自己SDK不穩(wěn)定導(dǎo)致,還是我們的使用存在問題,受影響的都會是用戶。

特殊場景需求:

  1. 能夠自動識別出是A服務(wù)的必現(xiàn)Crash,如果是偶現(xiàn)的crash不在兜底范圍之內(nèi)

  2. 確認是A服務(wù)導(dǎo)致Crash后,App下次啟動時切換為B服務(wù),等A服務(wù)修復(fù)完Crash之后能夠自動從兜底的底圖恢復(fù)到A服務(wù)

這個需求是要在線上發(fā)布的App中完成Crash的收集和分析處理,前面也講到了Release的App都會將符號裁剪掉,這種信息是無法做Crash分析處理的,但是線上Release包也沒有dSYM文件,這種況下如不借助dSYM文件一般是無法符號化的,那么有沒有其他思路進行符號化呢

符號化思路

從上面的Crash log可以了解到,調(diào)用堆棧的第三列是正在執(zhí)行的機器指令地址,那么這個地址(目標地址)再往前推進肯定就是當(dāng)前正在調(diào)用的方法地址,如果能夠拿到所有的方法地址,然后拿買一個方法地址和目標地址進行比較,與目標地址距離最近的那個地址所對應(yīng)的方法就是當(dāng)前正在調(diào)用的方法,也就是我們要得到的符號。

// 示例
5 MyApp 0x10369c584 0x101044000 + 40207748   

取0x10369c584和項目中拿到的每一個方法地址作比較,與0x10369c584差值最小的地址所對應(yīng)的方法和其所屬的類就是最終的目標符號信息。

取0x10369c584和項目中拿到的每一個方法地址作比較,與0x10369c584差值最小的地址所對應(yīng)的方法和其所屬的類就是最終的目標符號信息。</pre>

那么如何拿到所有的方法地址?

  1. 自己去解析內(nèi)存中加載的Mach-O文件,根據(jù)Mach-O文件格式先找到Class信息,然后找到對應(yīng)的Method信息,Method中保存了方法IMP(方法地址)和SEL(方法名)。但是這種方案涉及到了逆向工程的一些東西,過于復(fù)雜。

  2. App在點擊App啟動到main()之前,會使用dyld初始化運行環(huán)境,加載程序相關(guān)依賴庫,并對其鏈接和初始化,然后runtime會項目中所有類進行類結(jié)構(gòu)初始化,然后調(diào)用所有l(wèi)oad方法,最后dyld返回main函數(shù)地址,然后main函數(shù)被調(diào)用。這個過程會把程序中的所有方法加載到內(nèi)存中,我們在main()之后就可以使用objc提供的一系列方法拿到所有的Class和其對應(yīng)的Method。

    unsigned int classCount;
    const char **classNames;
    // 獲取指定類所在的動態(tài)庫
    const char * _Nonnull image = class_getImageName(self.class);
    // 獲取指定庫或框架中所有類的名稱
    classNames = objc_copyClassNamesForImage(image, &classCount);
    

綜合來看,對于當(dāng)前需求第二種方案會更加方便實現(xiàn)一些,那么現(xiàn)在來看第二種方案的技術(shù)實現(xiàn)。

技術(shù)實現(xiàn)

  1. 核心流程圖

    crash_07.png
  2. 代碼實現(xiàn)

    + (NSDictionary *)crashParseForKeyInfo:(NSArray<NSString *> *)stackSymbols {
        unsigned int classCount;
        const char **classNames;
        // 獲取指定類所在的動態(tài)庫
        const char * _Nonnull image = class_getImageName(self.class);
        // 獲取指定庫或框架中所有類的名稱
        classNames = objc_copyClassNamesForImage(image, &classCount);
        NSDictionary *methodAddressDict = [self getMethodAdressDict:classNames classCount:classCount];
    
        ...
    
        NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary];
        [stackSymbols enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
            ...
            __block NSString *crashMethodAddress = nil;
            [methodAddressDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary *obj, BOOL * _Nonnull stop) {
            
                ...
            
                if (crashMethodAddressNumber >= methodAddressNumber) {
                    tmpSpan = crashMethodAddressNumber - methodAddressNumber;
                    if (tmpSpan > kAdressSpanKey) {
                        return;
                    }
                    if (tmpSpan >= minSpan) {
                        return;
                    }
                    minSpan = tmpSpan;
                    crashMethodAddress = key;
                }
            }];
        
            NSDictionary *crashInfoDict = methodAddressDict[crashMethodAddress];
            tmpDict[kCrashClassNameKey] = crashInfoDict[kCrashClassNameKey];
            tmpDict[kCrashMethodNameKey] = crashInfoDict[kCrashMethodNameKey];
            *stop = YES;
        }];
        return [tmpDict copy];
    }
    
    // 獲取當(dāng)前項目所有類的className、methodName
    + (NSDictionary *)getMethodAdressDict:(const char **)classNames classCount:(unsigned int)count {
        NSMutableDictionary *tmpAddressDict = [NSMutableDictionary dictionary];
        for (unsigned int i = 0; i < count; i++) {
            const char *className = classNames[i];
            NSString *classNameStr = [NSString stringWithUTF8String:className];
            // 根據(jù)字段串反射為類對象
            Class cls = NSClassFromString(classNameStr);
            // 獲取當(dāng)前類對象的所有實例方法
            [tmpAddressDict addEntriesFromDictionary:[self getClassInfo:cls]];
            // 獲取當(dāng)前類對象的所有類方法
            [tmpAddressDict addEntriesFromDictionary:[self getClassInfo:object_getClass(cls)]];
        }
    
        return [tmpAddressDict copy];
    }
    
    // 獲取當(dāng)前類或元類的信息
    + (NSDictionary *)getClassInfo:(Class)cls {
        unsigned int methodCount;
        Method *methodList = class_copyMethodList(cls, &methodCount);
        NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary];
        for (unsigned int j = 0; j < methodCount; j++) {
            Method method = methodList[j];
            // 獲取方法IMP
            IMP imp = method_getImplementation(method);
            // 獲取方法SEL
            SEL selector = method_getName(method);
            NSString *methodAddress = [NSString stringWithFormat:@"%p", imp];
            NSMutableDictionary *tmpInfoDict = [NSMutableDictionary dictionary];
            NSString *className = NSStringFromClass(cls) ? NSStringFromClass(cls) : @"";
            NSString *methodName = NSStringFromSelector(selector) ? NSStringFromSelector(selector) : @"";
            tmpInfoDict[kCrashClassNameKey] = className;
            tmpInfoDict[kCrashMethodNameKey] = methodName;
            tmpDict[methodAddress] = [tmpInfoDict copy];
        }
        free(methodList);
    
        return [tmpDict copy];
    }
    
  3. 效果

    線上stackSymbols crash日志

    stackSymbols: 
    0   CoreFoundation                      0x000000018c7d6768 4FBDF167-161A-324C-A233-D516922C67E5 + 1218408;
    1   libobjc.A.dylib                     0x00000001a129d7a8 objc_exception_throw + 60;
    2   CoreFoundation                      0x000000018c6d55f8 4FBDF167-161A-324C-A233-D516922C67E5 + 165368;
    3   MyApp                               0x0000000102f37cd8 MyApp + 5536984;
    4   UIKitCore                           0x000000018f3d71d0 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 14537168;
    5   UIKitCore                           0x000000018f3d6d78 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 14536056;
    6   UIKitCore                           0x000000018f3d7538 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 14538040;
    7   UIKitCore                           0x000000018f6a1c98 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 17464472;
    8   UIKitCore                           0x000000018f1d46c4 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12428996;
    9   UIKitCore                           0x000000018f1c303c 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12357692;
    10  UIKitCore                           0x000000018f1f6f10 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12570384;
    11  CoreFoundation                      0x000000018c74f5e0 4FBDF167-161A-324C-A233-D516922C67E5 + 665056;
    12  CoreFoundation                      0x000000018c749704 4FBDF167-161A-324C-A233-D516922C67E5 + 640772;
    13  CoreFoundation                      0x000000018c749cb0 4FBDF167-161A-324C-A233-D516922C67E5 + 642224;
    14  CoreFoundation                      0x000000018c749360 CFRunLoopRunSpecific + 600;
    15  GraphicsServices                    0x00000001a3d87734 GSEventRunModal + 164;
    16  UIKitCore                           0x000000018f1c4584 33B02AB5-5DAF-3249-8DC6-5872DF830EC5 + 12363140;
    17  UIKitCore                           0x000000018f1c9df4 UIApplicationMain + 168;
    18  MyApp                               0x00000001029f40c8 MyApp + 16584;
    19  libdyld.dylib                       0x000000018c405cf8 E574A365-9878-348A-8E84-91E163CFC128 + 7416
    

    線上堆棧符號化結(jié)果:

    symbolsString: 
    className:HomeViewController; methodName:getName:;
    className:AppDelegate; methodName:application:didFinishLaunchingWithOptions:
    
  4. 方案缺點

    • 受限于objc相關(guān)方法的局限性,目前堆棧符號化僅僅是針對OC代碼的,C和C++代碼無法完成符號化。

    • 需要用項目所有方法(目前項目大約19萬條左右)進行遍歷,需要消耗較長時間完成符號化工作,根據(jù)需求特點做完優(yōu)化后耗時大約1s左右。

    但上述方案僅僅是在App啟動即發(fā)生Crash時才會進入上述符號化流程,故不會影響正常啟動。

問題

多個Crash信息收集功能并存

當(dāng)工程大到需要多個團隊共同開發(fā)一個App的時候,就可能會出現(xiàn)多個Crash收集功能并存的問題,如果處理不好后邊加入的收集功能可能就會影響已經(jīng)存在的Crash收集模塊的正常使用。

比如多方均通過NSSetUncaughtExceptionHandler注冊異常處理,如果想要讓大家的收集功能都能發(fā)揮各自的作用,可以采用下面的解決方案。

+ (void)registerHandlerWithMonitorString:(NSString *)key {
     if (NSGetUncaughtExceptionHandler() != MyExceptionHandler) {
        OldHandler = NSGetUncaughtExceptionHandler();
    }
    NSSetUncaughtExceptionHandler(&MyExceptionHandler);
}

void MyExceptionHandler(NSException *exception) {
    NSArray *callStack = exception.callStackSymbols;
    // 處理crash信息
    
    // 調(diào)用之前已經(jīng)注冊的handler
    if (OldHandler) {
        OldHandler(exception);
    }
}

在注冊時將之前別人注冊的handler取出備份,在自己的MyExceptionHandler中處理完后將別人的handler注冊傳遞回去,這樣大家都能完成自己的工作,皆大歡喜~

附錄

如何debug環(huán)境在Xcode中調(diào)試signal異常呢,因為Xcode屏蔽了signal回調(diào),所以需要lldb命令來幫助我們拿到回調(diào)信息。在執(zhí)行abort()代碼處打斷點,執(zhí)行到這行代碼時控制臺輸入:

pro hand -p true -s false SIGABRT

其中,SIGABRT可替換為其他signal異常類型?;剀嚭罂刂婆_輸出:

NAME         PASS   STOP   NOTIFY
===========  =====  =====  ======
SIGABRT      true   false  true 

表示已跳出Xcode屏蔽,可以拿到signal異?;卣{(diào)了。

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

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